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:
Fam Zheng 2026-04-09 17:59:48 +01:00
parent 128f2481c0
commit 9d5dd4eb16

View File

@ -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)
} }
} }