Skip to main content

j_agent/tools/
classification.rs

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/// 工具类型分类
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum ToolCategory {
11    /// 文件操作类 (Read, Write, Edit, Glob)
12    File,
13    /// 搜索类 (Grep)
14    Search,
15    /// 执行类 (Bash, Task, TaskOutput)
16    Execute,
17    /// 网络类 (WebFetch, WebSearch)
18    Network,
19    /// 计划类 (EnterPlanMode, ExitPlanMode)
20    Plan,
21    /// 代理类 (Agent)
22    Agent,
23    /// 协作者类 (Teammate)
24    Teammate,
25    /// 压缩类 (Compact)
26    Compact,
27    /// 发送消息 (SendMessage)
28    SendMessage,
29    /// 忽略消息 (IgnoreMessage)
30    IgnoreMessage,
31    /// 工作完成 (WorkDone)
32    WorkDone,
33    /// 其他类
34    Other,
35}
36
37impl ToolCategory {
38    /// 根据工具名称判断分类
39    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    /// 获取工具图标
59    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/// 工具执行状态
78#[derive(Debug, Clone, Copy, PartialEq)]
79pub enum ToolStatus {
80    /// 成功完成
81    Success,
82    /// 失败
83    Failed,
84}
85
86impl ToolStatus {
87    /// 状态图标
88    pub fn icon(&self) -> &'static str {
89        match self {
90            Self::Success => "✓",
91            Self::Failed => "✗",
92        }
93    }
94}
95
96/// 格式化 JSON 值为简短显示
97pub fn format_json_value(value: &serde_json::Value) -> String {
98    match value {
99        serde_json::Value::String(s) => {
100            // 使用字符数而不是字节数来截断,避免 UTF-8 边界问题
101            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
130/// 获取工具特性化结果摘要
131pub 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    // 工具特性化摘要
146    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
159/// Read 工具摘要:显示文件路径和行数
160fn 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        // 只取文件名部分,避免过长
171        let short = short_path(&path, 40);
172        format!("{} ({} 行)", short, lines)
173    } else {
174        format!("{} 行", lines)
175    }
176}
177
178/// Bash 工具摘要:显示命令预览
179fn 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        // 截取命令的第一行前 50 字符
196        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
209/// TodoWrite 工具摘要:显示操作描述
210fn 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
229/// TodoRead 工具摘要:显示读取数量
230fn 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
238/// Task 工具摘要
239fn 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                // 从 content 中尝试统计任务数
255                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
290/// 通用摘要(原有逻辑)
291/// Agent 工具摘要:提取 description + 首行输出
292fn 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    // 首行非空内容作为摘要
302    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
320/// Teammate 工具摘要:提取 name + 首行输出
321fn 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
346/// Compact 工具摘要:提取压缩信息
347fn get_compact_summary(content: &str) -> String {
348    // 内容格式: "📦 上下文已压缩 (N 条消息 → 摘要, transcript: path)"
349    // 直接取第一行作为摘要
350    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
377/// 截断路径,保留文件名和部分目录
378fn short_path(path: &str, max_len: usize) -> String {
379    if path.chars().count() <= max_len {
380        return path.to_string();
381    }
382    // 取最后几个路径段
383    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    // 保留最后 2-3 段
389    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}