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::SHELL => get_bash_summary(content, tool_args),
149 tool_names::TODO_WRITE => get_todo_write_summary(content, tool_args),
150 tool_names::TODO_READ => get_todo_read_summary(content),
151 tool_names::TASK => get_task_summary(content, tool_args),
152 tool_names::AGENT => get_agent_summary(content, tool_args),
153 tool_names::TEAMMATE => get_teammate_summary(content, tool_args),
154 tool_names::COMPACT => get_compact_summary(content),
155 _ => get_generic_summary(content),
156 }
157}
158
159fn get_read_summary(content: &str, tool_args: Option<&str>) -> String {
161 let lines = content.lines().count();
162 let file_path = tool_args
163 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
164 .and_then(|v| {
165 v.get("file_path")
166 .and_then(|p| p.as_str().map(|s| s.to_string()))
167 });
168
169 if let Some(path) = file_path {
170 let short = short_path(&path, 40);
172 format!("{} ({} 行)", short, lines)
173 } else {
174 format!("{} 行", lines)
175 }
176}
177
178fn get_bash_summary(content: &str, tool_args: Option<&str>) -> String {
180 let command = tool_args
181 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
182 .and_then(|v| {
183 v.get("command")
184 .and_then(|c| c.as_str().map(|s| s.to_string()))
185 });
186
187 let lines = content.lines().count();
188 let line_info = if lines > 1 {
189 format!(" ({} 行输出)", lines)
190 } else {
191 String::new()
192 };
193
194 if let Some(cmd) = command {
195 let first_line = cmd.lines().next().unwrap_or(&cmd);
197 let short_cmd: String = first_line.chars().take(CLASSIFY_TRUNCATE_LEN).collect();
198 let suffix = if first_line.chars().count() > CLASSIFY_TRUNCATE_LEN {
199 "…"
200 } else {
201 ""
202 };
203 format!("{}{}{}", short_cmd, suffix, line_info)
204 } else {
205 format!("完成{}", line_info)
206 }
207}
208
209fn get_todo_write_summary(_content: &str, tool_args: Option<&str>) -> String {
211 tool_args
212 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
213 .map(|v| {
214 let is_merge = v.get("merge").and_then(|m| m.as_bool()).unwrap_or(false);
215 let count = v
216 .get("todos")
217 .and_then(|t| t.as_array())
218 .map(|a| a.len())
219 .unwrap_or(0);
220 if is_merge {
221 format!("更新 {} 项待办", count)
222 } else {
223 format!("写入 {} 项待办", count)
224 }
225 })
226 .unwrap_or_else(|| "写入待办".to_string())
227}
228
229fn get_todo_read_summary(content: &str) -> String {
231 if let Ok(items) = serde_json::from_str::<Vec<serde_json::Value>>(content) {
232 format!("读取 {} 项待办", items.len())
233 } else {
234 get_generic_summary(content)
235 }
236}
237
238fn get_task_summary(content: &str, tool_args: Option<&str>) -> String {
240 let parsed = tool_args.and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok());
241
242 if let Some(ref v) = parsed {
243 let action = v.get("action").and_then(|a| a.as_str()).unwrap_or("");
244 match action {
245 "create" => {
246 let title = v
247 .get("title")
248 .and_then(|t| t.as_str())
249 .unwrap_or("untitled");
250 let short: String = title.chars().take(CLASSIFY_TITLE_TRUNCATE_LEN).collect();
251 format!("create: \"{}\"", short)
252 }
253 "list" => {
254 let count = content.lines().filter(|l| l.contains("\"id\"")).count();
256 if count > 0 {
257 format!("list: {} 项任务", count)
258 } else {
259 "list".to_string()
260 }
261 }
262 "get" => {
263 let task_id = v
264 .get("taskId")
265 .and_then(|t| t.as_u64())
266 .map(|id| format!("#{}", id))
267 .unwrap_or_default();
268 format!("get {}", task_id)
269 }
270 "update" => {
271 let task_id = v
272 .get("taskId")
273 .and_then(|t| t.as_u64())
274 .map(|id| format!("#{}", id))
275 .unwrap_or_default();
276 let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
277 if !status.is_empty() {
278 format!("update {} -> {}", task_id, status)
279 } else {
280 format!("update {}", task_id)
281 }
282 }
283 _ => get_generic_summary(content),
284 }
285 } else {
286 get_generic_summary(content)
287 }
288}
289
290fn get_agent_summary(content: &str, tool_args: Option<&str>) -> String {
293 let lines = content.lines().count();
294 let desc = tool_args
295 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
296 .and_then(|v| {
297 v.get("description")
298 .and_then(|d| d.as_str().map(|s| s.to_string()))
299 });
300
301 let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
303
304 if let Some(d) = desc {
305 let max_d: String = d.chars().take(20).collect();
306 if first_line.is_empty() {
307 max_d
308 } else {
309 let max_f: String = first_line.chars().take(40).collect();
310 format!("{}: {}", max_d, max_f)
311 }
312 } else if first_line.is_empty() {
313 format!("{} 行", lines)
314 } else {
315 let max_f: String = first_line.chars().take(50).collect();
316 max_f
317 }
318}
319
320fn get_teammate_summary(content: &str, tool_args: Option<&str>) -> String {
322 let name = tool_args
323 .and_then(|args| serde_json::from_str::<serde_json::Value>(args).ok())
324 .and_then(|v| {
325 v.get("name")
326 .and_then(|n| n.as_str().map(|s| s.to_string()))
327 });
328
329 let first_line = content.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
330
331 if let Some(n) = name {
332 if first_line.is_empty() {
333 n
334 } else {
335 let max_f: String = first_line.chars().take(40).collect();
336 format!("{}: {}", n, max_f)
337 }
338 } else if first_line.is_empty() {
339 "完成".to_string()
340 } else {
341 let max_f: String = first_line.chars().take(50).collect();
342 max_f
343 }
344}
345
346fn get_compact_summary(content: &str) -> String {
348 content
351 .lines()
352 .next()
353 .map(|l| {
354 let chars: String = l.chars().take(HOOK_LOG_DESC_MAX_LEN).collect();
355 chars
356 })
357 .unwrap_or_else(|| "压缩完成".to_string())
358}
359
360fn get_generic_summary(content: &str) -> String {
361 let lines = content.lines().count();
362 let chars = content.chars().count();
363
364 if lines > 1 {
365 if chars > CLASSIFY_SIZE_THRESHOLD_BYTES {
366 format!("{} 行, {:.1}KB", lines, chars as f64 / 1024.0)
367 } else {
368 format!("{} 行, {} 字符", lines, chars)
369 }
370 } else if chars > CLASSIFY_SIZE_THRESHOLD_CHARS {
371 format!("{:.1}KB", chars as f64 / 1024.0)
372 } else {
373 format!("{} 字符", chars)
374 }
375}
376
377fn short_path(path: &str, max_len: usize) -> String {
379 if path.chars().count() <= max_len {
380 return path.to_string();
381 }
382 let parts: Vec<&str> = path.split('/').collect();
384 if parts.len() <= 2 {
385 let truncated: String = path.chars().take(max_len.saturating_sub(1)).collect();
386 return format!("{}…", truncated);
387 }
388 let mut result = String::new();
390 for i in (0..parts.len()).rev() {
391 let candidate = parts[i..].join("/");
392 if candidate.chars().count() + 2 > max_len {
393 break;
394 }
395 result = candidate;
396 }
397 if result.is_empty() {
398 result = parts.last().unwrap_or(&"").to_string();
399 }
400 format!("…/{}", result)
401}