1use crate::constants::{
2 CLASSIFY_SIZE_THRESHOLD_BYTES, CLASSIFY_SIZE_THRESHOLD_CHARS, CLASSIFY_TITLE_TRUNCATE_LEN,
3 CLASSIFY_TRUNCATE_LEN, HOOK_LOG_DESC_MAX_LEN,
4};
5
6use super::tool_names;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum ToolCategory {
11 File,
13 Search,
15 Execute,
17 Network,
19 Plan,
21 Agent,
23 Teammate,
25 Compact,
27 SendMessage,
29 IgnoreMessage,
31 WorkDone,
33 Other,
35}
36
37impl ToolCategory {
38 pub fn from_name(name: &str) -> Self {
40 match name {
41 tool_names::READ | tool_names::WRITE | tool_names::EDIT | tool_names::GLOB => {
42 Self::File
43 }
44 tool_names::GREP => Self::Search,
45 tool_names::SHELL | tool_names::TASK | tool_names::TASK_OUTPUT => Self::Execute,
46 tool_names::WEB_FETCH | tool_names::WEB_SEARCH | tool_names::BROWSER => Self::Network,
47 tool_names::ENTER_PLAN_MODE | tool_names::EXIT_PLAN_MODE => Self::Plan,
48 tool_names::AGENT => Self::Agent,
49 tool_names::TEAMMATE => Self::Teammate,
50 tool_names::COMPACT => Self::Compact,
51 tool_names::SEND_MESSAGE => Self::SendMessage,
52 tool_names::IGNORE_MESSAGE => Self::IgnoreMessage,
53 tool_names::WORK_DONE => Self::WorkDone,
54 _ => Self::Other,
55 }
56 }
57
58 pub fn icon(&self) -> &'static str {
60 match self {
61 Self::File => "📄",
62 Self::Search => "🔍",
63 Self::Execute => "⚡",
64 Self::Network => "🌐",
65 Self::Plan => "📋",
66 Self::Agent => "🤖",
67 Self::Teammate => "👥",
68 Self::Compact => "📦",
69 Self::SendMessage => "✉️",
70 Self::IgnoreMessage => "💤",
71 Self::WorkDone => "🚩",
72 Self::Other => "🔧",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum ToolStatus {
80 Success,
82 Failed,
84}
85
86impl ToolStatus {
87 pub fn icon(&self) -> &'static str {
89 match self {
90 Self::Success => "✓",
91 Self::Failed => "✗",
92 }
93 }
94}
95
96pub fn format_json_value(value: &serde_json::Value) -> String {
98 match value {
99 serde_json::Value::String(s) => {
100 let char_count = s.chars().count();
102 if char_count > CLASSIFY_TRUNCATE_LEN {
103 let truncated: String = s.chars().take(CLASSIFY_TRUNCATE_LEN - 3).collect();
104 format!("\"{}...\"", truncated)
105 } else {
106 format!("\"{}\"", s)
107 }
108 }
109 serde_json::Value::Number(n) => n.to_string(),
110 serde_json::Value::Bool(b) => b.to_string(),
111 serde_json::Value::Null => "null".to_string(),
112 serde_json::Value::Array(arr) => {
113 if arr.is_empty() {
114 "[]".to_string()
115 } else {
116 format!("[{} items]", arr.len())
117 }
118 }
119 serde_json::Value::Object(obj) => {
120 if obj.is_empty() {
121 "{}".to_string()
122 } else {
123 let keys: Vec<&str> = obj.keys().take(3).map(|s| s.as_str()).collect();
124 format!("{{{}}}", keys.join(", "))
125 }
126 }
127 }
128}
129
130pub fn get_result_summary_for_tool(
132 content: &str,
133 is_error: bool,
134 tool_name: &str,
135 tool_args: Option<&str>,
136) -> String {
137 if is_error {
138 return "失败".to_string();
139 }
140
141 if content.is_empty() {
142 return "无输出".to_string();
143 }
144
145 match tool_name {
147 tool_names::READ => get_read_summary(content, tool_args),
148 tool_names::WRITE => get_write_summary(content, tool_args),
149 tool_names::EDIT => get_edit_summary(content, tool_args),
150 tool_names::SHELL => get_bash_summary(content, tool_args),
151 tool_names::GLOB => get_glob_summary(content, tool_args),
152 tool_names::GREP => get_grep_summary(content, tool_args),
153 tool_names::WEB_FETCH => get_web_fetch_summary(content, tool_args),
154 tool_names::WEB_SEARCH => get_web_search_summary(content, tool_args),
155 tool_names::BROWSER => get_browser_summary(content, tool_args),
156 tool_names::ASK => "用户已回答".to_string(),
157 tool_names::TASK_OUTPUT => get_task_output_result_summary(content, tool_args),
158 tool_names::TODO_WRITE => get_todo_write_summary(content, tool_args),
159 tool_names::TODO_READ => get_todo_read_summary(content),
160 tool_names::TASK => get_task_summary(content, tool_args),
161 tool_names::AGENT => get_agent_summary(content, tool_args),
162 tool_names::TEAMMATE => get_teammate_summary(content, tool_args),
163 tool_names::COMPACT => get_compact_summary(content),
164 tool_names::REGISTER_HOOK => "钩子已注册".to_string(),
165 tool_names::LOAD_SKILL => get_load_skill_result_summary(tool_args),
166 tool_names::SEND_MESSAGE => "消息已发送".to_string(),
167 tool_names::WORK_DONE => get_work_done_result_summary(tool_args),
168 tool_names::ENTER_PLAN_MODE | tool_names::EXIT_PLAN_MODE => {
169 get_plan_result_summary(tool_name)
170 }
171 tool_names::ENTER_WORKTREE | tool_names::EXIT_WORKTREE => {
172 get_worktree_result_summary(tool_name)
173 }
174 tool_names::LOAD_TOOL => get_load_tool_result_summary(tool_args),
175 tool_names::SESSION => "会话操作完成".to_string(),
176 tool_names::IGNORE_MESSAGE => "消息已忽略".to_string(),
177 #[cfg(target_os = "macos")]
178 tool_names::COMPUTER_USE => get_computer_use_result_summary(tool_args),
179 _ => get_generic_summary(content),
180 }
181}
182
183fn get_read_summary(content: &str, tool_args: Option<&str>) -> String {
185 let lines = content.lines().count();
186 let file_path = tool_args
187 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
188 .and_then(|v| {
189 v.get("file_path")
190 .and_then(|p| p.as_str().map(|s| s.to_string()))
191 });
192
193 if let Some(path) = file_path {
194 let short = short_path(&path, 40);
196 format!("{} ({} 行)", short, lines)
197 } else {
198 format!("{} 行", lines)
199 }
200}
201
202fn get_bash_summary(content: &str, tool_args: Option<&str>) -> String {
204 let command = tool_args
205 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
206 .and_then(|v| {
207 v.get("command")
208 .and_then(|c| c.as_str().map(|s| s.to_string()))
209 });
210
211 let lines = content.lines().count();
212 let line_info = if lines > 1 {
213 format!(" ({} 行输出)", lines)
214 } else {
215 String::new()
216 };
217
218 if let Some(cmd) = command {
219 let first_line = cmd.lines().next().unwrap_or(&cmd);
221 let short_cmd: String = first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
222 let suffix = if first_line.chars().count() > CLASSIFY_TRUNCATE_LEN {
223 "…"
224 } else {
225 ""
226 };
227 format!("{}{}{}", short_cmd, suffix, line_info)
228 } else {
229 format!("完成{}", line_info)
230 }
231}
232
233fn get_todo_write_summary(_content: &str, tool_args: Option<&str>) -> String {
235 tool_args
236 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
237 .map(|v| {
238 let is_merge = v.get("merge").and_then(|m| m.as_bool()).unwrap_or(false);
239 let count = v
240 .get("todos")
241 .and_then(|t| t.as_array())
242 .map(|a| a.len())
243 .unwrap_or(0);
244 if is_merge {
245 format!("更新 {} 项待办", count)
246 } else {
247 format!("写入 {} 项待办", count)
248 }
249 })
250 .unwrap_or_else(|| "写入待办".to_string())
251}
252
253fn get_todo_read_summary(content: &str) -> String {
255 if let Ok(items) = serde_json::from_str::<Vec<serde_json::Value>>(content) {
256 format!("读取 {} 项待办", items.len())
257 } else {
258 get_generic_summary(content)
259 }
260}
261
262fn get_task_summary(content: &str, tool_args: Option<&str>) -> String {
264 let parsed = tool_args.and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok());
265
266 if let Some(ref v) = parsed {
267 let action = v.get("action").and_then(|a| a.as_str()).unwrap_or("");
268 match action {
269 "create" => {
270 let title = v
271 .get("title")
272 .and_then(|t| t.as_str())
273 .unwrap_or("untitled");
274 let short: String = title.chars().take(CLASSIFY_TITLE_TRUNCATE_LEN).collect();
275 format!("create: \"{}\"", short)
276 }
277 "list" => {
278 let count = content.lines().filter(|l| l.contains("\"id\"")).count();
280 if count > 0 {
281 format!("list: {} 项任务", count)
282 } else {
283 "list".to_string()
284 }
285 }
286 "get" => {
287 let task_id = v
288 .get("taskId")
289 .and_then(|t| t.as_u64())
290 .map(|id| format!("#{}", id))
291 .unwrap_or_default();
292 format!("get {}", task_id)
293 }
294 "update" => {
295 let task_id = v
296 .get("taskId")
297 .and_then(|t| t.as_u64())
298 .map(|id| format!("#{}", id))
299 .unwrap_or_default();
300 let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
301 if !status.is_empty() {
302 format!("update {} -> {}", task_id, status)
303 } else {
304 format!("update {}", task_id)
305 }
306 }
307 _ => get_generic_summary(content),
308 }
309 } else {
310 get_generic_summary(content)
311 }
312}
313
314fn get_agent_summary(content: &str, tool_args: Option<&str>) -> String {
317 let lines = content.lines().count();
318 let desc = tool_args
319 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
320 .and_then(|v| {
321 v.get("description")
322 .and_then(|d| d.as_str().map(|s| s.to_string()))
323 });
324
325 let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
327
328 if let Some(d) = desc {
329 let max_d: String = d.chars().take(20).collect();
330 if first_line.is_empty() {
331 max_d
332 } else {
333 let max_f: String = first_line.chars().take(40).collect();
334 format!("{}: {}", max_d, max_f)
335 }
336 } else if first_line.is_empty() {
337 format!("{} 行", lines)
338 } else {
339 let max_f: String = first_line.chars().take(50).collect();
340 max_f
341 }
342}
343
344fn get_teammate_summary(content: &str, tool_args: Option<&str>) -> String {
346 let name = tool_args
347 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
348 .and_then(|v| {
349 v.get("name")
350 .and_then(|n| n.as_str().map(|s| s.to_string()))
351 });
352
353 let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
354
355 if let Some(n) = name {
356 if first_line.is_empty() {
357 n
358 } else {
359 let max_f: String = first_line.chars().take(40).collect();
360 format!("{}: {}", n, max_f)
361 }
362 } else if first_line.is_empty() {
363 "完成".to_string()
364 } else {
365 let max_f: String = first_line.chars().take(50).collect();
366 max_f
367 }
368}
369
370fn get_compact_summary(content: &str) -> String {
372 content
375 .lines()
376 .next()
377 .map(|l| {
378 let chars: String = l.chars().take(HOOK_LOG_DESC_MAX_LEN).collect();
379 chars
380 })
381 .unwrap_or_else(|| "压缩完成".to_string())
382}
383
384fn get_generic_summary(content: &str) -> String {
385 let lines = content.lines().count();
386 let chars = content.chars().count();
387
388 if lines > 1 {
389 if chars > CLASSIFY_SIZE_THRESHOLD_BYTES {
390 format!("{} 行, {:.1}KB", lines, chars as f64 / 1024.0)
391 } else {
392 format!("{} 行, {} 字符", lines, chars)
393 }
394 } else if chars > CLASSIFY_SIZE_THRESHOLD_CHARS {
395 format!("{:.1}KB", chars as f64 / 1024.0)
396 } else {
397 format!("{} 字符", chars)
398 }
399}
400
401fn short_path(path: &str, max_len: usize) -> String {
403 if path.chars().count() <= max_len {
404 return path.to_string();
405 }
406 let parts: Vec<&str> = path.split('/').collect();
408 if parts.len() <= 2 {
409 let truncated: String = path.chars().take(max_len.saturating_sub(1)).collect();
410 return format!("{}…", truncated);
411 }
412 let mut result = String::new();
414 for i in (0..parts.len()).rev() {
415 let candidate = parts[i..].join("/");
416 if candidate.chars().count() + 2 > max_len {
417 break;
418 }
419 result = candidate;
420 }
421 if result.is_empty() {
422 result = parts.last().unwrap_or(&"").to_string();
423 }
424 format!("…/{}", result)
425}
426
427fn get_write_summary(content: &str, tool_args: Option<&str>) -> String {
429 let file_path = tool_args
430 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
431 .and_then(|v| {
432 v.get("path")
433 .and_then(|p| p.as_str().map(|s| s.to_string()))
434 });
435
436 if let Some(path) = file_path {
437 let short = short_path(&path, 40);
438 let first_line = content.lines().next().unwrap_or("");
440 if first_line.is_empty() {
441 format!("写入 {}", short)
442 } else {
443 let truncated: String = first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
444 format!("写入 {}: {}", short, truncated)
445 }
446 } else {
447 get_generic_summary(content)
448 }
449}
450
451fn get_edit_summary(content: &str, tool_args: Option<&str>) -> String {
453 let file_path = tool_args
454 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
455 .and_then(|v| {
456 v.get("path")
457 .and_then(|p| p.as_str().map(|s| s.to_string()))
458 });
459
460 if let Some(path) = file_path {
461 let short = short_path(&path, 40);
462 let first_line = content.lines().next().unwrap_or("");
463 if first_line.is_empty() {
464 format!("编辑 {}", short)
465 } else {
466 let truncated: String = first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
467 format!("编辑 {}: {}", short, truncated)
468 }
469 } else {
470 get_generic_summary(content)
471 }
472}
473
474fn get_glob_summary(content: &str, tool_args: Option<&str>) -> String {
476 let pattern = tool_args
477 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
478 .and_then(|v| {
479 v.get("pattern")
480 .and_then(|p| p.as_str().map(|s| s.to_string()))
481 });
482
483 let match_count = content.lines().filter(|l| !l.trim().is_empty()).count();
485
486 if let Some(pat) = pattern {
487 let short: String = pat.chars().take(30).collect();
488 let suffix = if pat.chars().count() > 30 { "…" } else { "" };
489 format!("{}{} → {} 个匹配", short, suffix, match_count)
490 } else {
491 format!("{} 个匹配", match_count)
492 }
493}
494
495fn get_grep_summary(content: &str, tool_args: Option<&str>) -> String {
497 let pattern = tool_args
498 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
499 .and_then(|v| {
500 v.get("pattern")
501 .and_then(|p| p.as_str().map(|s| s.to_string()))
502 });
503
504 let lines = content.lines().count();
505
506 if let Some(pat) = pattern {
507 let short: String = pat.chars().take(30).collect();
508 let suffix = if pat.chars().count() > 30 { "…" } else { "" };
509 if lines > 1 {
510 format!("{}{} → {} 行匹配", short, suffix, lines)
511 } else {
512 format!("{}{}", short, suffix)
513 }
514 } else {
515 get_generic_summary(content)
516 }
517}
518
519fn get_web_fetch_summary(content: &str, tool_args: Option<&str>) -> String {
521 let url = tool_args
522 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
523 .and_then(|v| v.get("url").and_then(|u| u.as_str().map(|s| s.to_string())));
524
525 let lines = content.lines().count();
526
527 if let Some(u) = url {
528 let short: String = u.chars().take(50).collect();
529 let suffix = if u.chars().count() > 50 { "…" } else { "" };
530 if lines > 1 {
531 format!("{}{} ({} 行)", short, suffix, lines)
532 } else {
533 format!("{}{}", short, suffix)
534 }
535 } else {
536 get_generic_summary(content)
537 }
538}
539
540fn get_web_search_summary(content: &str, tool_args: Option<&str>) -> String {
542 let query = tool_args
543 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
544 .and_then(|v| {
545 v.get("query")
546 .and_then(|q| q.as_str().map(|s| s.to_string()))
547 });
548
549 let result_count = content.lines().filter(|l| l.contains("http")).count();
551
552 if let Some(q) = query {
553 let short: String = q.chars().take(30).collect();
554 let suffix = if q.chars().count() > 30 { "…" } else { "" };
555 if result_count > 0 {
556 format!("{}{} → {} 条结果", short, suffix, result_count)
557 } else {
558 format!("{}{}", short, suffix)
559 }
560 } else {
561 get_generic_summary(content)
562 }
563}
564
565fn get_browser_summary(content: &str, tool_args: Option<&str>) -> String {
567 let parsed = tool_args.and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok());
568
569 if let Some(ref v) = parsed {
570 let action = v.get("action").and_then(|a| a.as_str()).unwrap_or("");
571 let url = v.get("url").and_then(|u| u.as_str());
572
573 match action {
574 "open" | "navigate" => {
575 if let Some(u) = url {
576 let short: String = u.chars().take(40).collect();
577 let suffix = if u.chars().count() > 40 { "…" } else { "" };
578 format!("{}: {}{}", action, short, suffix)
579 } else {
580 format!("{}: {}", action, get_generic_summary(content))
581 }
582 }
583 "screenshot" | "snapshot" | "content" => {
584 let first_line = content.lines().next().unwrap_or("");
585 if first_line.is_empty() {
586 action.to_string()
587 } else {
588 let truncated: String =
589 first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
590 format!("{}: {}", action, truncated)
591 }
592 }
593 _ => {
594 let first_line = content.lines().next().unwrap_or("");
595 if first_line.is_empty() {
596 action.to_string()
597 } else {
598 let truncated: String =
599 first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
600 truncated
601 }
602 }
603 }
604 } else {
605 get_generic_summary(content)
606 }
607}
608
609fn get_task_output_result_summary(content: &str, tool_args: Option<&str>) -> String {
611 let task_id = tool_args
612 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
613 .and_then(|v| {
614 v.get("task_id")
615 .and_then(|t| t.as_str().map(|s| s.to_string()))
616 });
617
618 let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
619
620 if let Some(id) = task_id {
621 if first_line.is_empty() {
622 format!("获取任务 {} 输出", id)
623 } else {
624 let truncated: String = first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
625 format!("任务 {}: {}", id, truncated)
626 }
627 } else {
628 get_generic_summary(content)
629 }
630}
631
632fn get_load_skill_result_summary(tool_args: Option<&str>) -> String {
634 let name = tool_args
635 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
636 .and_then(|v| {
637 v.get("name")
638 .and_then(|n| n.as_str().map(|s| s.to_string()))
639 });
640
641 if let Some(n) = name {
642 format!("技能已加载: {}", n)
643 } else {
644 "技能已加载".to_string()
645 }
646}
647
648fn get_work_done_result_summary(tool_args: Option<&str>) -> String {
650 tool_args
651 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
652 .and_then(|v| {
653 v.get("summary")
654 .and_then(|s| s.as_str().map(|s| s.to_string()))
655 })
656 .map(|s| {
657 let truncated: String = s.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
658 format!("完成: {}", truncated)
659 })
660 .unwrap_or_else(|| "工作完成".to_string())
661}
662
663fn get_plan_result_summary(tool_name: &str) -> String {
665 if tool_name == tool_names::ENTER_PLAN_MODE {
666 "进入计划模式".to_string()
667 } else {
668 "退出计划模式".to_string()
669 }
670}
671
672fn get_worktree_result_summary(tool_name: &str) -> String {
674 if tool_name == tool_names::ENTER_WORKTREE {
675 "进入工作树".to_string()
676 } else {
677 "退出工作树".to_string()
678 }
679}
680
681fn get_load_tool_result_summary(tool_args: Option<&str>) -> String {
683 let name = tool_args
684 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
685 .and_then(|v| {
686 v.get("name")
687 .and_then(|n| n.as_str().map(|s| s.to_string()))
688 });
689
690 if let Some(n) = name {
691 format!("工具已加载: {}", n)
692 } else {
693 "工具已加载".to_string()
694 }
695}
696
697#[cfg(target_os = "macos")]
699fn get_computer_use_result_summary(tool_args: Option<&str>) -> String {
700 let action = tool_args
701 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
702 .and_then(|v| {
703 v.get("action")
704 .and_then(|a| a.as_str().map(|s| s.to_string()))
705 });
706
707 if let Some(a) = action {
708 format!("计算机操作: {}", a)
709 } else {
710 "计算机操作完成".to_string()
711 }
712}