add cc passthrough, diag tools dump, and search guidance in system prompt
- "cc" prefix messages bypass LLM backend and history, directly invoke claude -p - diag command now dumps all registered tools and sends as .md file - system prompt instructs LLM to use spawn_agent for search tasks - spawn_agent tool description updated to mention search/browser capabilities
This commit is contained in:
80
src/main.rs
80
src/main.rs
@@ -124,7 +124,7 @@ fn discover_tools() -> serde_json::Value {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "spawn_agent",
|
"name": "spawn_agent",
|
||||||
"description": "Spawn a Claude Code subagent to handle a complex task asynchronously. You'll be notified when it completes.",
|
"description": "Spawn a Claude Code subagent to handle a complex task asynchronously. The subagent has access to shell, browser, and search engine, making it ideal for web searches, information lookup, technical research, code tasks, and other complex operations. You'll be notified when it completes.",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -684,13 +684,20 @@ async fn handle_inner(
|
|||||||
let count = state.message_count(&sid).await;
|
let count = state.message_count(&sid).await;
|
||||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||||
let scratch = state.get_scratch().await;
|
let scratch = state.get_scratch().await;
|
||||||
let diag = format!(
|
let tools = discover_tools();
|
||||||
"session: {sid}\n\
|
let empty = vec![];
|
||||||
window: {count}/{MAX_WINDOW} (slide at {MAX_WINDOW}, drop {SLIDE_SIZE})\n\
|
let tools_arr = tools.as_array().unwrap_or(&empty);
|
||||||
total processed: {}\n\n\
|
|
||||||
persona ({} chars):\n{}\n\n\
|
let mut diag = format!(
|
||||||
scratch ({} chars):\n{}\n\n\
|
"# NOC Diag\n\n\
|
||||||
summary ({} chars):\n{}",
|
## Session\n\
|
||||||
|
- id: `{sid}`\n\
|
||||||
|
- window: {count}/{MAX_WINDOW} (slide at {MAX_WINDOW}, drop {SLIDE_SIZE})\n\
|
||||||
|
- total processed: {}\n\n\
|
||||||
|
## Persona ({} chars)\n```\n{}\n```\n\n\
|
||||||
|
## Scratch ({} chars)\n```\n{}\n```\n\n\
|
||||||
|
## Summary ({} chars)\n```\n{}\n```\n\n\
|
||||||
|
## Tools ({} registered)\n",
|
||||||
conv.total_messages + count,
|
conv.total_messages + count,
|
||||||
persona.len(),
|
persona.len(),
|
||||||
if persona.is_empty() { "(default)" } else { &persona },
|
if persona.is_empty() { "(default)" } else { &persona },
|
||||||
@@ -701,9 +708,41 @@ async fn handle_inner(
|
|||||||
"(empty)".to_string()
|
"(empty)".to_string()
|
||||||
} else {
|
} else {
|
||||||
conv.summary
|
conv.summary
|
||||||
}
|
},
|
||||||
|
tools_arr.len(),
|
||||||
);
|
);
|
||||||
bot.send_message(chat_id, diag).await?;
|
for tool in tools_arr {
|
||||||
|
let func = &tool["function"];
|
||||||
|
let name = func["name"].as_str().unwrap_or("?");
|
||||||
|
let desc = func["description"].as_str().unwrap_or("");
|
||||||
|
let params = serde_json::to_string_pretty(&func["parameters"])
|
||||||
|
.unwrap_or_default();
|
||||||
|
diag.push_str(&format!(
|
||||||
|
"### `{name}`\n{desc}\n\n```json\n{params}\n```\n\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = std::env::temp_dir().join(format!("noc-diag-{sid}.md"));
|
||||||
|
tokio::fs::write(&tmp, &diag).await?;
|
||||||
|
bot.send_document(chat_id, InputFile::file(&tmp))
|
||||||
|
.await?;
|
||||||
|
let _ = tokio::fs::remove_file(&tmp).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle "cc" prefix: pass directly to claude -p, no session, no history
|
||||||
|
if let Some(cc_prompt) = text.strip_prefix("cc").map(|s| s.trim_start()) {
|
||||||
|
if !cc_prompt.is_empty() {
|
||||||
|
info!(%sid, "cc passthrough");
|
||||||
|
let prompt = build_prompt(cc_prompt, &uploaded, &download_errors, &transcriptions);
|
||||||
|
match run_claude_streaming(&[], &prompt, bot, chat_id).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
error!(%sid, "cc claude: {e:#}");
|
||||||
|
let _ = bot.send_message(chat_id, format!("[error] {e:#}")).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,6 +914,8 @@ fn build_system_prompt(summary: &str, persona: &str) -> serde_json::Value {
|
|||||||
text.push_str(
|
text.push_str(
|
||||||
"\n\n你可以使用提供的工具来完成任务。\
|
"\n\n你可以使用提供的工具来完成任务。\
|
||||||
当需要执行命令、运行代码或启动复杂子任务时,直接调用对应的工具,不要只是描述你会怎么做。\
|
当需要执行命令、运行代码或启动复杂子任务时,直接调用对应的工具,不要只是描述你会怎么做。\
|
||||||
|
当需要搜索信息(如网页搜索、资料查找、技术调研等)时,使用 spawn_agent 启动一个子代理来完成搜索任务,\
|
||||||
|
子代理可以使用浏览器和搜索引擎,搜索完成后你会收到结果通知。\
|
||||||
输出格式:使用纯文本或基础Markdown(加粗、列表、代码块)。\
|
输出格式:使用纯文本或基础Markdown(加粗、列表、代码块)。\
|
||||||
不要使用LaTeX公式($...$)、特殊Unicode符号(→←↔)或HTML标签,Telegram无法渲染这些。",
|
不要使用LaTeX公式($...$)、特殊Unicode符号(→←↔)或HTML标签,Telegram无法渲染这些。",
|
||||||
);
|
);
|
||||||
@@ -887,11 +928,11 @@ fn build_system_prompt(summary: &str, persona: &str) -> serde_json::Value {
|
|||||||
serde_json::json!({"role": "system", "content": text})
|
serde_json::json!({"role": "system", "content": text})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build user message content, with optional images as multimodal input.
|
/// Build user message content, with optional images/videos as multimodal input.
|
||||||
fn build_user_content(
|
fn build_user_content(
|
||||||
text: &str,
|
text: &str,
|
||||||
scratch: &str,
|
scratch: &str,
|
||||||
images: &[PathBuf],
|
media: &[PathBuf],
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
let full_text = if scratch.is_empty() {
|
let full_text = if scratch.is_empty() {
|
||||||
text.to_string()
|
text.to_string()
|
||||||
@@ -899,9 +940,9 @@ fn build_user_content(
|
|||||||
format!("{text}\n\n[scratch]\n{scratch}")
|
format!("{text}\n\n[scratch]\n{scratch}")
|
||||||
};
|
};
|
||||||
|
|
||||||
// collect image data
|
// collect media data (images + videos)
|
||||||
let mut image_parts: Vec<serde_json::Value> = Vec::new();
|
let mut media_parts: Vec<serde_json::Value> = Vec::new();
|
||||||
for path in images {
|
for path in media {
|
||||||
let mime = match path
|
let mime = match path
|
||||||
.extension()
|
.extension()
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| e.to_str())
|
||||||
@@ -912,24 +953,27 @@ fn build_user_content(
|
|||||||
Some("png") => "image/png",
|
Some("png") => "image/png",
|
||||||
Some("gif") => "image/gif",
|
Some("gif") => "image/gif",
|
||||||
Some("webp") => "image/webp",
|
Some("webp") => "image/webp",
|
||||||
|
Some("mp4") => "video/mp4",
|
||||||
|
Some("webm") => "video/webm",
|
||||||
|
Some("mov") => "video/quicktime",
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
if let Ok(data) = std::fs::read(path) {
|
if let Ok(data) = std::fs::read(path) {
|
||||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
|
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
|
||||||
image_parts.push(serde_json::json!({
|
media_parts.push(serde_json::json!({
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": format!("data:{mime};base64,{b64}")}
|
"image_url": {"url": format!("data:{mime};base64,{b64}")}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if image_parts.is_empty() {
|
if media_parts.is_empty() {
|
||||||
// plain text — more compatible
|
// plain text — more compatible
|
||||||
serde_json::Value::String(full_text)
|
serde_json::Value::String(full_text)
|
||||||
} else {
|
} else {
|
||||||
// multimodal array
|
// multimodal array
|
||||||
let mut content = vec![serde_json::json!({"type": "text", "text": full_text})];
|
let mut content = vec![serde_json::json!({"type": "text", "text": full_text})];
|
||||||
content.extend(image_parts);
|
content.extend(media_parts);
|
||||||
serde_json::Value::Array(content)
|
serde_json::Value::Array(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user