kovi_plugin_oai/
lib.rs

1//! kovi-plugin-oai
2//!
3//! 符号指令系统 AI 聊天插件
4//!
5//! 指令格式: [&]["]智能体名[操作符][参数]
6//!
7//! 模式前缀: & 私有 | " 文本
8//! 操作符: # 创建 | ~ 复制/重新 | / 查看 | - 删除 | _ 导出 | ' 编辑 | ! 停止
9//! 对象符: @ 智能体 | $ 提示词 | % 模型 | : 描述
10//! 范围符: * 全部 | 数字索引
11
12// --- 类型定义 ---
13mod types {
14    use serde::{Deserialize, Serialize};
15    use std::collections::{HashMap, HashSet};
16
17    #[derive(Debug, Clone, Serialize, Deserialize)]
18    pub struct ChatMessage {
19        pub role: String,
20        pub content: String,
21        #[serde(default)]
22        pub images: Vec<String>,
23        #[serde(default)]
24        pub timestamp: i64,
25    }
26
27    impl ChatMessage {
28        pub fn new(role: &str, content: &str, images: Vec<String>) -> Self {
29            Self {
30                role: role.to_string(),
31                content: content.to_string(),
32                images,
33                timestamp: chrono::Local::now().timestamp(),
34            }
35        }
36    }
37
38    #[derive(Debug, Clone, Serialize, Deserialize)]
39    pub struct Agent {
40        pub name: String,
41        #[serde(default)]
42        pub description: String,
43        pub model: String,
44        pub system_prompt: String,
45        #[serde(default)]
46        pub public_history: Vec<ChatMessage>,
47        #[serde(default)]
48        pub private_histories: HashMap<String, Vec<ChatMessage>>,
49        #[serde(default)]
50        pub generation_id: u64,
51        #[serde(default)]
52        pub created_at: i64,
53    }
54
55    impl Agent {
56        pub fn new(name: &str, model: &str, prompt: &str, desc: &str) -> Self {
57            Self {
58                name: name.to_string(),
59                description: desc.to_string(),
60                model: model.to_string(),
61                system_prompt: prompt.to_string(),
62                public_history: Vec::new(),
63                private_histories: HashMap::new(),
64                generation_id: 0,
65                created_at: chrono::Local::now().timestamp(),
66            }
67        }
68
69        pub fn history_mut(&mut self, private: bool, uid: &str) -> &mut Vec<ChatMessage> {
70            if private {
71                self.private_histories.entry(uid.to_string()).or_default()
72            } else {
73                &mut self.public_history
74            }
75        }
76
77        pub fn history(&self, private: bool, uid: &str) -> &[ChatMessage] {
78            if private {
79                self.private_histories
80                    .get(uid)
81                    .map(|v| v.as_slice())
82                    .unwrap_or(&[])
83            } else {
84                &self.public_history
85            }
86        }
87
88        pub fn clear_history(&mut self, private: bool, uid: &str) {
89            if private {
90                if let Some(h) = self.private_histories.get_mut(uid) {
91                    h.clear();
92                }
93            } else {
94                self.public_history.clear();
95            }
96        }
97
98        pub fn delete_at(&mut self, private: bool, uid: &str, indices: &[usize]) -> Vec<usize> {
99            let h = self.history_mut(private, uid);
100            let mut deleted = Vec::new();
101            let mut sorted: Vec<usize> = indices.to_vec();
102            // 降序排序,从后往前删除
103            sorted.sort_by(|a, b| b.cmp(a));
104            sorted.dedup();
105            for i in sorted {
106                if i > 0 && i <= h.len() {
107                    h.remove(i - 1);
108                    deleted.push(i);
109                }
110            }
111            // 返回时恢复升序,便于显示
112            deleted.reverse();
113            deleted
114        }
115
116        pub fn edit_at(&mut self, private: bool, uid: &str, idx: usize, content: &str) -> bool {
117            let h = self.history_mut(private, uid);
118            if idx > 0 && idx <= h.len() {
119                h[idx - 1].content = content.to_string();
120                true
121            } else {
122                false
123            }
124        }
125    }
126
127    #[derive(Debug, Clone, Serialize, Deserialize, Default)]
128    pub struct Config {
129        pub api_base: String,
130        pub api_key: String,
131        #[serde(default)]
132        pub models: Vec<String>,
133        #[serde(default)]
134        pub agents: Vec<Agent>,
135        #[serde(default)]
136        pub default_model: String,
137        #[serde(default)]
138        pub default_prompt: String,
139    }
140
141    #[derive(Debug, Default)]
142    pub struct GeneratingState {
143        pub public: HashSet<String>,
144        pub private: HashMap<String, HashSet<String>>,
145    }
146
147    impl GeneratingState {
148        pub fn is_generating(&self, agent: &str, private: bool, uid: &str) -> bool {
149            if private {
150                self.private
151                    .get(agent)
152                    .map(|s| s.contains(uid))
153                    .unwrap_or(false)
154            } else {
155                self.public.contains(agent)
156            }
157        }
158
159        pub fn set_generating(&mut self, agent: &str, private: bool, uid: &str, generating: bool) {
160            if private {
161                let set = self.private.entry(agent.to_string()).or_default();
162                if generating {
163                    set.insert(uid.to_string());
164                } else {
165                    set.remove(uid);
166                }
167            } else if generating {
168                self.public.insert(agent.to_string());
169            } else {
170                self.public.remove(agent);
171            }
172        }
173    }
174}
175
176// --- 工具函数 ---
177mod utils {
178    use cdp_html_shot::{Browser, CaptureOptions, Viewport};
179    use kovi::bot::message::Message;
180    use kovi::tokio::time::{self, Duration};
181    use pulldown_cmark::{Options, Parser, html};
182    use regex::Regex;
183    use std::sync::OnceLock;
184
185    pub static RE_API: OnceLock<Regex> = OnceLock::new();
186    pub static RE_IDX: OnceLock<Regex> = OnceLock::new();
187
188    pub const MODEL_KEYWORDS: &[&str] = &[
189        "gpt-5", "claude", "gemini-3", "deepseek", "kimi", "grok-4", "banana", "sora-2",
190    ];
191
192    /// 全角转半角
193    pub fn normalize(s: &str) -> String {
194        s.chars()
195            .map(|c| match c {
196                '!' => '!',
197                '@' => '@',
198                '#' => '#',
199                '$' => '$',
200                '%' => '%',
201                '*' => '*',
202                '(' => '(',
203                ')' => ')',
204                '-' => '-',
205                '+' => '+',
206                ':' => ':',
207                ';' => ';',
208                '“' | '”' => '"',
209                '‘' | '’' => '\'',
210                ',' => ',',
211                '。' => '.',
212                '?' => '?',
213                '~' => '~',
214                '_' => '_',
215                '&' => '&',
216                '/' => '/',
217                '=' => '=',
218                _ => c,
219            })
220            .collect()
221    }
222
223    /// 解析 API 配置
224    pub fn parse_api(text: &str) -> Option<(String, String)> {
225        let re = RE_API.get_or_init(|| {
226            Regex::new(r"(?s)^(https?://\S+)\s+(sk-\S+)$|^(sk-\S+)\s+(https?://\S+)$").unwrap()
227        });
228        let t = text.trim();
229        re.captures(t).and_then(|c| {
230            c.get(1)
231                .zip(c.get(2))
232                .map(|(u, k)| (u.as_str().to_string(), k.as_str().to_string()))
233                .or_else(|| {
234                    c.get(3)
235                        .zip(c.get(4))
236                        .map(|(k, u)| (u.as_str().to_string(), k.as_str().to_string()))
237                })
238        })
239    }
240
241    /// 解析索引 (1, 1-5, 1,3,5)
242    pub fn parse_indices(s: &str) -> Vec<usize> {
243        let s = s.replace(',', ",");
244        let re = RE_IDX.get_or_init(|| Regex::new(r"(\d+)(?:-(\d+))?").unwrap());
245        let mut v = Vec::new();
246        for c in re.captures_iter(&s) {
247            if let Some(start) = c.get(1).and_then(|m| m.as_str().parse().ok()) {
248                if let Some(end) = c.get(2).and_then(|m| m.as_str().parse().ok()) {
249                    v.extend(start..=end);
250                } else {
251                    v.push(start);
252                }
253            }
254        }
255        v.sort();
256        v.dedup();
257        v
258    }
259
260    /// 过滤模型列表
261    pub fn filter_models(models: &[String]) -> Vec<String> {
262        models
263            .iter()
264            .filter(|m| {
265                let lower = m.to_lowercase();
266                MODEL_KEYWORDS.iter().any(|kw| lower.contains(kw))
267            })
268            .cloned()
269            .collect()
270    }
271
272    pub fn escape_markdown_special(s: &str) -> String {
273        // 使用 serde_json 转义特殊字符,然后去掉首尾引号
274        match kovi::serde_json::to_string(s) {
275            Ok(escaped) => {
276                let trimmed = escaped.trim_matches('"');
277                // 将 \n 还原为真实换行,保持可读性
278                trimmed.replace("\\n", "\n").replace("\\t", "\t")
279            }
280            Err(_) => s.to_string(),
281        }
282    }
283
284    pub async fn render_md(md: &str, title: &str) -> anyhow::Result<String> {
285        let mut opts = Options::empty();
286        opts.insert(Options::ENABLE_STRIKETHROUGH);
287        opts.insert(Options::ENABLE_TABLES);
288        let parser = Parser::new_ext(md, opts);
289        let mut html_body = String::new();
290        html::push_html(&mut html_body, parser);
291
292        let css = r#"
293 *{box-sizing:border-box}
294 body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6;background:#f5f5f5;color:#333;padding:0;margin:0}
295 .md{background:#fff;padding:16px 14px;margin:0;max-width:480px;width:90vw;word-wrap:break-word;overflow-wrap:break-word}
296 .title{font-size:13px;color:#888;border-bottom:1px solid #eee;padding-bottom:10px;margin-bottom:14px;font-weight:500}
297 h1,h2,h3{margin:16px 0 10px;font-weight:600;line-height:1.4}
298 h1{font-size:20px;border-bottom:2px solid #eee;padding-bottom:8px}
299 h2{font-size:18px;border-bottom:1px solid #eee;padding-bottom:6px}
300 h3{font-size:16px}
301 p{margin:10px 0}
302 table{border-collapse:collapse;margin:12px 0;width:100%;font-size:13px;display:block;overflow-x:auto}
303 td,th{padding:8px 10px;border:1px solid #ddd;text-align:left}
304 th{font-weight:600;background:#f8f9fa}
305 tr:nth-child(2n){background:#fafafa}
306 code{padding:2px 6px;background:#f0f0f0;border-radius:4px;font-family:"SF Mono",Consolas,"Liberation Mono",Menlo,monospace;font-size:13px;color:#d63384;white-space:pre-wrap;word-wrap:break-word;}
307 pre{background:#f6f8fa;border-radius:8px;padding:12px;overflow-x:auto;margin:12px 0;white-space:pre-wrap;word-wrap:break-word;overflow-wrap: break-word;}
308 pre code{background:none;padding:0;color:#333}
309 blockquote{margin:12px 0;padding:8px 12px;color:#666;border-left:3px solid #ddd;background:#fafafa;border-radius:0 4px 4px 0}
310 img{max-width:100%;height:auto;border-radius:6px;margin:8px 0}
311 ul,ol{padding-left:20px;margin:10px 0}
312 li{margin:4px 0}
313 hr{border:none;border-top:1px solid #eee;margin:16px 0}
314 a{color:#0066cc;text-decoration:none}
315 strong{font-weight:600}
316 .agent-card{background:#fafbfc;border:1px solid #e8e8e8;border-radius:8px;padding:12px;margin:10px 0}
317 .agent-name{font-size:16px;font-weight:600;color:#333;margin-bottom:8px}
318 .agent-info{font-size:13px;color:#666;line-height:1.8}
319 .agent-info code{font-size:12px}
320 .model-group{margin-bottom:16px;break-inside:avoid;}
321 .model-header{background:#f0f2f5;color:#444;padding:6px 10px;border-radius:6px;font-weight:600;font-size:13px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;border-left:3px solid #0066cc;}
322 .model-count{background:rgba(0,0,0,0.05);color:#666;font-size:11px;padding:1px 6px;border-radius:4px;}
323 .agent-grid{display:grid;/*手机端一行两列,充分利用宽度*/grid-template-columns:repeat(2,1fr);gap:8px;}
324 .agent-mini{background:#fff;border:1px solid #eee;border-radius:6px;padding:8px;display:flex;flex-direction:column;justify-content:center;transition:background 0.2s;}
325 .agent-mini-top{display:flex;align-items:center;margin-bottom:4px;}
326 .agent-idx{background:#e6f0ff;color:#0066cc;font-size:10px;font-weight:700;min-width:18px;height:18px;border-radius:4px;display:flex;align-items:center;justify-content:center;margin-right:6px;flex-shrink:0;}
327 .agent-mini-name{font-size:14px;font-weight:600;color:#333;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
328 .agent-mini-desc{font-size:11px;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}
329 .provider-section { margin-bottom: 20px; break-inside: avoid; }
330 .provider-title { font-size: 14px; font-weight: 700; color: #555; margin-bottom: 8px; padding-left: 4px; border-left: 3px solid #666; line-height: 1.2; }
331 .chip-container { display: flex; flex-wrap: wrap; gap: 8px; }
332 .chip { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 6px 10px; display: flex; align-items: center; font-size: 13px; color: #333; box-shadow: 0 1px 2px rgba(0,0,0,0.02); }
333 .chip-idx { background: #f0f0f0; color: #666; font-size: 11px; padding: 2px 5px; border-radius: 4px; margin-right: 6px; font-family: monospace; font-weight: 600; }
334 .chip-name { font-weight: 500; }
335 .chip-badge { margin-left: 6px; background: #e6f0ff; color: #0066cc; font-size: 10px; padding: 1px 5px; border-radius: 10px; font-weight: 600; }
336
337  .mod-group { margin-bottom: 16px; break-inside: avoid; }
338  .mod-title { font-size: 13px; font-weight: 700; color: #666; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; border-left: 3px solid #0066cc; padding-left: 6px; }
339  .chip-box { display: flex; flex-wrap: wrap; gap: 8px; }
340  .chip { background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; padding: 6px 10px; display: flex; align-items: center; font-size: 13px; color: #333; transition: all 0.2s; }
341  .chip-idx { background: #f5f5f5; color: #888; font-size: 11px; padding: 2px 6px; border-radius: 4px; margin-right: 8px; font-family: monospace; font-weight: 600; }
342  .chip-name { font-weight: 500; }
343  /* 正在使用的模型的徽标样式 */
344  .chip-bad { margin-left: 8px; background: #e6f7ff; color: #1890ff; font-size: 10px; padding: 2px 6px; border-radius: 10px; font-weight: 600; } "#;
345        let html = format!(
346            r#"<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>{css}</style></head><body><div class="md"><div class="title">{title}</div>{html_body}</div></body></html>"#
347        );
348
349        let browser = Browser::instance().await;
350        let tab = browser.new_tab().await?;
351
352        // 1. 设置初始视口
353        // 宽度 600 以适应 .md max-width: 480px 的卡片设计
354        // device_scale_factor: 2.0 提升截图清晰度
355        let width = 600;
356        tab.set_viewport(&Viewport::new(width, 100).with_device_scale_factor(2.0))
357            .await?;
358
359        tab.set_content(&html).await?;
360
361        time::sleep(Duration::from_millis(200)).await;
362
363        // 2. 获取实际内容高度并调整视口
364        // 修复长截图时底部出现大片空白的 Bug (Chromium Issue)
365        let height_js = "document.body.scrollHeight";
366        let body_height = tab.evaluate(height_js).await?.as_f64().unwrap_or(800.0) as u32;
367
368        // 设置新的视口高度以容纳所有内容
369        let viewport = Viewport::new(width, body_height + 100).with_device_scale_factor(2.0);
370        tab.set_viewport(&viewport).await?;
371
372        // 等待 Resize 生效
373        time::sleep(Duration::from_millis(100)).await;
374
375        // 3. 截图
376        // 显式传入 viewport 确保 screenshot 方法使用了正确的尺寸
377        let opts = CaptureOptions::new()
378            .with_viewport(viewport)
379            .with_quality(90);
380
381        let b64 = tab
382            .find_element(".md")
383            .await?
384            .screenshot_with_options(opts)
385            .await?;
386
387        let _ = tab.close().await;
388        Ok(b64)
389    }
390
391    /// 获取消息完整内容(含引用)
392    /// 参数 trigger_name: 触发对话的智能体名称,用于判断 at 是否在智能体名称之后
393    pub async fn get_full_content(
394        event: &std::sync::Arc<kovi::MsgEvent>,
395        bot: &std::sync::Arc<kovi::RuntimeBot>,
396        trigger_name: Option<&str>,
397    ) -> (String, Vec<String>) {
398        let mut quote_text = String::new();
399        let mut imgs = Vec::new();
400
401        // 1. 处理引用消息 (Reply)
402        if let Some(reply) = event.message.iter().find(|s| s.type_ == "reply")
403            && let Some(id) = reply.data.get("id").and_then(|v| v.as_str())
404            && let Ok(id) = id.parse::<i32>()
405            && let Ok(ret) = bot.get_msg(id).await
406            && let Some(msg_data) = ret.data.get("message")
407        {
408            let reply_msg = Message::from_value(msg_data.clone()).unwrap_or_default();
409            let mut temp_text = String::new();
410
411            for seg in reply_msg.iter() {
412                match seg.type_.as_str() {
413                    "text" => {
414                        if let Some(t) = seg.data.get("text").and_then(|v| v.as_str()) {
415                            temp_text.push_str(t);
416                        }
417                    }
418                    "image" => {
419                        if let Some(u) = seg.data.get("url").and_then(|v| v.as_str()) {
420                            imgs.push(u.to_string());
421                        }
422                    }
423                    "video" => {
424                        let url = seg
425                            .data
426                            .get("url")
427                            .or(seg.data.get("file"))
428                            .and_then(|v| v.as_str());
429                        if let Some(u) = url {
430                            imgs.push(u.to_string());
431                        }
432                    }
433                    _ => {}
434                }
435            }
436
437            let trimmed = temp_text.trim();
438            if !trimmed.is_empty() {
439                for line in trimmed.lines() {
440                    quote_text.push_str("> ");
441                    quote_text.push_str(line);
442                    quote_text.push('\n');
443                }
444                quote_text.push('\n');
445            }
446        }
447
448        // 2. 提取当前消息中的图片/视频/At头像
449        // 状态标记:是否已经找到了智能体名称
450        let mut found_trigger = false;
451
452        for seg in event.message.iter() {
453            if seg.type_ == "image"
454                && let Some(u) = seg.data.get("url").and_then(|v| v.as_str())
455            {
456                // 普通图片始终添加
457                imgs.push(u.to_string());
458            } else if seg.type_ == "video" {
459                // 视频始终添加
460                let url = seg
461                    .data
462                    .get("url")
463                    .or(seg.data.get("file"))
464                    .and_then(|v| v.as_str());
465                if let Some(u) = url {
466                    imgs.push(u.to_string());
467                }
468            } else if seg.type_ == "text" {
469                // 检查文本段中是否包含智能体名称
470                if let Some(name) = trigger_name {
471                    if !found_trigger {
472                        let text = seg.data.get("text").and_then(|v| v.as_str()).unwrap_or("");
473                        // 对比前先进行标准化和转小写,以匹配 parser 的逻辑
474                        let norm_text = normalize(text).to_lowercase();
475                        let norm_name = normalize(name).to_lowercase();
476
477                        if norm_text.contains(&norm_name) {
478                            found_trigger = true;
479                        }
480                    }
481                }
482            } else if seg.type_ == "at" {
483                // 只有在找到了智能体名称之后(found_trigger == true),才处理 at
484                if found_trigger {
485                    let qq = seg.data.get("qq").and_then(|v| {
486                        if let Some(s) = v.as_str() {
487                            Some(s.to_string())
488                        } else if v.is_number() {
489                            Some(v.to_string())
490                        } else {
491                            None
492                        }
493                    });
494
495                    if let Some(id) = qq {
496                        if id != "all" {
497                            imgs.push(format!("https://q.qlogo.cn/g?b=qq&nk={}&s=640", id));
498                        }
499                    }
500                }
501            }
502        }
503
504        (quote_text, imgs)
505    }
506    /// 格式化历史记录
507    pub fn format_history(
508        hist: &[super::types::ChatMessage],
509        offset: usize,
510        text_mode: bool,
511    ) -> String {
512        let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
513
514        hist.iter()
515            .enumerate()
516            .map(|(i, m)| {
517                let emoji = match m.role.as_str() {
518                    "user" => "👤",
519                    "assistant" => "🤖",
520                    "system" => "⚙️",
521                    _ => "❓",
522                };
523                let time = chrono::DateTime::from_timestamp(m.timestamp, 0)
524                    .map(|dt| {
525                        use chrono::TimeZone;
526                        chrono::Local
527                            .from_utc_datetime(&dt.naive_utc())
528                            .format("%m-%d %H:%M")
529                            .to_string()
530                    })
531                    .unwrap_or_default();
532
533                let mut body = m.content.clone();
534
535                if text_mode {
536                    body = re.replace_all(&body, "[图片]").to_string();
537                }
538
539                if !m.images.is_empty() {
540                    if !body.is_empty() {
541                        body.push_str("\n\n");
542                    }
543
544                    if text_mode {
545                        let links = m
546                            .images
547                            .iter()
548                            .map(|u| {
549                                if u.starts_with("data:") {
550                                    "- [Base64 Image]".to_string()
551                                } else {
552                                    format!("- [图片] {}", u)
553                                }
554                            })
555                            .collect::<Vec<_>>()
556                            .join("\n");
557                        body.push_str(&links);
558                    } else {
559                        let imgs = m
560                            .images
561                            .iter()
562                            .map(|u| format!("![image]({})", u))
563                            .collect::<Vec<_>>()
564                            .join("\n");
565                        body.push_str(&imgs);
566                    }
567                }
568
569                if body.trim().is_empty() {
570                    body = "(无内容)".to_string();
571                }
572
573                format!("**#{} {} {}**\n{}", offset + i + 1, emoji, time, body)
574            })
575            .collect::<Vec<_>>()
576            .join("\n\n---\n\n")
577    }
578
579    /// 截断字符串
580    pub fn truncate_str(s: &str, max_chars: usize) -> String {
581        let chars: Vec<char> = s.chars().collect();
582        if chars.len() <= max_chars {
583            s.to_string()
584        } else {
585            chars[..max_chars].iter().collect::<String>() + "..."
586        }
587    }
588
589    pub fn format_export_txt(
590        agent_name: &str,
591        model: &str,
592        scope: &str,
593        hist: &[super::types::ChatMessage],
594    ) -> String {
595        let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
596
597        let mut content = String::new();
598        let separator = "─".repeat(40);
599        let thin_sep = "┄".repeat(40);
600
601        // 头部信息
602        content.push_str(&format!("┏{}┓\n", "━".repeat(40)));
603        content.push_str(&format!("┃  智能体: {:<32}┃\n", agent_name));
604        content.push_str(&format!("┃  模  型: {:<32}┃\n", model));
605        content.push_str(&format!("┃  类  型: {:<32}┃\n", scope));
606        content.push_str(&format!(
607            "┃  导  出: {:<32}┃\n",
608            chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
609        ));
610        content.push_str(&format!("┃  记录数: {:<32}┃\n", hist.len()));
611        content.push_str(&format!("┗{}┛\n\n", "━".repeat(40)));
612
613        // 历史记录
614        for (i, m) in hist.iter().enumerate() {
615            let time = chrono::DateTime::from_timestamp(m.timestamp, 0)
616                .map(|t| {
617                    use chrono::TimeZone;
618                    chrono::Local
619                        .from_utc_datetime(&t.naive_utc())
620                        .format("%Y-%m-%d %H:%M:%S")
621                        .to_string()
622                })
623                .unwrap_or_else(|| "未知时间".to_string());
624
625            let role_name = match m.role.as_str() {
626                "user" => "👤 用户",
627                "assistant" => "🤖 助手",
628                "system" => "⚙️ 系统",
629                _ => &m.role,
630            };
631
632            content.push_str(&format!("【#{} {} | {}】\n", i + 1, role_name, time));
633            content.push_str(&format!("{}\n", thin_sep));
634
635            let clean_content = re.replace_all(&m.content, "[图片数据]");
636            content.push_str(&clean_content);
637            content.push('\n');
638
639            if !m.images.is_empty() {
640                content.push_str(&format!("\n📷 附图 ({} 张):\n", m.images.len()));
641                for (j, url) in m.images.iter().enumerate() {
642                    if url.starts_with("data:") {
643                        content.push_str(&format!("   {}. [Base64 Image Data]\n", j + 1));
644                    } else {
645                        content.push_str(&format!("   {}. {}\n", j + 1, url));
646                    }
647                }
648            }
649
650            content.push_str(&format!("\n{}\n\n", separator));
651        }
652
653        content
654    }
655}
656
657// --- 指令解析器 ---
658mod parser {
659    use super::utils::normalize;
660
661    #[derive(Debug, Clone, Copy, PartialEq)]
662    pub enum Scope {
663        Public,
664        Private,
665    }
666
667    #[derive(Debug, Clone, PartialEq, Default)]
668    pub enum Action {
669        Chat,
670        Regenerate,
671        Stop,
672        #[default]
673        Create,
674        Copy,
675        Rename,
676        SetDesc,
677        Delete,
678        List,
679        SetModel,
680        SetPrompt,
681        ViewPrompt,
682        ListModels,
683        ViewAll(Scope),
684        ViewAt(Scope),
685        Export(Scope),
686        EditAt(Scope),
687        DeleteAt(Scope),
688        ClearHistory(Scope),
689        ClearAllPublic,
690        ClearEverything,
691        Help,
692        AutoFillDescriptions(String),
693    }
694
695    #[derive(Debug, Clone)]
696    pub struct Command {
697        pub agent: String,
698        pub action: Action,
699        pub args: String,
700        pub indices: Vec<usize>,
701        pub private_reply: bool,
702        pub text_mode: bool,
703    }
704
705    impl Command {
706        pub fn new(agent: &str, action: Action) -> Self {
707            Self {
708                agent: agent.to_string(),
709                action,
710                args: String::new(),
711                indices: Vec::new(),
712                private_reply: false,
713                text_mode: false,
714            }
715        }
716    }
717
718    pub fn parse_global(raw: &str) -> Option<Command> {
719        let norm = normalize(raw.trim());
720
721        if norm == "oai" {
722            return Some(Command::new("", Action::Help));
723        }
724
725        if norm == "/#" {
726            return Some(Command::new("", Action::List));
727        }
728
729        if norm == "/%" {
730            return Some(Command::new("", Action::ListModels));
731        }
732
733        if norm == "-*" {
734            return Some(Command::new("", Action::ClearAllPublic));
735        }
736
737        if norm == "-*!" {
738            return Some(Command::new("", Action::ClearEverything));
739        }
740
741        if norm.starts_with("##:") {
742            let args = norm.get(3..).unwrap_or("").trim().to_string();
743            return Some(Command::new("", Action::AutoFillDescriptions(args)));
744        }
745
746        None
747    }
748
749    pub fn parse_create(raw: &str) -> Option<(String, String, String, String)> {
750        let norm = normalize(raw.trim());
751        if !norm.starts_with("##") {
752            return None;
753        }
754
755        let start_pos = norm.find("##").unwrap() + "##".len();
756        let after = &raw.trim()[start_pos..];
757
758        let name_end = after
759            .find(|c: char| c.is_whitespace() || c == '(' || c == '(')
760            .unwrap_or(after.len());
761        let name = after[..name_end].trim().to_string();
762
763        if name.is_empty()
764            || name.chars().count() > 7
765            || name.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
766        {
767            return None;
768        }
769
770        let rest = &after[name_end..];
771
772        let (desc, after_desc) = if rest.starts_with('(') || rest.starts_with('(') {
773            if let Some(pos) = rest.find(')').or_else(|| rest.find(')')) {
774                (rest[1..pos].to_string(), &rest[pos + 1..])
775            } else {
776                (String::new(), rest)
777            }
778        } else {
779            (String::new(), rest)
780        };
781
782        let parts: Vec<&str> = after_desc.split_whitespace().collect();
783        let model = parts.first().unwrap_or(&"").to_string();
784        if model.chars().count() > 50 {
785            return None;
786        }
787        let prompt = if parts.len() > 1 {
788            parts[1..].join(" ")
789        } else {
790            String::new()
791        };
792
793        Some((name, desc, model, prompt))
794    }
795
796    pub fn parse_delete_agent(raw: &str, agents: &[String]) -> Option<String> {
797        let norm = normalize(raw.trim());
798        if !norm.starts_with("-#") {
799            return None;
800        }
801        let name = norm[2..].trim();
802        if agents.iter().any(|a| a.eq_ignore_ascii_case(name)) {
803            Some(name.to_string())
804        } else {
805            None
806        }
807    }
808
809    pub fn parse_agent_cmd(raw: &str, agents: &[String]) -> Option<Command> {
810        let raw = raw.trim();
811        if raw.is_empty() {
812            return None;
813        }
814
815        let norm = normalize(raw);
816        let chars: Vec<char> = norm.chars().collect();
817
818        let mut char_idx = 0;
819        let mut private_reply = false;
820        let mut text_mode = false;
821
822        while char_idx < chars.len() {
823            match chars[char_idx] {
824                '&' => {
825                    private_reply = true;
826                    char_idx += 1;
827                }
828                '"' => {
829                    text_mode = true;
830                    char_idx += 1;
831                }
832                _ => break,
833            }
834        }
835
836        let byte_idx: usize = chars.iter().take(char_idx).map(|c| c.len_utf8()).sum();
837        let content = &norm[byte_idx..];
838
839        let mut agent_name = String::new();
840        let mut match_char_len = 0;
841        let mut sorted = agents.to_vec();
842        sorted.sort_by_key(|b| std::cmp::Reverse(b.chars().count()));
843
844        for name in &sorted {
845            let name_lower = name.to_lowercase();
846            let content_lower = content.to_lowercase();
847            if content_lower.starts_with(&name_lower) {
848                agent_name = name.clone();
849                match_char_len = name.chars().count();
850                break;
851            }
852        }
853
854        if agent_name.is_empty() {
855            return None;
856        }
857
858        let match_byte_len: usize = content
859            .chars()
860            .take(match_char_len)
861            .map(|c| c.len_utf8())
862            .sum();
863        let suffix = content[match_byte_len..].trim();
864
865        let raw_suffix = {
866            let prefix_bytes: usize = raw.chars().take(char_idx).map(|c| c.len_utf8()).sum();
867            let agent_bytes: usize = raw[prefix_bytes..]
868                .chars()
869                .take(match_char_len)
870                .map(|c| c.len_utf8())
871                .sum();
872            raw[prefix_bytes + agent_bytes..].trim()
873        };
874
875        let (action, args, indices) = parse_suffix(suffix, raw_suffix, private_reply);
876
877        Some(Command {
878            agent: agent_name,
879            action,
880            args,
881            indices,
882            private_reply,
883            text_mode,
884        })
885    }
886
887    fn parse_suffix(norm: &str, raw: &str, has_priv_prefix: bool) -> (Action, String, Vec<usize>) {
888        let s = norm.trim();
889        let r = raw.trim();
890
891        if s.is_empty() {
892            return (Action::Chat, r.to_string(), vec![]);
893        }
894
895        if (s == "~" || s == "~")
896            || ((s.starts_with('~') || s.starts_with('~'))
897                && !s.starts_with("~#")
898                && !s.starts_with("~$")
899                && !s.starts_with("~#")
900                && !s.starts_with("~$"))
901        {
902            let skip_len = if s.starts_with('~') {
903                '~'.len_utf8()
904            } else {
905                '~'.len_utf8()
906            };
907            let arg = r.get(skip_len..).unwrap_or("").trim();
908            return (Action::Regenerate, arg.to_string(), vec![]);
909        }
910
911        if s == "!" {
912            return (Action::Stop, String::new(), vec![]);
913        }
914
915        if s.starts_with("~#") || s.starts_with("~#") {
916            let skip_len = if r.starts_with("~#") {
917                "~#".chars().map(|c| c.len_utf8()).sum()
918            } else {
919                "~#".chars().map(|c| c.len_utf8()).sum()
920            };
921            let arg = r.get(skip_len..).unwrap_or("").trim();
922            return (Action::Copy, arg.to_string(), vec![]);
923        }
924
925        if s.starts_with("~=") || s.starts_with("~=") {
926            let skip_len = if r.starts_with("~=") {
927                "~=".chars().map(|c| c.len_utf8()).sum()
928            } else {
929                "~=".chars().map(|c| c.len_utf8()).sum()
930            };
931            let arg = r.get(skip_len..).unwrap_or("").trim();
932            return (Action::Rename, arg.to_string(), vec![]);
933        }
934
935        if (s.starts_with(':') || s.starts_with(':'))
936            && !s.starts_with(":/")
937            && !s.starts_with(":/")
938        {
939            let skip_len = if r.starts_with(':') {
940                ':'.len_utf8()
941            } else {
942                ':'.len_utf8()
943            };
944            let arg = r.get(skip_len..).unwrap_or("").trim();
945            return (Action::SetDesc, arg.to_string(), vec![]);
946        }
947
948        if s.starts_with('%') {
949            let arg = r.get(1..).unwrap_or("").trim();
950            return (Action::SetModel, arg.to_string(), vec![]);
951        }
952
953        if s.starts_with('$') && s != "/$" {
954            let arg = r.get(1..).unwrap_or("").trim();
955            return (Action::SetPrompt, arg.to_string(), vec![]);
956        }
957
958        if s == "/$" {
959            return (Action::ViewPrompt, String::new(), vec![]);
960        }
961
962        let (has_local_priv, clean, clean_raw) = if let Some(stripped) = s.strip_prefix('&') {
963            (true, stripped, r.strip_prefix('&').unwrap_or("").trim())
964        } else {
965            (false, s, r)
966        };
967
968        let scope = if has_priv_prefix || has_local_priv {
969            Scope::Private
970        } else {
971            Scope::Public
972        };
973
974        if clean == "/*" {
975            return (Action::ViewAll(scope), String::new(), vec![]);
976        }
977
978        if clean.starts_with('/') && clean.len() > 1 {
979            let idx_part = &clean[1..];
980            let indices = super::utils::parse_indices(idx_part);
981            if !indices.is_empty() {
982                return (Action::ViewAt(scope), String::new(), indices);
983            }
984        }
985
986        if clean == "_*" {
987            return (Action::Export(scope), String::new(), vec![]);
988        }
989
990        if clean.starts_with('\'') {
991            let parts: Vec<&str> = clean_raw.get(1..).unwrap_or("").splitn(2, ' ').collect();
992            if !parts.is_empty() {
993                let indices = super::utils::parse_indices(parts[0]);
994                let content = parts.get(1).unwrap_or(&"").to_string();
995                return (Action::EditAt(scope), content, indices);
996            }
997        }
998
999        if clean == "-*" {
1000            return (Action::ClearHistory(scope), String::new(), vec![]);
1001        }
1002
1003        if clean.starts_with('-') && clean.len() > 1 {
1004            let idx_part = &clean[1..];
1005            let indices = super::utils::parse_indices(idx_part);
1006            if !indices.is_empty() {
1007                return (Action::DeleteAt(scope), String::new(), indices);
1008            }
1009        }
1010
1011        (Action::Chat, r.to_string(), vec![])
1012    }
1013}
1014
1015// --- 数据管理 ---
1016mod data {
1017    use super::types::{Config, GeneratingState};
1018    use async_openai::Client;
1019    use async_openai::config::OpenAIConfig;
1020    use kovi::tokio::sync::RwLock;
1021    use kovi::utils::{load_json_data, save_json_data};
1022    use std::path::PathBuf;
1023
1024    pub struct Manager {
1025        pub config: RwLock<Config>,
1026        pub generating: RwLock<GeneratingState>,
1027        path: PathBuf,
1028    }
1029
1030    impl Manager {
1031        pub fn new(dir: PathBuf) -> Self {
1032            let path = dir.join("config.json");
1033            let default = Config {
1034                default_model: "gpt-4o".to_string(),
1035                default_prompt: "You are a helpful assistant.".to_string(),
1036                ..Default::default()
1037            };
1038            let config = load_json_data(default.clone(), path.clone()).unwrap_or(default);
1039            Self {
1040                config: RwLock::new(config),
1041                generating: RwLock::new(GeneratingState::default()),
1042                path,
1043            }
1044        }
1045
1046        pub fn save(&self, cfg: &Config) {
1047            let _ = save_json_data(cfg, &self.path);
1048        }
1049
1050        pub async fn fetch_models(&self) -> anyhow::Result<Vec<String>> {
1051            let (base, key) = {
1052                let c = self.config.read().await;
1053                (c.api_base.clone(), c.api_key.clone())
1054            };
1055
1056            if base.is_empty() {
1057                return Err(anyhow::anyhow!("API未配置"));
1058            }
1059
1060            let config = OpenAIConfig::new().with_api_base(base).with_api_key(key);
1061
1062            let client = Client::with_config(config);
1063
1064            let response = client.models().list().await?;
1065
1066            // 提取模型 ID 并排序
1067            let mut models: Vec<String> = response.data.into_iter().map(|m| m.id).collect();
1068
1069            models.sort();
1070
1071            let filtered = super::utils::filter_models(&models);
1072            let final_models = if filtered.is_empty() {
1073                models
1074            } else {
1075                filtered
1076            };
1077
1078            {
1079                let mut c = self.config.write().await;
1080                c.models = final_models.clone();
1081                self.save(&c);
1082            }
1083            Ok(final_models)
1084        }
1085
1086        pub fn resolve_model(&self, input: &str, models: &[String]) -> Option<String> {
1087            if input.is_empty() {
1088                return None;
1089            }
1090            if let Ok(i) = input.parse::<usize>()
1091                && i > 0
1092                && i <= models.len()
1093            {
1094                return Some(models[i - 1].clone());
1095            }
1096            let lower = input.to_lowercase();
1097            for m in models {
1098                if m.to_lowercase().contains(&lower) {
1099                    return Some(m.clone());
1100                }
1101            }
1102            Some(input.to_string())
1103        }
1104
1105        pub async fn agent_names(&self) -> Vec<String> {
1106            self.config
1107                .read()
1108                .await
1109                .agents
1110                .iter()
1111                .map(|a| a.name.clone())
1112                .collect()
1113        }
1114    }
1115}
1116
1117// --- 业务逻辑 ---
1118mod logic {
1119    use crate::utils::truncate_str;
1120
1121    use super::data::Manager;
1122    use super::parser::{Action, Command, Scope};
1123    use super::types::{Agent, ChatMessage};
1124    use super::utils::{escape_markdown_special, format_export_txt, format_history, render_md};
1125    use async_openai::{
1126        Client,
1127        config::OpenAIConfig,
1128        types::{
1129            ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
1130            ChatCompletionRequestMessageContentPartImageArgs,
1131            ChatCompletionRequestMessageContentPartTextArgs,
1132            ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
1133            CreateChatCompletionRequestArgs, ImageUrlArgs,
1134        },
1135    };
1136    use kovi::bot::message::Message;
1137    use kovi_plugin_expand_napcat::NapCatApi;
1138    use regex::Regex;
1139    use std::{fs::File, io::Write, sync::Arc};
1140
1141    pub(crate) fn reply_text(event: &Arc<kovi::MsgEvent>, text: impl Into<String>) {
1142        event.reply(
1143            Message::new()
1144                .add_reply(event.message_id)
1145                .add_text(text.into()),
1146        );
1147    }
1148
1149    async fn reply(event: &Arc<kovi::MsgEvent>, text: &str, text_mode: bool, header: &str) {
1150        let msg = Message::new().add_reply(event.message_id);
1151
1152        if text_mode {
1153            event.reply(msg.add_text(text));
1154            return;
1155        }
1156        match render_md(text, header).await {
1157            Ok(b64) => event.reply(msg.add_image(&format!("base64://{}", b64))),
1158            Err(_) => {
1159                let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
1160                let clean_text = re.replace_all(text, "[图片渲染失败]").to_string();
1161                event.reply(msg.add_text(&clean_text));
1162            }
1163        }
1164    }
1165
1166    fn extract_image_urls(content: &str) -> Vec<String> {
1167        let re = Regex::new(
1168                    r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)|(?:https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp))",
1169                )
1170                .unwrap();
1171
1172        let mut urls: Vec<String> = re
1173            .captures_iter(content)
1174            .filter_map(|cap| cap.get(1).or(cap.get(0)).map(|m| m.as_str().to_string()))
1175            .collect();
1176
1177        let mut seen = std::collections::HashSet::new();
1178        urls.retain(|url| seen.insert(url.clone()));
1179
1180        urls
1181    }
1182
1183    fn extract_video_urls(content: &str) -> Vec<String> {
1184        // 匹配 [download video](url)
1185        let re = Regex::new(r"\[download video\]\((https?://[^\s\)]+)\)").unwrap();
1186        re.captures_iter(content)
1187            .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
1188            .collect()
1189    }
1190
1191    #[allow(clippy::too_many_arguments)]
1192    async fn chat(
1193        name: &str,
1194        prompt: &str,
1195        imgs: Vec<String>,
1196        regen: bool,
1197        cmd: &Command,
1198        event: &Arc<kovi::MsgEvent>,
1199        mgr: &Arc<Manager>,
1200        bot: &Arc<kovi::RuntimeBot>,
1201    ) {
1202        struct ChatContext<'a> {
1203            name: &'a str,
1204            prompt: &'a str,
1205            imgs: Vec<String>,
1206            regen: bool,
1207            cmd: &'a Command,
1208            event: &'a Arc<kovi::MsgEvent>,
1209            mgr: &'a Arc<Manager>,
1210            bot: &'a Arc<kovi::RuntimeBot>,
1211        }
1212
1213        async fn inner(ctx: ChatContext<'_>) {
1214            let is_priv_ctx = ctx.cmd.private_reply;
1215            let uid = ctx.event.user_id.to_string();
1216
1217            {
1218                let generating = ctx.mgr.generating.read().await;
1219                if generating.is_generating(ctx.name, is_priv_ctx, &uid) {
1220                    reply_text(ctx.event, "⏳ 正在生成中,请等待或使用 智能体! 停止");
1221                    return;
1222                }
1223            }
1224
1225            let (agent, api) = {
1226                let c = ctx.mgr.config.read().await;
1227                let a = c.agents.iter().find(|a| a.name == ctx.name).cloned();
1228                (a, (c.api_base.clone(), c.api_key.clone()))
1229            };
1230
1231            let agent = match agent {
1232                Some(a) => a,
1233                None => {
1234                    reply_text(ctx.event, format!("❌ 智能体 {} 不存在", ctx.name));
1235                    return;
1236                }
1237            };
1238
1239            if api.0.is_empty() || api.1.is_empty() {
1240                reply_text(ctx.event, "❌ API 未配置");
1241                return;
1242            }
1243
1244            match ctx
1245                .bot
1246                .set_msg_emoji_like(ctx.event.message_id.into(), "124")
1247                .await
1248            {
1249                Ok(_) => {
1250                    // kovi::log::info!("点赞成功");
1251                }
1252                Err(e) => {
1253                    kovi::log::error!("点赞失败: {:?}", e);
1254                }
1255            }
1256
1257            let mut hist = agent.history(is_priv_ctx, &uid).to_vec();
1258
1259            if ctx.regen {
1260                if hist.last().map(|m| m.role == "assistant").unwrap_or(false) {
1261                    hist.pop();
1262                }
1263                if !ctx.prompt.is_empty() {
1264                    if hist.last().map(|m| m.role == "user").unwrap_or(false) {
1265                        hist.pop();
1266                    }
1267                    hist.push(ChatMessage::new("user", ctx.prompt, ctx.imgs.clone()));
1268                }
1269            } else {
1270                if ctx.prompt.is_empty() && ctx.imgs.is_empty() {
1271                    reply_text(ctx.event, "💬 请输入内容");
1272                    return;
1273                }
1274                hist.push(ChatMessage::new("user", ctx.prompt, ctx.imgs.clone()));
1275            }
1276
1277            let gen_id = {
1278                let mut c = ctx.mgr.config.write().await;
1279                if let Some(a) = c.agents.iter_mut().find(|a| a.name == ctx.name) {
1280                    *a.history_mut(is_priv_ctx, &uid) = hist.clone();
1281                    a.generation_id += 1;
1282                    let id = a.generation_id;
1283                    ctx.mgr.save(&c);
1284                    id
1285                } else {
1286                    return;
1287                }
1288            };
1289
1290            {
1291                let mut generating = ctx.mgr.generating.write().await;
1292                generating.set_generating(ctx.name, is_priv_ctx, &uid, true);
1293            }
1294
1295            let client =
1296                Client::with_config(OpenAIConfig::new().with_api_base(api.0).with_api_key(api.1));
1297
1298            let mut msgs: Vec<ChatCompletionRequestMessage> = vec![];
1299
1300            if !agent.system_prompt.is_empty() {
1301                msgs.push(
1302                    ChatCompletionRequestSystemMessageArgs::default()
1303                        .content(agent.system_prompt.clone())
1304                        .build()
1305                        .unwrap()
1306                        .into(),
1307                );
1308            }
1309            let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
1310            for m in &hist {
1311                if m.role == "user" {
1312                    let mut parts = Vec::new();
1313                    if !m.content.is_empty() {
1314                        parts.push(
1315                            ChatCompletionRequestMessageContentPartTextArgs::default()
1316                                .text(m.content.clone())
1317                                .build()
1318                                .unwrap()
1319                                .into(),
1320                        );
1321                    }
1322                    for url in &m.images {
1323                        parts.push(
1324                            ChatCompletionRequestMessageContentPartImageArgs::default()
1325                                .image_url(ImageUrlArgs::default().url(url).build().unwrap())
1326                                .build()
1327                                .unwrap()
1328                                .into(),
1329                        );
1330                    }
1331                    if parts.is_empty() {
1332                        continue;
1333                    }
1334                    msgs.push(
1335                        ChatCompletionRequestUserMessageArgs::default()
1336                            .content(parts)
1337                            .build()
1338                            .unwrap()
1339                            .into(),
1340                    );
1341                } else if m.role == "assistant" {
1342                    let clean_content = re.replace_all(&m.content, "[Image Created]").to_string();
1343
1344                    msgs.push(
1345                        ChatCompletionRequestAssistantMessageArgs::default()
1346                            .content(clean_content)
1347                            .build()
1348                            .unwrap()
1349                            .into(),
1350                    );
1351
1352                    let gen_imgs = extract_image_urls(&m.content);
1353                    if !gen_imgs.is_empty() {
1354                        let mut img_parts = Vec::new();
1355                        for url in gen_imgs {
1356                            img_parts.push(
1357                                ChatCompletionRequestMessageContentPartImageArgs::default()
1358                                    .image_url(ImageUrlArgs::default().url(url).build().unwrap())
1359                                    .build()
1360                                    .unwrap()
1361                                    .into(),
1362                            );
1363                        }
1364                        msgs.push(
1365                            ChatCompletionRequestUserMessageArgs::default()
1366                                .content(img_parts)
1367                                .build()
1368                                .unwrap()
1369                                .into(),
1370                        );
1371                    }
1372                }
1373            }
1374
1375            let req = match CreateChatCompletionRequestArgs::default()
1376                .model(&agent.model)
1377                .messages(msgs)
1378                .build()
1379            {
1380                Ok(r) => r,
1381                Err(e) => {
1382                    let mut generating = ctx.mgr.generating.write().await;
1383                    generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1384                    reply_text(ctx.event, format!("❌ 请求构建失败: {}", e));
1385                    return;
1386                }
1387            };
1388
1389            match kovi::tokio::time::timeout(
1390                std::time::Duration::from_secs(300),
1391                client.chat().create(req),
1392            )
1393            .await
1394            {
1395                // 情况 1: 触发超时 (超过 5 分钟)
1396                Err(_) => {
1397                    {
1398                        let mut generating = ctx.mgr.generating.write().await;
1399                        generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1400                    }
1401                    reply_text(
1402                        ctx.event,
1403                        "⏳ 请求超时:模型响应时间超过 5 分钟,已强制停止。",
1404                    );
1405                }
1406                // 情况 2: 请求在限时内完成 (包含 成功响应 或 API报错)
1407                Ok(result) => match result {
1408                    Ok(res) => {
1409                        {
1410                            let mut generating = ctx.mgr.generating.write().await;
1411                            generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1412                        }
1413
1414                        {
1415                            let c = ctx.mgr.config.read().await;
1416                            if let Some(a) = c.agents.iter().find(|a| a.name == ctx.name)
1417                                && a.generation_id != gen_id
1418                            {
1419                                return;
1420                            }
1421                        }
1422
1423                        if let Some(choice) = res.choices.first()
1424                            && let Some(content) = &choice.message.content
1425                        {
1426                            let msg_index = {
1427                                let c = ctx.mgr.config.read().await;
1428                                if let Some(a) = c.agents.iter().find(|a| a.name == ctx.name) {
1429                                    a.history(is_priv_ctx, &uid).len() + 1
1430                                } else {
1431                                    0
1432                                }
1433                            };
1434
1435                            {
1436                                let mut c = ctx.mgr.config.write().await;
1437                                if let Some(a) = c.agents.iter_mut().find(|a| a.name == ctx.name) {
1438                                    a.history_mut(is_priv_ctx, &uid).push(ChatMessage::new(
1439                                        "assistant",
1440                                        content,
1441                                        vec![],
1442                                    ));
1443                                }
1444                                ctx.mgr.save(&c);
1445                            }
1446
1447                            let image_urls = extract_image_urls(content);
1448
1449                            let header = format!(
1450                                "{} #{}回复{}",
1451                                agent.name,
1452                                msg_index,
1453                                if ctx.cmd.private_reply {
1454                                    " (私有)"
1455                                } else {
1456                                    ""
1457                                }
1458                            );
1459
1460                            let display_content = if !image_urls.is_empty() && !ctx.cmd.text_mode {
1461                                let urls_text = image_urls
1462                                    .iter()
1463                                    .map(|u| {
1464                                        if u.starts_with("data:") {
1465                                            "- [Base64 Image]".to_string()
1466                                        } else {
1467                                            format!("- {}", u)
1468                                        }
1469                                    })
1470                                    .collect::<Vec<_>>()
1471                                    .join("\n");
1472                                format!("{}\n\n---\n**图片链接:**\n{}", content, urls_text)
1473                            } else {
1474                                content.clone()
1475                            };
1476
1477                            let reply_text_content = if ctx.cmd.text_mode && !image_urls.is_empty()
1478                            {
1479                                // 使用与 extract_image_urls 相同的逻辑替换
1480                                let re =
1481                                    Regex::new(r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)")
1482                                        .unwrap();
1483                                re.replace_all(content, |caps: &regex::Captures| {
1484                                    let url = &caps[1];
1485                                    if url.starts_with("data:") {
1486                                        "[图片]".to_string()
1487                                    } else {
1488                                        url.to_string()
1489                                    }
1490                                })
1491                                .to_string()
1492                            } else {
1493                                display_content.clone()
1494                            };
1495
1496                            reply(ctx.event, &reply_text_content, ctx.cmd.text_mode, &header).await;
1497
1498                            for url in &image_urls {
1499                                if url.starts_with("data:") {
1500                                    if let Some(base64_data) = url.split(',').nth(1) {
1501                                        ctx.event.reply(
1502                                            Message::new()
1503                                                .add_image(&format!("base64://{}", base64_data)),
1504                                        );
1505                                    }
1506                                } else {
1507                                    ctx.event.reply(Message::new().add_image(url));
1508                                }
1509                            }
1510
1511                            let video_urls = extract_video_urls(content);
1512                            for url in video_urls {
1513                                // 使用 OneBot 标准 video 段发送,data 放 file 字段,框架会自动处理下载/转发
1514                                let mut vec = Vec::new();
1515                                let segment = kovi::bot::message::Segment::new(
1516                                    "video",
1517                                    kovi::serde_json::json!({
1518                                        "file": url
1519                                    }),
1520                                );
1521                                vec.push(segment);
1522                                let msg = kovi::bot::message::Message::from(vec);
1523                                ctx.event.reply(msg);
1524                            }
1525                        }
1526                    }
1527                    Err(e) => {
1528                        {
1529                            let mut generating = ctx.mgr.generating.write().await;
1530                            generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1531                        }
1532                        reply_text(ctx.event, format!("❌ API错误: {}", e));
1533                    }
1534                },
1535            }
1536        }
1537
1538        inner(ChatContext {
1539            name,
1540            prompt,
1541            imgs,
1542            regen,
1543            cmd,
1544            event,
1545            mgr,
1546            bot,
1547        })
1548        .await;
1549    }
1550
1551    pub async fn execute(
1552        cmd: Command,
1553        prompt: String,
1554        imgs: Vec<String>,
1555        event: &Arc<kovi::MsgEvent>,
1556        mgr: &Arc<Manager>,
1557        bot: &Arc<kovi::RuntimeBot>,
1558    ) {
1559        let name = &cmd.agent;
1560        let uid = event.user_id.to_string();
1561
1562        match cmd.action {
1563            Action::Chat => {
1564                chat(name, &prompt, imgs, false, &cmd, event, mgr, bot).await;
1565            }
1566
1567            Action::Regenerate => {
1568                chat(name, &cmd.args, imgs, true, &cmd, event, mgr, bot).await;
1569            }
1570
1571            Action::Stop => {
1572                let is_priv_ctx = cmd.private_reply;
1573                {
1574                    let mut generating = mgr.generating.write().await;
1575                    generating.set_generating(name, is_priv_ctx, &uid, false);
1576                }
1577                let mut c = mgr.config.write().await;
1578                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1579                    a.generation_id += 1;
1580                    mgr.save(&c);
1581                    reply_text(event, "🛑 已停止");
1582                } else {
1583                    reply_text(event, format!("❌ 智能体 {} 不存在", name));
1584                }
1585            }
1586
1587            Action::Copy => {
1588                if cmd.args.is_empty() {
1589                    reply_text(event, "❌ 请指定新名称: 智能体~#新名称");
1590                    return;
1591                }
1592
1593                if cmd.args.chars().count() > 7
1594                    || cmd.args.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
1595                {
1596                    reply_text(event, "❌ 名称限制:最多7字且不能包含指令符号");
1597                    return;
1598                }
1599
1600                let mut c = mgr.config.write().await;
1601                if c.agents.iter().any(|a| a.name == cmd.args) {
1602                    reply_text(event, format!("❌ {} 已存在", cmd.args));
1603                    return;
1604                }
1605                if let Some(src) = c.agents.iter().find(|a| a.name == *name).cloned() {
1606                    let mut new_agent = Agent::new(
1607                        &cmd.args,
1608                        &src.model,
1609                        &src.system_prompt,
1610                        &format!("复制自 {}", name),
1611                    );
1612                    new_agent.description = src.description.clone();
1613                    c.agents.push(new_agent);
1614                    mgr.save(&c);
1615                    reply_text(event, format!("📑 已复制 {} → {}", name, cmd.args));
1616                } else {
1617                    reply_text(event, format!("❌ {} 不存在", name));
1618                }
1619            }
1620
1621            Action::Rename => {
1622                if cmd.args.is_empty() {
1623                    reply_text(event, "❌ 请指定新名称: 智能体~=新名称");
1624                    return;
1625                }
1626
1627                if cmd.args.chars().count() > 7
1628                    || cmd.args.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
1629                {
1630                    reply_text(event, "❌ 名称限制:最多7字且不能包含指令符号");
1631                    return;
1632                }
1633
1634                let mut c = mgr.config.write().await;
1635                if c.agents.iter().any(|a| a.name == cmd.args) {
1636                    reply_text(event, format!("❌ 目标名称 {} 已存在", cmd.args));
1637                    return;
1638                }
1639
1640                // 先找要重命名的智能体的索引
1641                let idx_opt = c.agents.iter().position(|a| a.name == *name);
1642                if let Some(idx) = idx_opt {
1643                    c.agents[idx].name = cmd.args.clone();
1644                    mgr.save(&c);
1645                    reply_text(event, format!("🏷️ 已重命名 {} → {}", name, cmd.args));
1646                } else {
1647                    reply_text(event, format!("❌ {} 不存在", name));
1648                }
1649            }
1650
1651            Action::SetDesc => {
1652                if cmd.args.is_empty() {
1653                    reply_text(event, "❌ 请提供描述: 智能体:描述内容");
1654                    return;
1655                }
1656                let mut c = mgr.config.write().await;
1657                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1658                    a.description = cmd.args.clone();
1659                    mgr.save(&c);
1660                    reply_text(event, format!("📝 {} 描述已更新", name));
1661                } else {
1662                    reply_text(event, format!("❌ {} 不存在", name));
1663                }
1664            }
1665
1666            Action::SetModel => {
1667                if cmd.args.is_empty() {
1668                    reply_text(event, "❌ 请指定模型: 智能体%模型名");
1669                    return;
1670                }
1671                let mut c = mgr.config.write().await;
1672                let models = c.models.clone();
1673                if let Some(model) = mgr.resolve_model(&cmd.args, &models) {
1674                    if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1675                        let old = a.model.clone();
1676                        a.model = model.clone();
1677                        mgr.save(&c);
1678                        reply_text(event, format!("🔄 {} 模型: {} → {}", name, old, model));
1679                    } else {
1680                        reply_text(event, format!("❌ {} 不存在", name));
1681                    }
1682                } else {
1683                    reply_text(event, "❌ 无效模型");
1684                }
1685            }
1686
1687            Action::SetPrompt => {
1688                let mut c = mgr.config.write().await;
1689                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1690                    a.system_prompt = cmd.args.clone();
1691                    mgr.save(&c);
1692                    if cmd.args.is_empty() {
1693                        reply_text(event, format!("📝 {} 提示词已清空", name));
1694                    } else {
1695                        reply_text(event, format!("📝 {} 提示词已更新", name));
1696                    }
1697                } else {
1698                    reply_text(event, format!("❌ {} 不存在", name));
1699                }
1700            }
1701
1702            Action::ViewPrompt => {
1703                let c = mgr.config.read().await;
1704                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
1705                    if cmd.text_mode {
1706                        reply_text(event, &a.system_prompt);
1707                        return;
1708                    }
1709                    let prompt_display = if a.system_prompt.is_empty() {
1710                        "(空)".to_string()
1711                    } else {
1712                        escape_markdown_special(&a.system_prompt)
1713                    };
1714                    let content = format!(
1715                        "**模型**: `{}`\n\n**提示词**:\n```\n{}\n```",
1716                        a.model, prompt_display
1717                    );
1718                    reply(
1719                        event,
1720                        &content,
1721                        cmd.text_mode,
1722                        &format!("{} 系统提示词", a.name),
1723                    )
1724                    .await;
1725                } else {
1726                    reply_text(event, format!("❌ {} 不存在", name));
1727                }
1728            }
1729
1730            Action::List => {
1731                let c = mgr.config.read().await;
1732                if c.agents.is_empty() {
1733                    reply_text(event, "📋 暂无智能体,使用 ##名称 模型 提示词 创建");
1734                    return;
1735                }
1736
1737                // 分组逻辑:使用 BTreeMap 自动按模型名称排序
1738                use std::collections::BTreeMap;
1739                let mut groups: BTreeMap<String, Vec<(usize, &Agent)>> = BTreeMap::new();
1740
1741                // 遍历并分组 (保留原始索引 i+1 以便用户操作)
1742                for (i, a) in c.agents.iter().enumerate() {
1743                    groups.entry(a.model.clone()).or_default().push((i + 1, a));
1744                }
1745
1746                // 生成 HTML
1747                let mut html_parts = Vec::new();
1748
1749                // 遍历每一个模型分组
1750                for (model, mut agents) in groups {
1751                    // 组内按智能体名称排序
1752                    agents.sort_by(|a, b| a.1.name.to_lowercase().cmp(&b.1.name.to_lowercase()));
1753
1754                    // 组头
1755                    html_parts.push(format!(
1756                                              r#"<div class="model-group"><div class="model-header"><span>📦 {}</span><span class="model-count">{}</span></div><div class="agent-grid">"#,
1757                                              model, agents.len()
1758                                          ));
1759
1760                    // 组内网格
1761                    for (real_idx, a) in agents {
1762                        // 逻辑:优先显示描述;如果没有描述,则截取系统提示词的前 20 个字作为预览;
1763                        let desc_display = if !a.description.is_empty() {
1764                            truncate_str(&a.description, 20)
1765                        } else if !a.system_prompt.is_empty() {
1766                            truncate_str(&a.system_prompt, 20)
1767                        } else {
1768                            "无描述".to_string()
1769                        };
1770
1771                        html_parts.push(format!(
1772                                            r#"<div class="agent-mini"><div class="agent-mini-top"><div class="agent-idx">{}</div><div class="agent-mini-name">{}</div></div><div class="agent-mini-desc">{}</div></div>"#,
1773                                            real_idx, a.name, desc_display
1774                                        ));
1775                    }
1776                    html_parts.push("</div></div>".to_string());
1777                }
1778
1779                let list = html_parts.join("\n");
1780
1781                reply(
1782                    event,
1783                    &list,
1784                    cmd.text_mode,
1785                    &format!("📋 智能体列表 (共{}个)", c.agents.len()),
1786                )
1787                .await;
1788            }
1789
1790            Action::Delete => {
1791                let mut c = mgr.config.write().await;
1792                if let Some(idx) = c.agents.iter().position(|a| a.name == *name) {
1793                    c.agents.remove(idx);
1794                    mgr.save(&c);
1795                    reply_text(event, format!("🗑️ 已删除 {}", name));
1796                } else {
1797                    reply_text(event, format!("❌ {} 不存在", name));
1798                }
1799            }
1800
1801            Action::ListModels => {
1802                let c = mgr.config.read().await;
1803
1804                // 1. 如果配置为空,尝试抓取
1805                if c.models.is_empty() {
1806                    drop(c);
1807                    reply_text(event, "⏳ 正在获取模型列表...");
1808                    if let Err(e) = mgr.fetch_models().await {
1809                        reply_text(event, format!("❌ 获取失败: {}", e));
1810                        return;
1811                    }
1812                }
1813
1814                // 重新读取
1815                let c = mgr.config.read().await;
1816                let models = &c.models;
1817
1818                if models.is_empty() {
1819                    reply_text(event, "📭 未找到可用模型 (请检查过滤关键字)");
1820                    return;
1821                }
1822
1823                // 2. 统计使用热度 (哪个模型被多少个智能体使用了)
1824                use std::collections::HashMap;
1825                let mut usage_count = HashMap::new();
1826                for agent in &c.agents {
1827                    *usage_count.entry(agent.model.clone()).or_insert(0) += 1;
1828                }
1829
1830                // 3. 动态分组逻辑
1831                // 直接利用 utils::MODEL_KEYWORDS 进行分组
1832                let mut groups: HashMap<String, Vec<(usize, String)>> = HashMap::new();
1833                let mut other_models = Vec::new();
1834
1835                for (i, m) in models.iter().enumerate() {
1836                    let idx = i + 1;
1837                    let lower = m.to_lowercase();
1838                    let mut matched = false;
1839
1840                    for &kw in crate::utils::MODEL_KEYWORDS {
1841                        if lower.contains(kw) {
1842                            // 将关键字首字母大写作为组名 (e.g. "gpt-5" -> "Gpt-5 Series")
1843                            let group_name = format!(
1844                                "{} Series",
1845                                kw.chars().next().unwrap().to_uppercase().to_string() + &kw[1..]
1846                            );
1847                            groups.entry(group_name).or_default().push((idx, m.clone()));
1848                            matched = true;
1849                            break;
1850                        }
1851                    }
1852
1853                    if !matched {
1854                        other_models.push((idx, m.clone()));
1855                    }
1856                }
1857
1858                // 4. 生成 HTML
1859                let mut html = String::new();
1860
1861                // 辅助渲染函数
1862                let render_group = |title: &str, items: &Vec<(usize, String)>| -> String {
1863                    let mut s = format!(
1864                        r#"<div class="mod-group"><div class="mod-title">{}</div><div class="chip-box">"#,
1865                        title
1866                    );
1867                    for (idx, name) in items {
1868                        let badge = if let Some(cnt) = usage_count.get(name) {
1869                            format!(r#"<span class="chip-bad">{}用</span>"#, cnt)
1870                        } else {
1871                            String::new()
1872                        };
1873                        s.push_str(&format!(
1874                                        r#"<div class="chip"><span class="chip-idx">{}</span><span class="chip-name">{}</span>{}</div>"#,
1875                                        idx, name, badge
1876                                    ));
1877                    }
1878                    s.push_str("</div></div>");
1879                    s
1880                };
1881
1882                // 按 MODEL_KEYWORDS 的定义顺序渲染 (保证顺序可控)
1883                for &kw in crate::utils::MODEL_KEYWORDS {
1884                    let group_name = format!(
1885                        "{} Series",
1886                        kw.chars().next().unwrap().to_uppercase().to_string() + &kw[1..]
1887                    );
1888                    if let Some(items) = groups.get(&group_name) {
1889                        html.push_str(&render_group(&group_name, items));
1890                    }
1891                }
1892
1893                // 渲染未分类的模型 (如果有漏网之鱼)
1894                if !other_models.is_empty() {
1895                    html.push_str(&render_group("Other Models", &other_models));
1896                }
1897
1898                // 5. 发送
1899                reply(
1900                    event,
1901                    &html,
1902                    cmd.text_mode,
1903                    &format!("🧩 模型列表 (共{}个)", models.len()),
1904                )
1905                .await;
1906            }
1907
1908            Action::ViewAll(scope) => {
1909                let c = mgr.config.read().await;
1910                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
1911                    let priv_scope = matches!(scope, Scope::Private);
1912                    let hist = a.history(priv_scope, &uid);
1913                    if hist.is_empty() {
1914                        let s = if priv_scope { "私有" } else { "公有" };
1915                        reply_text(event, format!("📭 {} {}历史为空", name, s));
1916                        return;
1917                    }
1918                    let content = format_history(hist, 0, cmd.text_mode);
1919                    let header = format!(
1920                        "{} {}历史 ({} 条)",
1921                        name,
1922                        if priv_scope { "私有" } else { "公有" },
1923                        hist.len()
1924                    );
1925                    reply(event, &content, cmd.text_mode, &header).await;
1926                } else {
1927                    reply_text(event, format!("❌ {} 不存在", name));
1928                }
1929            }
1930
1931            Action::ViewAt(scope) => {
1932                if cmd.indices.is_empty() {
1933                    reply_text(event, "❌ 请指定索引: 智能体/索引");
1934                    return;
1935                }
1936                let c = mgr.config.read().await;
1937                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
1938                    let priv_scope = matches!(scope, Scope::Private);
1939                    let hist = a.history(priv_scope, &uid);
1940                    let mut results = Vec::new();
1941                    let mut extra_images = Vec::new();
1942
1943                    let re =
1944                        Regex::new(r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)").unwrap();
1945
1946                    for i in &cmd.indices {
1947                        if *i > 0 && *i <= hist.len() {
1948                            let m = &hist[i - 1];
1949                            let emoji = match m.role.as_str() {
1950                                "user" => "👤",
1951                                "assistant" => "🤖",
1952                                _ => "❓",
1953                            };
1954
1955                            let mut content = m.content.clone();
1956                            let mut msg_imgs = extract_image_urls(&content);
1957                            msg_imgs.extend(m.images.clone());
1958
1959                            if cmd.text_mode {
1960                                content = re
1961                                    .replace_all(&content, |caps: &regex::Captures| {
1962                                        let url = &caps[1];
1963                                        if url.starts_with("data:") {
1964                                            "[图片]".to_string()
1965                                        } else {
1966                                            url.to_string()
1967                                        }
1968                                    })
1969                                    .to_string();
1970                            }
1971
1972                            if !m.images.is_empty() {
1973                                if !content.is_empty() {
1974                                    content.push_str("\n\n");
1975                                }
1976                                for url in &m.images {
1977                                    if cmd.text_mode {
1978                                        if url.starts_with("data:") {
1979                                            content.push_str("\n- [Base64 Image]");
1980                                        } else {
1981                                            content.push_str(&format!("\n- {}", url));
1982                                        }
1983                                    } else {
1984                                        content.push_str(&format!("\n![image]({})", url));
1985                                    }
1986                                }
1987                            }
1988
1989                            extra_images.extend(msg_imgs);
1990
1991                            results.push(format!("**#{} {}**\n{}", i, emoji, content));
1992                        }
1993                    }
1994
1995                    if results.is_empty() {
1996                        reply_text(event, "❌ 索引无效");
1997                    } else {
1998                        reply(
1999                            event,
2000                            &results.join("\n\n---\n\n"),
2001                            cmd.text_mode,
2002                            &format!("{} 历史记录", name),
2003                        )
2004                        .await;
2005
2006                        for url in extra_images {
2007                            if url.starts_with("data:") {
2008                                if let Some(base64_data) = url.split(',').nth(1) {
2009                                    event.reply(
2010                                        Message::new()
2011                                            .add_image(&format!("base64://{}", base64_data)),
2012                                    );
2013                                }
2014                            } else {
2015                                event.reply(Message::new().add_image(&url));
2016                            }
2017                        }
2018                    }
2019                } else {
2020                    reply_text(event, format!("❌ {} 不存在", name));
2021                }
2022            }
2023
2024            Action::Export(scope) => {
2025                let c = mgr.config.read().await;
2026                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
2027                    let priv_scope = matches!(scope, Scope::Private);
2028                    let hist = a.history(priv_scope, &uid);
2029                    if hist.is_empty() {
2030                        reply_text(event, "📭 历史为空");
2031                        return;
2032                    }
2033
2034                    let scope_str = if priv_scope { "私有" } else { "公有" };
2035                    let content = format_export_txt(name, &a.model, scope_str, hist);
2036
2037                    let scope_file = if priv_scope { "private" } else { "public" };
2038                    let fname = format!(
2039                        "{}_{}_{}_{}.txt",
2040                        name,
2041                        scope_file,
2042                        uid,
2043                        chrono::Local::now().format("%Y%m%d%H%M%S")
2044                    );
2045                    let path = bot.get_data_path().join(&fname);
2046                    match File::create(&path) {
2047                        Ok(mut f) => {
2048                            if f.write_all(content.as_bytes()).is_ok() {
2049                                let path_str = path.to_string_lossy().to_string();
2050                                let result = if let Some(gid) = event.group_id {
2051                                    bot.upload_group_file(gid, &path_str, &fname, None).await
2052                                } else {
2053                                    bot.upload_private_file(event.user_id, &path_str, &fname)
2054                                        .await
2055                                };
2056                                match result {
2057                                    Ok(_) => reply_text(event, format!("📤 已导出: {}", fname)),
2058                                    Err(e) => reply_text(event, format!("❌ 上传失败: {}", e)),
2059                                }
2060                            } else {
2061                                reply_text(event, "❌ 写入失败");
2062                            }
2063                        }
2064                        Err(e) => reply_text(event, format!("❌ 创建文件失败: {}", e)),
2065                    }
2066                } else {
2067                    reply_text(event, format!("❌ {} 不存在", name));
2068                }
2069            }
2070
2071            Action::EditAt(scope) => {
2072                if cmd.indices.is_empty() {
2073                    reply_text(event, "❌ 请指定索引: 智能体'索引 新内容");
2074                    return;
2075                }
2076                if cmd.args.is_empty() {
2077                    reply_text(event, "❌ 请提供新内容");
2078                    return;
2079                }
2080                let idx = cmd.indices[0];
2081                let mut c = mgr.config.write().await;
2082                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2083                    let priv_scope = matches!(scope, Scope::Private);
2084                    if a.edit_at(priv_scope, &uid, idx, &cmd.args) {
2085                        mgr.save(&c);
2086                        reply_text(event, format!("✏️ 已编辑第 {} 条", idx));
2087                    } else {
2088                        reply_text(event, format!("❌ 索引 {} 无效", idx));
2089                    }
2090                } else {
2091                    reply_text(event, format!("❌ {} 不存在", name));
2092                }
2093            }
2094
2095            Action::DeleteAt(scope) => {
2096                if cmd.indices.is_empty() {
2097                    reply_text(event, "❌ 请指定索引: 智能体-索引 (支持 1,3,5 或 1-5)");
2098                    return;
2099                }
2100                let mut c = mgr.config.write().await;
2101                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2102                    let priv_scope = matches!(scope, Scope::Private);
2103                    let deleted = a.delete_at(priv_scope, &uid, &cmd.indices);
2104                    if deleted.is_empty() {
2105                        reply_text(event, "❌ 索引无效");
2106                    } else {
2107                        mgr.save(&c);
2108                        let s = deleted
2109                            .iter()
2110                            .map(|i| i.to_string())
2111                            .collect::<Vec<_>>()
2112                            .join(", ");
2113                        reply_text(
2114                            event,
2115                            format!("🗑️ 已删除第 {} 条 (共{}条)", s, deleted.len()),
2116                        );
2117                    }
2118                } else {
2119                    reply_text(event, format!("❌ {} 不存在", name));
2120                }
2121            }
2122
2123            Action::ClearHistory(scope) => {
2124                let is_priv_ctx = cmd.private_reply;
2125                {
2126                    let mut generating = mgr.generating.write().await;
2127                    generating.set_generating(name, is_priv_ctx, &uid, false);
2128                }
2129                let mut c = mgr.config.write().await;
2130                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2131                    let priv_scope = matches!(scope, Scope::Private);
2132                    let s = if priv_scope { "私有" } else { "公有" };
2133                    a.clear_history(priv_scope, &uid);
2134                    a.generation_id += 1;
2135                    mgr.save(&c);
2136                    reply_text(event, format!("🧹 {} {}历史已清空", name, s));
2137                } else {
2138                    reply_text(event, format!("❌ {} 不存在", name));
2139                }
2140            }
2141
2142            Action::ClearAllPublic => {
2143                {
2144                    let mut generating = mgr.generating.write().await;
2145                    generating.public.clear();
2146                }
2147                let mut c = mgr.config.write().await;
2148                let cnt = c.agents.len();
2149                for a in c.agents.iter_mut() {
2150                    a.public_history.clear();
2151                    a.generation_id += 1;
2152                }
2153                mgr.save(&c);
2154                reply_text(event, format!("🧹 已清空 {} 个智能体的公有历史", cnt));
2155            }
2156
2157            Action::ClearEverything => {
2158                {
2159                    let mut generating = mgr.generating.write().await;
2160                    generating.public.clear();
2161                    generating.private.clear();
2162                }
2163                let mut c = mgr.config.write().await;
2164                let cnt = c.agents.len();
2165                for a in c.agents.iter_mut() {
2166                    a.public_history.clear();
2167                    a.private_histories.clear();
2168                    a.generation_id += 1;
2169                }
2170                mgr.save(&c);
2171                reply_text(event, format!("⚠️ 已清空 {} 个智能体的所有历史", cnt));
2172            }
2173
2174            Action::Help => {
2175                let help = r#"## 模式前缀(可组合)
2176| 符号 | 含义 |
2177|:---:|------|
2178| `&` | 私有模式 |
2179| `"` | 文本模式 |
2180
2181## 智能体管理
2182| 指令 | 功能 | 示例 |
2183|------|------|------|
2184| `##名称 模型 提示词` | 创建/更新 | `##助手 gpt-4o 你是助手` |
2185| `##:模型` | 批量生成描述 | `##:gpt-4o` |
2186| `智能体~=新名` | 重命名 | `助手~=管家` |
2187| `智能体~#新名` | 复制 | `助手~#助手2` |
2188| `智能体:描述` | 设置描述 | `助手:通用助手` |
2189| `-#名称` | 删除 | `-#助手` |
2190| `/#` | 列表 | `/#` |
2191
2192## 配置修改
2193| 指令 | 功能 | 示例 |
2194|------|------|------|
2195| `智能体%模型` | 修改模型 | `助手%gpt-4` |
2196| `智能体$提示词` | 修改提示词 | `助手$你是...` |
2197| `智能体$` | 清空提示词 | `助手$` |
2198| `智能体/$` | 查看提示词 | `助手/$` |
2199| `/%` | 模型列表 | `/%` |
2200
2201## 对话控制
2202| 指令 | 功能 |
2203|------|------|
2204| `智能体 内容` | 对话 |
2205| `"智能体 内容` | 文本模式对话 |
2206| `&智能体 内容` | 私有对话 |
2207| `智能体~` | 重新生成 |
2208| `智能体!` | 停止生成 |
2209
2210## 历史管理
2211| 指令 | 功能 |
2212|------|------|
2213| `智能体/*` | 查看所有 |
2214| `智能体/1` | 查看第1条 |
2215| `智能体/1-5` | 查看1-5条 |
2216| `智能体_*` | 导出(.txt) |
2217| `智能体'1 新内容` | 编辑第1条 |
2218| `智能体-1` | 删除第1条 |
2219| `智能体-1,3,5` | 删除多条 |
2220| `智能体-1-5` | 删除范围 |
2221| `智能体-*` | 清空历史 |
2222
2223> 加 `&` 前缀操作私有历史: `&智能体/*`
2224
2225## 危险操作
2226| 指令 | 功能 |
2227|------|------|
2228| `-*` | 清空所有智能体公有历史 |
2229| `-*!` | 清空所有历史 |
2230
2231## API 配置
2232直接发送: `API地址 API密钥`
2233    "#;
2234                reply(event, help, cmd.text_mode, "🤖 OAI 符号指令帮助").await;
2235            }
2236
2237            Action::AutoFillDescriptions(model_ref) => {
2238                let (target_agents, api_config, use_model) = {
2239                    let c = mgr.config.read().await;
2240
2241                    // 1. 确定使用的模型
2242                    let models = c.models.clone();
2243                    let resolved_model = if model_ref.is_empty() {
2244                        c.default_model.clone()
2245                    } else {
2246                        mgr.resolve_model(&model_ref, &models).unwrap_or(model_ref)
2247                    };
2248
2249                    // 2. 筛选需要生成的智能体 (描述为空 或 仅仅是"新建智能体")
2250                    let targets: Vec<(String, String)> = c
2251                        .agents
2252                        .iter()
2253                        .filter(|a| a.description.is_empty() || a.description == "新建智能体")
2254                        .map(|a| (a.name.clone(), a.system_prompt.clone()))
2255                        .collect();
2256
2257                    (
2258                        targets,
2259                        (c.api_base.clone(), c.api_key.clone()),
2260                        resolved_model,
2261                    )
2262                };
2263
2264                if target_agents.is_empty() {
2265                    reply_text(event, "✅ 所有智能体均已有描述,无需处理。");
2266                    return;
2267                }
2268
2269                if api_config.0.is_empty() || api_config.1.is_empty() {
2270                    reply_text(event, "❌ API 未配置");
2271                    return;
2272                }
2273
2274                reply_text(
2275                    event,
2276                    format!(
2277                        "🤖 开始使用 [{}] 为 {} 个智能体生成描述,请稍候...",
2278                        use_model,
2279                        target_agents.len()
2280                    ),
2281                );
2282
2283                let client = Client::with_config(
2284                    OpenAIConfig::new()
2285                        .with_api_base(api_config.0)
2286                        .with_api_key(api_config.1),
2287                );
2288
2289                let mut success_count = 0;
2290
2291                for (name, prompt) in target_agents {
2292                    // 这里的 Prompt 专门用于生成简短描述
2293                    let gen_prompt = format!(
2294                        "请阅读以下角色的 System Prompt,为其生成一个极简短的中文功能描述(Role/Tag)。\n\
2295                                    要求:\n1. 必须控制在 10 个字以内\n2. 不要包含任何标点符号\n3. 直接输出描述内容,不要解释\n\n\
2296                                    System Prompt:\n{}",
2297                        prompt
2298                    );
2299
2300                    let req = CreateChatCompletionRequestArgs::default()
2301                        .model(&use_model)
2302                        .messages(vec![
2303                            ChatCompletionRequestUserMessageArgs::default()
2304                                .content(gen_prompt)
2305                                .build()
2306                                .unwrap()
2307                                .into(),
2308                        ])
2309                        .build();
2310
2311                    if let Ok(req) = req
2312                        && let Ok(res) = client.chat().create(req).await
2313                        && let Some(choice) = res.choices.first()
2314                        && let Some(content) = &choice.message.content
2315                    {
2316                        let new_desc = content.trim().replace(['"', '“', '”', '。', '.'], ""); // 简单清洗
2317
2318                        // 获取写锁更新数据
2319                        let mut c = mgr.config.write().await;
2320                        if let Some(a) = c.agents.iter_mut().find(|a| a.name == name) {
2321                            a.description = new_desc.clone();
2322                            mgr.save(&c);
2323                            success_count += 1;
2324                        }
2325                    }
2326
2327                    // 小停顿,避免并发过高 (100毫秒)
2328                    kovi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2329                }
2330
2331                reply_text(
2332                    event,
2333                    format!("✅ 批量处理完成,已更新 {} 个智能体的描述。", success_count),
2334                );
2335            }
2336
2337            Action::Create => {}
2338        }
2339    }
2340
2341    pub async fn handle_create(
2342        name: &str,
2343        desc: &str,
2344        model: &str,
2345        prompt: &str,
2346        event: &Arc<kovi::MsgEvent>,
2347        mgr: &Arc<Manager>,
2348    ) {
2349        let mut c = mgr.config.write().await;
2350        let models = c.models.clone();
2351
2352        let model = mgr
2353            .resolve_model(model, &models)
2354            .unwrap_or_else(|| model.to_string());
2355
2356        let prompt = if prompt.is_empty() && !c.agents.iter().any(|a| a.name == name) {
2357            c.default_prompt.clone()
2358        } else {
2359            prompt.to_string()
2360        };
2361
2362        if let Some(a) = c.agents.iter_mut().find(|a| a.name == name) {
2363            if !model.is_empty() {
2364                a.model = model.clone();
2365            }
2366            a.system_prompt = prompt;
2367            if !desc.is_empty() {
2368                a.description = desc.to_string();
2369            }
2370            let updated_model = a.model.clone();
2371            mgr.save(&c);
2372            reply_text(
2373                event,
2374                format!("📝 已更新 {} (模型: {})", name, updated_model),
2375            );
2376        } else {
2377            let description = if desc.is_empty() {
2378                "新建智能体".to_string()
2379            } else {
2380                desc.to_string()
2381            };
2382            c.agents
2383                .push(Agent::new(name, &model, &prompt, &description));
2384            mgr.save(&c);
2385            reply_text(event, format!("🤖 已创建 {} (模型: {})", name, model));
2386        }
2387    }
2388}
2389
2390// --- 入口 ---
2391use crate::logic::reply_text;
2392use cdp_html_shot::Browser;
2393use kovi::PluginBuilder;
2394use std::sync::Arc;
2395
2396#[kovi::plugin]
2397async fn main() {
2398    let bot = PluginBuilder::get_runtime_bot();
2399    let mgr = Arc::new(data::Manager::new(bot.get_data_path()));
2400
2401    let m = mgr.clone();
2402    kovi::tokio::spawn(async move {
2403        let _ = m.fetch_models().await;
2404    });
2405
2406    let mgr_clone = mgr.clone();
2407    PluginBuilder::on_msg(move |event| {
2408        let mgr = mgr_clone.clone();
2409        let bot = bot.clone();
2410        async move {
2411            let raw = match event.borrow_text() {
2412                Some(v) => v,
2413                None => return,
2414            };
2415
2416            if let Some((url, key)) = utils::parse_api(raw) {
2417                let mut c = mgr.config.write().await;
2418                c.api_base = url.clone();
2419                c.api_key = key;
2420                mgr.save(&c);
2421                drop(c);
2422                reply_text(&event, format!("✅ API 已配置: {}", url));
2423                match mgr.fetch_models().await {
2424                    Ok(models) => reply_text(&event, format!("📋 已获取 {} 个模型", models.len())),
2425                    Err(e) => reply_text(&event, format!("⚠️ 获取模型失败: {}", e)),
2426                }
2427                return;
2428            }
2429
2430            if let Some(cmd) = parser::parse_global(raw) {
2431                logic::execute(cmd, String::new(), vec![], &event, &mgr, &bot).await;
2432                return;
2433            }
2434
2435            if let Some((name, desc, model, prompt)) = parser::parse_create(raw) {
2436                logic::handle_create(&name, &desc, &model, &prompt, &event, &mgr).await;
2437                return;
2438            }
2439
2440            let agents = mgr.agent_names().await;
2441            if let Some(name) = parser::parse_delete_agent(raw, &agents) {
2442                let cmd = parser::Command::new(&name, parser::Action::Delete);
2443                logic::execute(cmd, String::new(), vec![], &event, &mgr, &bot).await;
2444                return;
2445            }
2446
2447            if let Some(cmd) = parser::parse_agent_cmd(raw, &agents) {
2448                let (quote, imgs) = utils::get_full_content(&event, &bot, Some(&cmd.agent)).await;
2449
2450                // 拼接提示词:引用 + 用户输入参数
2451                let prompt = if matches!(
2452                    cmd.action,
2453                    parser::Action::Chat | parser::Action::Regenerate
2454                ) {
2455                    format!("{}{}", quote, cmd.args).trim().to_string()
2456                } else {
2457                    cmd.args.clone()
2458                };
2459
2460                logic::execute(cmd, prompt, imgs, &event, &mgr, &bot).await;
2461            }
2462        }
2463    });
2464
2465    let mgr_drop = mgr.clone();
2466    PluginBuilder::drop({
2467        move || {
2468            let mgr = mgr_drop.clone();
2469            async move {
2470                // 保存配置
2471                let c = mgr.config.read().await;
2472                mgr.save(&c);
2473                // 关闭全局浏览器实例
2474                // Browser::instance().await.close_async().await.unwrap();
2475                Browser::shutdown_global().await;
2476            }
2477        }
2478    });
2479}