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",
"function": {
"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": {
"type": "object",
"properties": {
@ -684,13 +684,20 @@ async fn handle_inner(
let count = state.message_count(&sid).await;
let persona = state.get_config("persona").await.unwrap_or_default();
let scratch = state.get_scratch().await;
let diag = format!(
"session: {sid}\n\
window: {count}/{MAX_WINDOW} (slide at {MAX_WINDOW}, drop {SLIDE_SIZE})\n\
total processed: {}\n\n\
persona ({} chars):\n{}\n\n\
scratch ({} chars):\n{}\n\n\
summary ({} chars):\n{}",
let tools = discover_tools();
let empty = vec![];
let tools_arr = tools.as_array().unwrap_or(&empty);
let mut diag = format!(
"# NOC Diag\n\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,
persona.len(),
if persona.is_empty() { "(default)" } else { &persona },
@ -701,9 +708,41 @@ async fn handle_inner(
"(empty)".to_string()
} else {
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(());
}
}
@ -875,6 +914,8 @@ fn build_system_prompt(summary: &str, persona: &str) -> serde_json::Value {
text.push_str(
"\n\n你可以使用提供的工具来完成任务。\
\
使 spawn_agent \
使\
使Markdown\
使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})
}
/// 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(
text: &str,
scratch: &str,
images: &[PathBuf],
media: &[PathBuf],
) -> serde_json::Value {
let full_text = if scratch.is_empty() {
text.to_string()
@ -899,9 +940,9 @@ fn build_user_content(
format!("{text}\n\n[scratch]\n{scratch}")
};
// collect image data
let mut image_parts: Vec<serde_json::Value> = Vec::new();
for path in images {
// collect media data (images + videos)
let mut media_parts: Vec<serde_json::Value> = Vec::new();
for path in media {
let mime = match path
.extension()
.and_then(|e| e.to_str())
@ -912,24 +953,27 @@ fn build_user_content(
Some("png") => "image/png",
Some("gif") => "image/gif",
Some("webp") => "image/webp",
Some("mp4") => "video/mp4",
Some("webm") => "video/webm",
Some("mov") => "video/quicktime",
_ => continue,
};
if let Ok(data) = std::fs::read(path) {
let b64 = base64::engine::general_purpose::STANDARD.encode(&data);
image_parts.push(serde_json::json!({
media_parts.push(serde_json::json!({
"type": "image_url",
"image_url": {"url": format!("data:{mime};base64,{b64}")}
}));
}
}
if image_parts.is_empty() {
if media_parts.is_empty() {
// plain text — more compatible
serde_json::Value::String(full_text)
} else {
// multimodal array
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)
}
}