Skip to main content

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                    && !found_trigger
472                {
473                    let text = seg.data.get("text").and_then(|v| v.as_str()).unwrap_or("");
474                    // 对比前先进行标准化和转小写,以匹配 parser 的逻辑
475                    let norm_text = normalize(text).to_lowercase();
476                    let norm_name = normalize(name).to_lowercase();
477
478                    if norm_text.contains(&norm_name) {
479                        found_trigger = true;
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                        && id != "all"
497                    {
498                        imgs.push(format!("https://q.qlogo.cn/g?b=qq&nk={}&s=640", id));
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        UpdateApi(String, String),
694    }
695
696    #[derive(Debug, Clone)]
697    pub struct Command {
698        pub agent: String,
699        pub action: Action,
700        pub args: String,
701        pub indices: Vec<usize>,
702        pub private_reply: bool,
703        pub text_mode: bool,
704        pub temp_mode: bool,
705    }
706
707    impl Command {
708        pub fn new(agent: &str, action: Action) -> Self {
709            Self {
710                agent: agent.to_string(),
711                action,
712                args: String::new(),
713                indices: Vec::new(),
714                private_reply: false,
715                text_mode: false,
716                temp_mode: false,
717            }
718        }
719    }
720
721    pub fn parse_global(raw: &str) -> Option<Command> {
722        let norm = normalize(raw.trim());
723
724        if norm.starts_with("oai") {
725            let rest = norm.get(3..).unwrap_or("").trim();
726            if rest.is_empty() {
727                return Some(Command::new("", Action::Help));
728            }
729            if let Some((u, k)) = super::utils::parse_api(rest) {
730                return Some(Command::new("", Action::UpdateApi(u, k)));
731            }
732        }
733
734        if norm == "/#" {
735            return Some(Command::new("", Action::List));
736        }
737
738        if norm == "/%" {
739            return Some(Command::new("", Action::ListModels));
740        }
741
742        if norm == "-*" {
743            return Some(Command::new("", Action::ClearAllPublic));
744        }
745
746        if norm == "-*!" {
747            return Some(Command::new("", Action::ClearEverything));
748        }
749
750        if norm.starts_with("##:") {
751            let args = norm.get(3..).unwrap_or("").trim().to_string();
752            return Some(Command::new("", Action::AutoFillDescriptions(args)));
753        }
754
755        None
756    }
757
758    pub fn parse_create(raw: &str) -> Option<(String, String, String, String)> {
759        let norm = normalize(raw.trim());
760        if !norm.starts_with("##") {
761            return None;
762        }
763
764        let start_pos = norm.find("##").unwrap() + "##".len();
765        let after = &raw.trim()[start_pos..];
766
767        let name_end = after
768            .find(|c: char| c.is_whitespace() || c == '(' || c == '(')
769            .unwrap_or(after.len());
770        let name = after[..name_end].trim().to_string();
771
772        if name.is_empty()
773            || name.chars().count() > 7
774            || name.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
775        {
776            return None;
777        }
778
779        let rest = &after[name_end..];
780
781        let (desc, after_desc) = if rest.starts_with('(') || rest.starts_with('(') {
782            if let Some(pos) = rest.find(')').or_else(|| rest.find(')')) {
783                (rest[1..pos].to_string(), &rest[pos + 1..])
784            } else {
785                (String::new(), rest)
786            }
787        } else {
788            (String::new(), rest)
789        };
790
791        let parts: Vec<&str> = after_desc.split_whitespace().collect();
792        let model = parts.first().unwrap_or(&"").to_string();
793        if model.chars().count() > 50 {
794            return None;
795        }
796        let prompt = if parts.len() > 1 {
797            parts[1..].join(" ")
798        } else {
799            String::new()
800        };
801
802        Some((name, desc, model, prompt))
803    }
804
805    pub fn parse_delete_agent(raw: &str, agents: &[String]) -> Option<String> {
806        let norm = normalize(raw.trim());
807        if !norm.starts_with("-#") {
808            return None;
809        }
810        let name = norm[2..].trim();
811        if agents.iter().any(|a| a.eq_ignore_ascii_case(name)) {
812            Some(name.to_string())
813        } else {
814            None
815        }
816    }
817
818    pub fn parse_agent_cmd(raw: &str, agents: &[String]) -> Option<Command> {
819        let raw = raw.trim();
820        if raw.is_empty() {
821            return None;
822        }
823
824        let norm = normalize(raw);
825        let chars: Vec<char> = norm.chars().collect();
826
827        let mut char_idx = 0;
828        let mut private_reply = false;
829        let mut text_mode = false;
830        let mut temp_mode = false;
831
832        // 1. 前缀解析
833        while char_idx < chars.len() {
834            match chars[char_idx] {
835                '&' => {
836                    private_reply = true;
837                    char_idx += 1;
838                }
839                '"' => {
840                    text_mode = true;
841                    char_idx += 1;
842                }
843                '~' => {
844                    temp_mode = true;
845                    char_idx += 1;
846                }
847                _ => break,
848            }
849        }
850
851        let byte_idx: usize = chars.iter().take(char_idx).map(|c| c.len_utf8()).sum();
852        let content = &norm[byte_idx..];
853
854        // 2. 智能体名称匹配 (按长度降序,解决包含关系问题)
855        let mut agent_name = String::new();
856        let mut match_char_len = 0;
857        let mut sorted = agents.to_vec();
858        // 关键:必须按长度倒序,确保 "小帅2" 先于 "小帅" 被匹配
859        sorted.sort_by_key(|b| std::cmp::Reverse(b.chars().count()));
860
861        for name in &sorted {
862            let name_lower = name.to_lowercase();
863            let content_lower = content.to_lowercase();
864            if content_lower.starts_with(&name_lower) {
865                agent_name = name.clone();
866                match_char_len = name.chars().count();
867                break;
868            }
869        }
870
871        if agent_name.is_empty() {
872            return None;
873        }
874
875        // 3. 后缀提取
876        let match_byte_len: usize = content
877            .chars()
878            .take(match_char_len)
879            .map(|c| c.len_utf8())
880            .sum();
881        let suffix = content[match_byte_len..].trim();
882
883        // 计算原始字符串中的后缀部分(为了保留参数的原始格式,如大小写)
884        let raw_suffix = {
885            let prefix_bytes: usize = raw.chars().take(char_idx).map(|c| c.len_utf8()).sum();
886            let agent_bytes: usize = raw[prefix_bytes..]
887                .chars()
888                .take(match_char_len)
889                .map(|c| c.len_utf8())
890                .sum();
891            raw[prefix_bytes + agent_bytes..].trim()
892        };
893
894        let (action, args, indices) = parse_suffix(suffix, raw_suffix, private_reply);
895
896        Some(Command {
897            agent: agent_name,
898            action,
899            args,
900            indices,
901            private_reply,
902            text_mode,
903            temp_mode,
904        })
905    }
906
907    fn parse_suffix(norm: &str, raw: &str, has_priv_prefix: bool) -> (Action, String, Vec<usize>) {
908        let s = norm.trim(); // 此时 s 里的全角符号已被 normalize 转为半角
909        let r = raw.trim(); // r 是原始字符串
910
911        // 1. 空指令 -> 聊天
912        if s.is_empty() {
913            return (Action::Chat, r.to_string(), vec![]);
914        }
915
916        // 2. 停止指令 (!)
917        if s == "!" {
918            return (Action::Stop, String::new(), vec![]);
919        }
920
921        // 3. 复制指令 (~#) - 必须在普通 ~ 之前判断
922        // 注意:normalize 已经把 ~ 转为 ~,把 # 转为 #
923        if s.starts_with("~#") {
924            // 计算原始字符串中需要跳过的长度
925            let skip_len = if r.starts_with("~#") {
926                "~#".len()
927            } else if r.starts_with("~#") {
928                "~#".len()
929            } else if r.starts_with("~#") {
930                "~#".len()
931            } else {
932                "~#".len()
933            };
934            let arg = r.get(skip_len..).unwrap_or("").trim();
935            return (Action::Copy, arg.to_string(), vec![]);
936        }
937
938        // 4. 重命名指令 (~=) - 必须在普通 ~ 之前判断
939        if s.starts_with("~=") {
940            let skip_len = if r.starts_with("~=") {
941                "~=".len()
942            } else if r.starts_with("~=") {
943                "~=".len()
944            } else if r.starts_with("~=") {
945                "~=".len()
946            } else {
947                "~=".len()
948            };
949            let arg = r.get(skip_len..).unwrap_or("").trim();
950            return (Action::Rename, arg.to_string(), vec![]);
951        }
952
953        // 5. 重新生成指令 (~) - 放在最后判断
954        // 匹配 "~" 单独出现,或者 "~内容"
955        if s.starts_with('~') {
956            let skip_len = if r.starts_with('~') {
957                '~'.len_utf8()
958            } else {
959                '~'.len_utf8()
960            };
961            let arg = r.get(skip_len..).unwrap_or("").trim();
962            return (Action::Regenerate, arg.to_string(), vec![]);
963        }
964
965        // 6. 设置描述 (:)
966        if s.starts_with(':') && !s.starts_with(":/") {
967            let skip_len = if r.starts_with(':') {
968                ':'.len_utf8()
969            } else {
970                ':'.len_utf8()
971            };
972            let arg = r.get(skip_len..).unwrap_or("").trim();
973            return (Action::SetDesc, arg.to_string(), vec![]);
974        }
975
976        // 7. 设置模型 (%)
977        if s.starts_with('%') {
978            let arg = r.get(1..).unwrap_or("").trim();
979            return (Action::SetModel, arg.to_string(), vec![]);
980        }
981
982        // 8. 设置/查看提示词 ($)
983        if s == "/$" {
984            return (Action::ViewPrompt, String::new(), vec![]);
985        }
986        if s.starts_with('$') {
987            let arg = r.get(1..).unwrap_or("").trim();
988            return (Action::SetPrompt, arg.to_string(), vec![]);
989        }
990
991        // 9. 历史/查看/编辑/删除类操作
992        // 处理 & 后缀 (局部私有操作,如 智能体&/*)
993        let (has_local_priv, clean, clean_raw) = if let Some(stripped) = s.strip_prefix('&') {
994            (true, stripped, r.strip_prefix('&').unwrap_or("").trim())
995        } else {
996            (false, s, r)
997        };
998
999        let scope = if has_priv_prefix || has_local_priv {
1000            Scope::Private
1001        } else {
1002            Scope::Public
1003        };
1004
1005        if clean == "/*" {
1006            return (Action::ViewAll(scope), String::new(), vec![]);
1007        }
1008
1009        if clean.starts_with('/') && clean.len() > 1 {
1010            let idx_part = &clean[1..];
1011            let indices = super::utils::parse_indices(idx_part);
1012            if !indices.is_empty() {
1013                return (Action::ViewAt(scope), String::new(), indices);
1014            }
1015        }
1016
1017        if clean == "_*" {
1018            return (Action::Export(scope), String::new(), vec![]);
1019        }
1020
1021        // 编辑指令 ('): 支持 '1 新内容
1022        if clean.starts_with('\'') {
1023            // splitn(2) 确保只分割出索引和内容两部分
1024            let parts: Vec<&str> = clean_raw.get(1..).unwrap_or("").splitn(2, ' ').collect();
1025            if !parts.is_empty() {
1026                let indices = super::utils::parse_indices(parts[0]);
1027                let content = parts.get(1).unwrap_or(&"").to_string();
1028                return (Action::EditAt(scope), content, indices);
1029            }
1030        }
1031
1032        if clean == "-*" {
1033            return (Action::ClearHistory(scope), String::new(), vec![]);
1034        }
1035
1036        if clean.starts_with('-') && clean.len() > 1 {
1037            let idx_part = &clean[1..];
1038            let indices = super::utils::parse_indices(idx_part);
1039            if !indices.is_empty() {
1040                return (Action::DeleteAt(scope), String::new(), indices);
1041            }
1042        }
1043
1044        // 默认 fallback: 视为普通聊天内容
1045        (Action::Chat, r.to_string(), vec![])
1046    }
1047}
1048
1049// --- 数据管理 ---
1050mod data {
1051    use super::types::{Config, GeneratingState};
1052    use async_openai::Client;
1053    use async_openai::config::OpenAIConfig;
1054    use kovi::tokio::sync::RwLock;
1055    use kovi::utils::{load_json_data, save_json_data};
1056    use std::path::PathBuf;
1057
1058    pub struct Manager {
1059        pub config: RwLock<Config>,
1060        pub generating: RwLock<GeneratingState>,
1061        path: PathBuf,
1062    }
1063
1064    impl Manager {
1065        pub fn new(dir: PathBuf) -> Self {
1066            let path = dir.join("config.json");
1067            let default = Config {
1068                default_model: "gpt-4o".to_string(),
1069                default_prompt: "You are a helpful assistant.".to_string(),
1070                ..Default::default()
1071            };
1072            let config = load_json_data(default.clone(), path.clone()).unwrap_or(default);
1073            Self {
1074                config: RwLock::new(config),
1075                generating: RwLock::new(GeneratingState::default()),
1076                path,
1077            }
1078        }
1079
1080        pub fn save(&self, cfg: &Config) {
1081            let _ = save_json_data(cfg, &self.path);
1082        }
1083
1084        pub async fn fetch_models(&self) -> anyhow::Result<Vec<String>> {
1085            let (base, key) = {
1086                let c = self.config.read().await;
1087                (c.api_base.clone(), c.api_key.clone())
1088            };
1089
1090            if base.is_empty() {
1091                return Err(anyhow::anyhow!("API未配置"));
1092            }
1093
1094            let config = OpenAIConfig::new().with_api_base(base).with_api_key(key);
1095
1096            let client = Client::with_config(config);
1097
1098            let response = client.models().list().await?;
1099
1100            // 提取模型 ID 并排序
1101            let mut models: Vec<String> = response.data.into_iter().map(|m| m.id).collect();
1102
1103            models.sort();
1104
1105            let filtered = super::utils::filter_models(&models);
1106            let final_models = if filtered.is_empty() {
1107                models
1108            } else {
1109                filtered
1110            };
1111
1112            {
1113                let mut c = self.config.write().await;
1114                c.models = final_models.clone();
1115                self.save(&c);
1116            }
1117            Ok(final_models)
1118        }
1119
1120        pub fn resolve_model(&self, input: &str, models: &[String]) -> Option<String> {
1121            if input.is_empty() {
1122                return None;
1123            }
1124            if let Ok(i) = input.parse::<usize>()
1125                && i > 0
1126                && i <= models.len()
1127            {
1128                return Some(models[i - 1].clone());
1129            }
1130            let lower = input.to_lowercase();
1131            for m in models {
1132                if m.to_lowercase().contains(&lower) {
1133                    return Some(m.clone());
1134                }
1135            }
1136            Some(input.to_string())
1137        }
1138
1139        pub async fn agent_names(&self) -> Vec<String> {
1140            self.config
1141                .read()
1142                .await
1143                .agents
1144                .iter()
1145                .map(|a| a.name.clone())
1146                .collect()
1147        }
1148    }
1149}
1150
1151// --- 业务逻辑 ---
1152mod logic {
1153    use crate::utils::truncate_str;
1154
1155    use super::data::Manager;
1156    use super::parser::{Action, Command, Scope};
1157    use super::types::{Agent, ChatMessage};
1158    use super::utils::{escape_markdown_special, format_export_txt, format_history, render_md};
1159    use async_openai::{
1160        Client,
1161        config::OpenAIConfig,
1162        types::{
1163            ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
1164            ChatCompletionRequestMessageContentPartImageArgs,
1165            ChatCompletionRequestMessageContentPartTextArgs,
1166            ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
1167            CreateChatCompletionRequestArgs, ImageUrlArgs,
1168        },
1169    };
1170    use kovi::bot::message::Message;
1171    use kovi_plugin_expand_napcat::NapCatApi;
1172    use regex::Regex;
1173    use std::{fs::File, io::Write, sync::Arc};
1174
1175    pub(crate) fn reply_text(event: &Arc<kovi::MsgEvent>, text: impl Into<String>) {
1176        event.reply(
1177            Message::new()
1178                .add_reply(event.message_id)
1179                .add_text(text.into()),
1180        );
1181    }
1182
1183    async fn reply(event: &Arc<kovi::MsgEvent>, text: &str, text_mode: bool, header: &str) {
1184        let msg = Message::new().add_reply(event.message_id);
1185
1186        if text_mode {
1187            event.reply(msg.add_text(text));
1188            return;
1189        }
1190        match render_md(text, header).await {
1191            Ok(b64) => event.reply(msg.add_image(&format!("base64://{}", b64))),
1192            Err(_) => {
1193                let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
1194                let clean_text = re.replace_all(text, "[图片渲染失败]").to_string();
1195                event.reply(msg.add_text(&clean_text));
1196            }
1197        }
1198    }
1199
1200    fn extract_image_urls(content: &str) -> Vec<String> {
1201        let re = Regex::new(
1202                    r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)|(?:https?://[^\s]+\.(?:png|jpg|jpeg|gif|webp|bmp))",
1203                )
1204                .unwrap();
1205
1206        let mut urls: Vec<String> = re
1207            .captures_iter(content)
1208            .filter_map(|cap| cap.get(1).or(cap.get(0)).map(|m| m.as_str().to_string()))
1209            .collect();
1210
1211        let mut seen = std::collections::HashSet::new();
1212        urls.retain(|url| seen.insert(url.clone()));
1213
1214        urls
1215    }
1216
1217    fn extract_video_urls(content: &str) -> Vec<String> {
1218        // 匹配 [download video](url)
1219        let re = Regex::new(r"\[download video\]\((https?://[^\s\)]+)\)").unwrap();
1220        re.captures_iter(content)
1221            .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
1222            .collect()
1223    }
1224
1225    #[allow(clippy::too_many_arguments)]
1226    async fn chat(
1227        name: &str,
1228        prompt: &str,
1229        imgs: Vec<String>,
1230        regen: bool,
1231        cmd: &Command,
1232        event: &Arc<kovi::MsgEvent>,
1233        mgr: &Arc<Manager>,
1234        bot: &Arc<kovi::RuntimeBot>,
1235    ) {
1236        struct ChatContext<'a> {
1237            name: &'a str,
1238            prompt: &'a str,
1239            imgs: Vec<String>,
1240            regen: bool,
1241            cmd: &'a Command,
1242            event: &'a Arc<kovi::MsgEvent>,
1243            mgr: &'a Arc<Manager>,
1244            bot: &'a Arc<kovi::RuntimeBot>,
1245        }
1246
1247        async fn inner(ctx: ChatContext<'_>) {
1248            let is_priv_ctx = ctx.cmd.private_reply;
1249            let uid = ctx.event.user_id.to_string();
1250            let temp_mode = ctx.cmd.temp_mode;
1251
1252            // 如果是临时模式,跳过"正在生成"检查,不阻塞
1253            if !temp_mode {
1254                let generating = ctx.mgr.generating.read().await;
1255                if generating.is_generating(ctx.name, is_priv_ctx, &uid) {
1256                    reply_text(ctx.event, "⏳ 正在生成中,请等待或使用 智能体! 停止");
1257                    return;
1258                }
1259            }
1260
1261            let (agent, api) = {
1262                let c = ctx.mgr.config.read().await;
1263                let a = c.agents.iter().find(|a| a.name == ctx.name).cloned();
1264                (a, (c.api_base.clone(), c.api_key.clone()))
1265            };
1266
1267            let agent = match agent {
1268                Some(a) => a,
1269                None => {
1270                    reply_text(ctx.event, format!("❌ 智能体 {} 不存在", ctx.name));
1271                    return;
1272                }
1273            };
1274
1275            if api.0.is_empty() || api.1.is_empty() {
1276                reply_text(ctx.event, "❌ API 未配置");
1277                return;
1278            }
1279
1280            match ctx
1281                .bot
1282                .set_msg_emoji_like(ctx.event.message_id.into(), "124")
1283                .await
1284            {
1285                Ok(_) => {}
1286                Err(e) => {
1287                    kovi::log::error!("点赞失败: {:?}", e);
1288                }
1289            }
1290
1291            // 临时模式下不加载历史,创建一个空历史用于本次构建消息
1292            let mut hist = if temp_mode {
1293                Vec::new()
1294            } else {
1295                agent.history(is_priv_ctx, &uid).to_vec()
1296            };
1297
1298            if ctx.regen {
1299                if hist.last().map(|m| m.role == "assistant").unwrap_or(false) {
1300                    hist.pop();
1301                }
1302                if !ctx.prompt.is_empty() {
1303                    if hist.last().map(|m| m.role == "user").unwrap_or(false) {
1304                        hist.pop();
1305                    }
1306                    hist.push(ChatMessage::new("user", ctx.prompt, ctx.imgs.clone()));
1307                }
1308            } else {
1309                if ctx.prompt.is_empty() && ctx.imgs.is_empty() {
1310                    reply_text(ctx.event, "💬 请输入内容");
1311                    return;
1312                }
1313                hist.push(ChatMessage::new("user", ctx.prompt, ctx.imgs.clone()));
1314            }
1315
1316            // 临时模式不保存历史,也不更新 generation_id
1317            let gen_id = if temp_mode {
1318                0 // 临时 ID
1319            } else {
1320                let mut c = ctx.mgr.config.write().await;
1321                if let Some(a) = c.agents.iter_mut().find(|a| a.name == ctx.name) {
1322                    *a.history_mut(is_priv_ctx, &uid) = hist.clone();
1323                    a.generation_id += 1;
1324                    let id = a.generation_id;
1325                    ctx.mgr.save(&c);
1326                    id
1327                } else {
1328                    return;
1329                }
1330            };
1331
1332            // 临时模式不设置生成锁,避免阻塞主对话
1333            if !temp_mode {
1334                let mut generating = ctx.mgr.generating.write().await;
1335                generating.set_generating(ctx.name, is_priv_ctx, &uid, true);
1336            }
1337
1338            let client =
1339                Client::with_config(OpenAIConfig::new().with_api_base(api.0).with_api_key(api.1));
1340
1341            let mut msgs: Vec<ChatCompletionRequestMessage> = vec![];
1342
1343            if !agent.system_prompt.is_empty() {
1344                msgs.push(
1345                    ChatCompletionRequestSystemMessageArgs::default()
1346                        .content(agent.system_prompt.clone())
1347                        .build()
1348                        .unwrap()
1349                        .into(),
1350                );
1351            }
1352            let re = Regex::new(r"!\[.*?\]\((data:image/[^\s\)]+)\)").unwrap();
1353            for m in &hist {
1354                if m.role == "user" {
1355                    let mut parts = Vec::new();
1356                    if !m.content.is_empty() {
1357                        parts.push(
1358                            ChatCompletionRequestMessageContentPartTextArgs::default()
1359                                .text(m.content.clone())
1360                                .build()
1361                                .unwrap()
1362                                .into(),
1363                        );
1364                    }
1365                    for url in &m.images {
1366                        parts.push(
1367                            ChatCompletionRequestMessageContentPartImageArgs::default()
1368                                .image_url(ImageUrlArgs::default().url(url).build().unwrap())
1369                                .build()
1370                                .unwrap()
1371                                .into(),
1372                        );
1373                    }
1374                    if parts.is_empty() {
1375                        continue;
1376                    }
1377                    msgs.push(
1378                        ChatCompletionRequestUserMessageArgs::default()
1379                            .content(parts)
1380                            .build()
1381                            .unwrap()
1382                            .into(),
1383                    );
1384                } else if m.role == "assistant" {
1385                    let clean_content = re.replace_all(&m.content, "[Image Created]").to_string();
1386
1387                    msgs.push(
1388                        ChatCompletionRequestAssistantMessageArgs::default()
1389                            .content(clean_content)
1390                            .build()
1391                            .unwrap()
1392                            .into(),
1393                    );
1394
1395                    let gen_imgs = extract_image_urls(&m.content);
1396                    if !gen_imgs.is_empty() {
1397                        let mut img_parts = Vec::new();
1398                        for url in gen_imgs {
1399                            img_parts.push(
1400                                ChatCompletionRequestMessageContentPartImageArgs::default()
1401                                    .image_url(ImageUrlArgs::default().url(url).build().unwrap())
1402                                    .build()
1403                                    .unwrap()
1404                                    .into(),
1405                            );
1406                        }
1407                        msgs.push(
1408                            ChatCompletionRequestUserMessageArgs::default()
1409                                .content(img_parts)
1410                                .build()
1411                                .unwrap()
1412                                .into(),
1413                        );
1414                    }
1415                }
1416            }
1417
1418            let req = match CreateChatCompletionRequestArgs::default()
1419                .model(&agent.model)
1420                .messages(msgs)
1421                .build()
1422            {
1423                Ok(r) => r,
1424                Err(e) => {
1425                    if !temp_mode {
1426                        let mut generating = ctx.mgr.generating.write().await;
1427                        generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1428                    }
1429                    reply_text(ctx.event, format!("❌ 请求构建失败: {}", e));
1430                    return;
1431                }
1432            };
1433
1434            match kovi::tokio::time::timeout(
1435                std::time::Duration::from_secs(300),
1436                client.chat().create(req),
1437            )
1438            .await
1439            {
1440                // 超时
1441                Err(_) => {
1442                    if !temp_mode {
1443                        let mut generating = ctx.mgr.generating.write().await;
1444                        generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1445                    }
1446                    reply_text(
1447                        ctx.event,
1448                        "⏳ 请求超时:模型响应时间超过 5 分钟,已强制停止。",
1449                    );
1450                }
1451                // 完成
1452                Ok(result) => match result {
1453                    Ok(res) => {
1454                        if !temp_mode {
1455                            let mut generating = ctx.mgr.generating.write().await;
1456                            generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1457                        }
1458
1459                        // 非临时模式下检查 ID 是否变更(是否被手动停止)
1460                        if !temp_mode {
1461                            let c = ctx.mgr.config.read().await;
1462                            if let Some(a) = c.agents.iter().find(|a| a.name == ctx.name)
1463                                && a.generation_id != gen_id
1464                            {
1465                                return;
1466                            }
1467                        }
1468
1469                        if let Some(choice) = res.choices.first()
1470                            && let Some(content) = &choice.message.content
1471                        {
1472                            let msg_index = if temp_mode {
1473                                0
1474                            } else {
1475                                let c = ctx.mgr.config.read().await;
1476                                if let Some(a) = c.agents.iter().find(|a| a.name == ctx.name) {
1477                                    a.history(is_priv_ctx, &uid).len() + 1
1478                                } else {
1479                                    0
1480                                }
1481                            };
1482
1483                            // 临时模式不保存回复到历史
1484                            if !temp_mode {
1485                                let mut c = ctx.mgr.config.write().await;
1486                                if let Some(a) = c.agents.iter_mut().find(|a| a.name == ctx.name) {
1487                                    a.history_mut(is_priv_ctx, &uid).push(ChatMessage::new(
1488                                        "assistant",
1489                                        content,
1490                                        vec![],
1491                                    ));
1492                                }
1493                                ctx.mgr.save(&c);
1494                            }
1495
1496                            let image_urls = extract_image_urls(content);
1497
1498                            let header = if temp_mode {
1499                                format!("{} (临时会话)", agent.name)
1500                            } else {
1501                                format!(
1502                                    "{} #{}回复{}",
1503                                    agent.name,
1504                                    msg_index,
1505                                    if ctx.cmd.private_reply {
1506                                        " (私有)"
1507                                    } else {
1508                                        ""
1509                                    }
1510                                )
1511                            };
1512
1513                            let display_content = if !image_urls.is_empty() && !ctx.cmd.text_mode {
1514                                let urls_text = image_urls
1515                                    .iter()
1516                                    .map(|u| {
1517                                        if u.starts_with("data:") {
1518                                            "- [Base64 Image]".to_string()
1519                                        } else {
1520                                            format!("- {}", u)
1521                                        }
1522                                    })
1523                                    .collect::<Vec<_>>()
1524                                    .join("\n");
1525                                format!("{}\n\n---\n**图片链接:**\n{}", content, urls_text)
1526                            } else {
1527                                content.clone()
1528                            };
1529
1530                            let reply_text_content = if ctx.cmd.text_mode && !image_urls.is_empty()
1531                            {
1532                                // 使用与 extract_image_urls 相同的逻辑替换
1533                                let re =
1534                                    Regex::new(r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)")
1535                                        .unwrap();
1536                                re.replace_all(content, |caps: &regex::Captures| {
1537                                    let url = &caps[1];
1538                                    if url.starts_with("data:") {
1539                                        "[图片]".to_string()
1540                                    } else {
1541                                        url.to_string()
1542                                    }
1543                                })
1544                                .to_string()
1545                            } else {
1546                                display_content.clone()
1547                            };
1548
1549                            reply(ctx.event, &reply_text_content, ctx.cmd.text_mode, &header).await;
1550
1551                            for url in &image_urls {
1552                                if url.starts_with("data:") {
1553                                    if let Some(base64_data) = url.split(',').nth(1) {
1554                                        ctx.event.reply(
1555                                            Message::new()
1556                                                .add_image(&format!("base64://{}", base64_data)),
1557                                        );
1558                                    }
1559                                } else {
1560                                    ctx.event.reply(Message::new().add_image(url));
1561                                }
1562                            }
1563
1564                            let video_urls = extract_video_urls(content);
1565                            for url in video_urls {
1566                                // 使用 OneBot 标准 video 段发送,data 放 file 字段,框架会自动处理下载/转发
1567                                let mut vec = Vec::new();
1568                                let segment = kovi::bot::message::Segment::new(
1569                                    "video",
1570                                    kovi::serde_json::json!({
1571                                        "file": url
1572                                    }),
1573                                );
1574                                vec.push(segment);
1575                                let msg = kovi::bot::message::Message::from(vec);
1576                                ctx.event.reply(msg);
1577                            }
1578                        }
1579                    }
1580                    Err(e) => {
1581                        {
1582                            let mut generating = ctx.mgr.generating.write().await;
1583                            generating.set_generating(ctx.name, is_priv_ctx, &uid, false);
1584                        }
1585                        reply_text(ctx.event, format!("❌ API错误: {}", e));
1586                    }
1587                },
1588            }
1589        }
1590
1591        inner(ChatContext {
1592            name,
1593            prompt,
1594            imgs,
1595            regen,
1596            cmd,
1597            event,
1598            mgr,
1599            bot,
1600        })
1601        .await;
1602    }
1603
1604    pub async fn execute(
1605        cmd: Command,
1606        prompt: String,
1607        imgs: Vec<String>,
1608        event: &Arc<kovi::MsgEvent>,
1609        mgr: &Arc<Manager>,
1610        bot: &Arc<kovi::RuntimeBot>,
1611    ) {
1612        let name = &cmd.agent;
1613        let uid = event.user_id.to_string();
1614
1615        match cmd.action {
1616            Action::UpdateApi(url, key) => {
1617                let mut c = mgr.config.write().await;
1618                c.api_base = url.clone();
1619                c.api_key = key;
1620                mgr.save(&c);
1621                drop(c);
1622
1623                reply_text(event, format!("✅ API 已配置: {}", url));
1624
1625                match mgr.fetch_models().await {
1626                    Ok(models) => reply_text(
1627                        event,
1628                        format!("📋 验证成功,已获取 {} 个模型", models.len()),
1629                    ),
1630                    Err(e) => reply_text(event, format!("⚠️ 获取模型失败: {}", e)),
1631                }
1632            }
1633
1634            Action::Chat => {
1635                chat(name, &prompt, imgs, false, &cmd, event, mgr, bot).await;
1636            }
1637
1638            Action::Regenerate => {
1639                chat(name, &cmd.args, imgs, true, &cmd, event, mgr, bot).await;
1640            }
1641
1642            Action::Stop => {
1643                let is_priv_ctx = cmd.private_reply;
1644                {
1645                    let mut generating = mgr.generating.write().await;
1646                    generating.set_generating(name, is_priv_ctx, &uid, false);
1647                }
1648                let mut c = mgr.config.write().await;
1649                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1650                    a.generation_id += 1;
1651                    mgr.save(&c);
1652                    reply_text(event, "🛑 已停止");
1653                } else {
1654                    reply_text(event, format!("❌ 智能体 {} 不存在", name));
1655                }
1656            }
1657
1658            Action::Copy => {
1659                if cmd.args.is_empty() {
1660                    reply_text(event, "❌ 请指定新名称: 智能体~#新名称");
1661                    return;
1662                }
1663
1664                if cmd.args.chars().count() > 7
1665                    || cmd.args.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
1666                {
1667                    reply_text(event, "❌ 名称限制:最多7字且不能包含指令符号");
1668                    return;
1669                }
1670
1671                let mut c = mgr.config.write().await;
1672                if c.agents.iter().any(|a| a.name == cmd.args) {
1673                    reply_text(event, format!("❌ {} 已存在", cmd.args));
1674                    return;
1675                }
1676                if let Some(src) = c.agents.iter().find(|a| a.name == *name).cloned() {
1677                    let mut new_agent = Agent::new(
1678                        &cmd.args,
1679                        &src.model,
1680                        &src.system_prompt,
1681                        &format!("复制自 {}", name),
1682                    );
1683                    new_agent.description = src.description.clone();
1684                    c.agents.push(new_agent);
1685                    mgr.save(&c);
1686                    reply_text(event, format!("📑 已复制 {} → {}", name, cmd.args));
1687                } else {
1688                    reply_text(event, format!("❌ {} 不存在", name));
1689                }
1690            }
1691
1692            Action::Rename => {
1693                if cmd.args.is_empty() {
1694                    reply_text(event, "❌ 请指定新名称: 智能体~=新名称");
1695                    return;
1696                }
1697
1698                if cmd.args.chars().count() > 7
1699                    || cmd.args.chars().any(|c| "&\"#~/ -_'!@$%:*".contains(c))
1700                {
1701                    reply_text(event, "❌ 名称限制:最多7字且不能包含指令符号");
1702                    return;
1703                }
1704
1705                let mut c = mgr.config.write().await;
1706                if c.agents.iter().any(|a| a.name == cmd.args) {
1707                    reply_text(event, format!("❌ 目标名称 {} 已存在", cmd.args));
1708                    return;
1709                }
1710
1711                // 先找要重命名的智能体的索引
1712                let idx_opt = c.agents.iter().position(|a| a.name == *name);
1713                if let Some(idx) = idx_opt {
1714                    c.agents[idx].name = cmd.args.clone();
1715                    mgr.save(&c);
1716                    reply_text(event, format!("🏷️ 已重命名 {} → {}", name, cmd.args));
1717                } else {
1718                    reply_text(event, format!("❌ {} 不存在", name));
1719                }
1720            }
1721
1722            Action::SetDesc => {
1723                if cmd.args.is_empty() {
1724                    reply_text(event, "❌ 请提供描述: 智能体:描述内容");
1725                    return;
1726                }
1727                let mut c = mgr.config.write().await;
1728                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1729                    a.description = cmd.args.clone();
1730                    mgr.save(&c);
1731                    reply_text(event, format!("📝 {} 描述已更新", name));
1732                } else {
1733                    reply_text(event, format!("❌ {} 不存在", name));
1734                }
1735            }
1736
1737            Action::SetModel => {
1738                if cmd.args.is_empty() {
1739                    reply_text(event, "❌ 请指定模型: 智能体%模型名");
1740                    return;
1741                }
1742                let mut c = mgr.config.write().await;
1743                let models = c.models.clone();
1744                if let Some(model) = mgr.resolve_model(&cmd.args, &models) {
1745                    if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1746                        let old = a.model.clone();
1747                        a.model = model.clone();
1748                        mgr.save(&c);
1749                        reply_text(event, format!("🔄 {} 模型: {} → {}", name, old, model));
1750                    } else {
1751                        reply_text(event, format!("❌ {} 不存在", name));
1752                    }
1753                } else {
1754                    reply_text(event, "❌ 无效模型");
1755                }
1756            }
1757
1758            Action::SetPrompt => {
1759                let mut c = mgr.config.write().await;
1760                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
1761                    a.system_prompt = cmd.args.clone();
1762                    mgr.save(&c);
1763                    if cmd.args.is_empty() {
1764                        reply_text(event, format!("📝 {} 提示词已清空", name));
1765                    } else {
1766                        reply_text(event, format!("📝 {} 提示词已更新", name));
1767                    }
1768                } else {
1769                    reply_text(event, format!("❌ {} 不存在", name));
1770                }
1771            }
1772
1773            Action::ViewPrompt => {
1774                let c = mgr.config.read().await;
1775                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
1776                    if cmd.text_mode {
1777                        reply_text(event, &a.system_prompt);
1778                        return;
1779                    }
1780                    let prompt_display = if a.system_prompt.is_empty() {
1781                        "(空)".to_string()
1782                    } else {
1783                        escape_markdown_special(&a.system_prompt)
1784                    };
1785                    let content = format!(
1786                        "**模型**: `{}`\n\n**提示词**:\n```\n{}\n```",
1787                        a.model, prompt_display
1788                    );
1789                    reply(
1790                        event,
1791                        &content,
1792                        cmd.text_mode,
1793                        &format!("{} 系统提示词", a.name),
1794                    )
1795                    .await;
1796                } else {
1797                    reply_text(event, format!("❌ {} 不存在", name));
1798                }
1799            }
1800
1801            Action::List => {
1802                let c = mgr.config.read().await;
1803                if c.agents.is_empty() {
1804                    reply_text(event, "📋 暂无智能体,使用 ##名称 模型 提示词 创建");
1805                    return;
1806                }
1807
1808                // 分组逻辑:使用 BTreeMap 自动按模型名称排序
1809                use std::collections::BTreeMap;
1810                let mut groups: BTreeMap<String, Vec<(usize, &Agent)>> = BTreeMap::new();
1811
1812                // 遍历并分组 (保留原始索引 i+1 以便用户操作)
1813                for (i, a) in c.agents.iter().enumerate() {
1814                    groups.entry(a.model.clone()).or_default().push((i + 1, a));
1815                }
1816
1817                // 生成 HTML
1818                let mut html_parts = Vec::new();
1819
1820                // 遍历每一个模型分组
1821                for (model, mut agents) in groups {
1822                    // 组内按智能体名称排序
1823                    agents.sort_by(|a, b| a.1.name.to_lowercase().cmp(&b.1.name.to_lowercase()));
1824
1825                    // 组头
1826                    html_parts.push(format!(
1827                                              r#"<div class="model-group"><div class="model-header"><span>📦 {}</span><span class="model-count">{}</span></div><div class="agent-grid">"#,
1828                                              model, agents.len()
1829                                          ));
1830
1831                    // 组内网格
1832                    for (real_idx, a) in agents {
1833                        // 逻辑:优先显示描述;如果没有描述,则截取系统提示词的前 20 个字作为预览;
1834                        let desc_display = if !a.description.is_empty() {
1835                            truncate_str(&a.description, 20)
1836                        } else if !a.system_prompt.is_empty() {
1837                            truncate_str(&a.system_prompt, 20)
1838                        } else {
1839                            "无描述".to_string()
1840                        };
1841
1842                        html_parts.push(format!(
1843                                            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>"#,
1844                                            real_idx, a.name, desc_display
1845                                        ));
1846                    }
1847                    html_parts.push("</div></div>".to_string());
1848                }
1849
1850                let list = html_parts.join("\n");
1851
1852                reply(
1853                    event,
1854                    &list,
1855                    cmd.text_mode,
1856                    &format!("📋 智能体列表 (共{}个)", c.agents.len()),
1857                )
1858                .await;
1859            }
1860
1861            Action::Delete => {
1862                let mut c = mgr.config.write().await;
1863                if let Some(idx) = c.agents.iter().position(|a| a.name == *name) {
1864                    c.agents.remove(idx);
1865                    mgr.save(&c);
1866                    reply_text(event, format!("🗑️ 已删除 {}", name));
1867                } else {
1868                    reply_text(event, format!("❌ {} 不存在", name));
1869                }
1870            }
1871
1872            Action::ListModels => {
1873                let c = mgr.config.read().await;
1874
1875                // 1. 如果配置为空,尝试抓取
1876                if c.models.is_empty() {
1877                    drop(c);
1878                    reply_text(event, "⏳ 正在获取模型列表...");
1879                    if let Err(e) = mgr.fetch_models().await {
1880                        reply_text(event, format!("❌ 获取失败: {}", e));
1881                        return;
1882                    }
1883                }
1884
1885                // 重新读取
1886                let c = mgr.config.read().await;
1887                let models = &c.models;
1888
1889                if models.is_empty() {
1890                    reply_text(event, "📭 未找到可用模型 (请检查过滤关键字)");
1891                    return;
1892                }
1893
1894                // 2. 统计使用热度 (哪个模型被多少个智能体使用了)
1895                use std::collections::HashMap;
1896                let mut usage_count = HashMap::new();
1897                for agent in &c.agents {
1898                    *usage_count.entry(agent.model.clone()).or_insert(0) += 1;
1899                }
1900
1901                // 3. 动态分组逻辑
1902                // 直接利用 utils::MODEL_KEYWORDS 进行分组
1903                let mut groups: HashMap<String, Vec<(usize, String)>> = HashMap::new();
1904                let mut other_models = Vec::new();
1905
1906                for (i, m) in models.iter().enumerate() {
1907                    let idx = i + 1;
1908                    let lower = m.to_lowercase();
1909                    let mut matched = false;
1910
1911                    for &kw in crate::utils::MODEL_KEYWORDS {
1912                        if lower.contains(kw) {
1913                            // 将关键字首字母大写作为组名 (e.g. "gpt-5" -> "Gpt-5 Series")
1914                            let group_name = format!(
1915                                "{} Series",
1916                                kw.chars().next().unwrap().to_uppercase().to_string() + &kw[1..]
1917                            );
1918                            groups.entry(group_name).or_default().push((idx, m.clone()));
1919                            matched = true;
1920                            break;
1921                        }
1922                    }
1923
1924                    if !matched {
1925                        other_models.push((idx, m.clone()));
1926                    }
1927                }
1928
1929                // 4. 生成 HTML
1930                let mut html = String::new();
1931
1932                // 辅助渲染函数
1933                let render_group = |title: &str, items: &Vec<(usize, String)>| -> String {
1934                    let mut s = format!(
1935                        r#"<div class="mod-group"><div class="mod-title">{}</div><div class="chip-box">"#,
1936                        title
1937                    );
1938                    for (idx, name) in items {
1939                        let badge = if let Some(cnt) = usage_count.get(name) {
1940                            format!(r#"<span class="chip-bad">{}用</span>"#, cnt)
1941                        } else {
1942                            String::new()
1943                        };
1944                        s.push_str(&format!(
1945                                        r#"<div class="chip"><span class="chip-idx">{}</span><span class="chip-name">{}</span>{}</div>"#,
1946                                        idx, name, badge
1947                                    ));
1948                    }
1949                    s.push_str("</div></div>");
1950                    s
1951                };
1952
1953                // 按 MODEL_KEYWORDS 的定义顺序渲染 (保证顺序可控)
1954                for &kw in crate::utils::MODEL_KEYWORDS {
1955                    let group_name = format!(
1956                        "{} Series",
1957                        kw.chars().next().unwrap().to_uppercase().to_string() + &kw[1..]
1958                    );
1959                    if let Some(items) = groups.get(&group_name) {
1960                        html.push_str(&render_group(&group_name, items));
1961                    }
1962                }
1963
1964                // 渲染未分类的模型 (如果有漏网之鱼)
1965                if !other_models.is_empty() {
1966                    html.push_str(&render_group("Other Models", &other_models));
1967                }
1968
1969                // 5. 发送
1970                reply(
1971                    event,
1972                    &html,
1973                    cmd.text_mode,
1974                    &format!("🧩 模型列表 (共{}个)", models.len()),
1975                )
1976                .await;
1977            }
1978
1979            Action::ViewAll(scope) => {
1980                let c = mgr.config.read().await;
1981                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
1982                    let priv_scope = matches!(scope, Scope::Private);
1983                    let hist = a.history(priv_scope, &uid);
1984                    if hist.is_empty() {
1985                        let s = if priv_scope { "私有" } else { "公有" };
1986                        reply_text(event, format!("📭 {} {}历史为空", name, s));
1987                        return;
1988                    }
1989                    let content = format_history(hist, 0, cmd.text_mode);
1990                    let header = format!(
1991                        "{} {}历史 ({} 条)",
1992                        name,
1993                        if priv_scope { "私有" } else { "公有" },
1994                        hist.len()
1995                    );
1996                    reply(event, &content, cmd.text_mode, &header).await;
1997                } else {
1998                    reply_text(event, format!("❌ {} 不存在", name));
1999                }
2000            }
2001
2002            Action::ViewAt(scope) => {
2003                if cmd.indices.is_empty() {
2004                    reply_text(event, "❌ 请指定索引: 智能体/索引");
2005                    return;
2006                }
2007                let c = mgr.config.read().await;
2008                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
2009                    let priv_scope = matches!(scope, Scope::Private);
2010                    let hist = a.history(priv_scope, &uid);
2011                    let mut results = Vec::new();
2012                    let mut extra_images = Vec::new();
2013
2014                    let re =
2015                        Regex::new(r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)").unwrap();
2016
2017                    for i in &cmd.indices {
2018                        if *i > 0 && *i <= hist.len() {
2019                            let m = &hist[i - 1];
2020                            let emoji = match m.role.as_str() {
2021                                "user" => "👤",
2022                                "assistant" => "🤖",
2023                                _ => "❓",
2024                            };
2025
2026                            let mut content = m.content.clone();
2027                            let mut msg_imgs = extract_image_urls(&content);
2028                            msg_imgs.extend(m.images.clone());
2029
2030                            if cmd.text_mode {
2031                                content = re
2032                                    .replace_all(&content, |caps: &regex::Captures| {
2033                                        let url = &caps[1];
2034                                        if url.starts_with("data:") {
2035                                            "[图片]".to_string()
2036                                        } else {
2037                                            url.to_string()
2038                                        }
2039                                    })
2040                                    .to_string();
2041                            }
2042
2043                            if !m.images.is_empty() {
2044                                if !content.is_empty() {
2045                                    content.push_str("\n\n");
2046                                }
2047                                for url in &m.images {
2048                                    if cmd.text_mode {
2049                                        if url.starts_with("data:") {
2050                                            content.push_str("\n- [Base64 Image]");
2051                                        } else {
2052                                            content.push_str(&format!("\n- {}", url));
2053                                        }
2054                                    } else {
2055                                        content.push_str(&format!("\n![image]({})", url));
2056                                    }
2057                                }
2058                            }
2059
2060                            extra_images.extend(msg_imgs);
2061
2062                            results.push(format!("**#{} {}**\n{}", i, emoji, content));
2063                        }
2064                    }
2065
2066                    if results.is_empty() {
2067                        reply_text(event, "❌ 索引无效");
2068                    } else {
2069                        reply(
2070                            event,
2071                            &results.join("\n\n---\n\n"),
2072                            cmd.text_mode,
2073                            &format!("{} 历史记录", name),
2074                        )
2075                        .await;
2076
2077                        for url in extra_images {
2078                            if url.starts_with("data:") {
2079                                if let Some(base64_data) = url.split(',').nth(1) {
2080                                    event.reply(
2081                                        Message::new()
2082                                            .add_image(&format!("base64://{}", base64_data)),
2083                                    );
2084                                }
2085                            } else {
2086                                event.reply(Message::new().add_image(&url));
2087                            }
2088                        }
2089                    }
2090                } else {
2091                    reply_text(event, format!("❌ {} 不存在", name));
2092                }
2093            }
2094
2095            Action::Export(scope) => {
2096                let c = mgr.config.read().await;
2097                if let Some(a) = c.agents.iter().find(|a| a.name == *name) {
2098                    let priv_scope = matches!(scope, Scope::Private);
2099                    let hist = a.history(priv_scope, &uid);
2100                    if hist.is_empty() {
2101                        reply_text(event, "📭 历史为空");
2102                        return;
2103                    }
2104
2105                    let scope_str = if priv_scope { "私有" } else { "公有" };
2106                    let content = format_export_txt(name, &a.model, scope_str, hist);
2107
2108                    let scope_file = if priv_scope { "private" } else { "public" };
2109                    let fname = format!(
2110                        "{}_{}_{}_{}.txt",
2111                        name,
2112                        scope_file,
2113                        uid,
2114                        chrono::Local::now().format("%Y%m%d%H%M%S")
2115                    );
2116                    let path = bot.get_data_path().join(&fname);
2117                    match File::create(&path) {
2118                        Ok(mut f) => {
2119                            if f.write_all(content.as_bytes()).is_ok() {
2120                                let path_str = path.to_string_lossy().to_string();
2121                                let result = if let Some(gid) = event.group_id {
2122                                    bot.upload_group_file(gid, &path_str, &fname, None).await
2123                                } else {
2124                                    bot.upload_private_file(event.user_id, &path_str, &fname)
2125                                        .await
2126                                };
2127                                match result {
2128                                    Ok(_) => reply_text(event, format!("📤 已导出: {}", fname)),
2129                                    Err(e) => reply_text(event, format!("❌ 上传失败: {}", e)),
2130                                }
2131                            } else {
2132                                reply_text(event, "❌ 写入失败");
2133                            }
2134                        }
2135                        Err(e) => reply_text(event, format!("❌ 创建文件失败: {}", e)),
2136                    }
2137                } else {
2138                    reply_text(event, format!("❌ {} 不存在", name));
2139                }
2140            }
2141
2142            Action::EditAt(scope) => {
2143                if cmd.indices.is_empty() {
2144                    reply_text(event, "❌ 请指定索引: 智能体'索引 新内容");
2145                    return;
2146                }
2147                if cmd.args.is_empty() {
2148                    reply_text(event, "❌ 请提供新内容");
2149                    return;
2150                }
2151                let idx = cmd.indices[0];
2152                let mut c = mgr.config.write().await;
2153                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2154                    let priv_scope = matches!(scope, Scope::Private);
2155                    if a.edit_at(priv_scope, &uid, idx, &cmd.args) {
2156                        mgr.save(&c);
2157                        reply_text(event, format!("✏️ 已编辑第 {} 条", idx));
2158                    } else {
2159                        reply_text(event, format!("❌ 索引 {} 无效", idx));
2160                    }
2161                } else {
2162                    reply_text(event, format!("❌ {} 不存在", name));
2163                }
2164            }
2165
2166            Action::DeleteAt(scope) => {
2167                if cmd.indices.is_empty() {
2168                    reply_text(event, "❌ 请指定索引: 智能体-索引 (支持 1,3,5 或 1-5)");
2169                    return;
2170                }
2171                let mut c = mgr.config.write().await;
2172                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2173                    let priv_scope = matches!(scope, Scope::Private);
2174                    let deleted = a.delete_at(priv_scope, &uid, &cmd.indices);
2175                    if deleted.is_empty() {
2176                        reply_text(event, "❌ 索引无效");
2177                    } else {
2178                        mgr.save(&c);
2179                        let s = deleted
2180                            .iter()
2181                            .map(|i| i.to_string())
2182                            .collect::<Vec<_>>()
2183                            .join(", ");
2184                        reply_text(
2185                            event,
2186                            format!("🗑️ 已删除第 {} 条 (共{}条)", s, deleted.len()),
2187                        );
2188                    }
2189                } else {
2190                    reply_text(event, format!("❌ {} 不存在", name));
2191                }
2192            }
2193
2194            Action::ClearHistory(scope) => {
2195                let is_priv_ctx = cmd.private_reply;
2196                {
2197                    let mut generating = mgr.generating.write().await;
2198                    generating.set_generating(name, is_priv_ctx, &uid, false);
2199                }
2200                let mut c = mgr.config.write().await;
2201                if let Some(a) = c.agents.iter_mut().find(|a| a.name == *name) {
2202                    let priv_scope = matches!(scope, Scope::Private);
2203                    let s = if priv_scope { "私有" } else { "公有" };
2204                    a.clear_history(priv_scope, &uid);
2205                    a.generation_id += 1;
2206                    mgr.save(&c);
2207                    reply_text(event, format!("🧹 {} {}历史已清空", name, s));
2208                } else {
2209                    reply_text(event, format!("❌ {} 不存在", name));
2210                }
2211            }
2212
2213            Action::ClearAllPublic => {
2214                {
2215                    let mut generating = mgr.generating.write().await;
2216                    generating.public.clear();
2217                }
2218                let mut c = mgr.config.write().await;
2219                let cnt = c.agents.len();
2220                for a in c.agents.iter_mut() {
2221                    a.public_history.clear();
2222                    a.generation_id += 1;
2223                }
2224                mgr.save(&c);
2225                reply_text(event, format!("🧹 已清空 {} 个智能体的公有历史", cnt));
2226            }
2227
2228            Action::ClearEverything => {
2229                {
2230                    let mut generating = mgr.generating.write().await;
2231                    generating.public.clear();
2232                    generating.private.clear();
2233                }
2234                let mut c = mgr.config.write().await;
2235                let cnt = c.agents.len();
2236                for a in c.agents.iter_mut() {
2237                    a.public_history.clear();
2238                    a.private_histories.clear();
2239                    a.generation_id += 1;
2240                }
2241                mgr.save(&c);
2242                reply_text(event, format!("⚠️ 已清空 {} 个智能体的所有历史", cnt));
2243            }
2244
2245            Action::Help => {
2246                let help = r#"## 模式前缀(可组合)
2247| 符号 | 含义 |
2248|:---:|------|
2249| `&` | 私有模式 (独立历史) |
2250| `"` | 文本模式 (不转图片) |
2251| `~` | 临时模式 (无历史/不阻塞) |
2252
2253## 智能体管理
2254| 指令 | 功能 | 示例 |
2255|------|------|------|
2256| `##名称 模型 提示词` | 创建/更新 | `##助手 gpt-4o 你是助手` |
2257| `##:模型` | 批量生成描述 | `##:gpt-4o` |
2258| `智能体~=新名` | 重命名 | `助手~=管家` |
2259| `智能体~#新名` | 复制 | `助手~#助手2` |
2260| `智能体:描述` | 设置描述 | `助手:通用助手` |
2261| `-#名称` | 删除 | `-#助手` |
2262| `/#` | 列表 | `/#` |
2263
2264## 配置修改
2265| 指令 | 功能 | 示例 |
2266|------|------|------|
2267| `智能体%模型` | 修改模型 | `助手%gpt-4` |
2268| `智能体$提示词` | 修改提示词 | `助手$你是...` |
2269| `智能体$` | 清空提示词 | `助手$` |
2270| `智能体/$` | 查看提示词 | `助手/$` |
2271| `/%` | 模型列表 | `/%` |
2272
2273## 对话控制
2274| 指令 | 功能 |
2275|------|------|
2276| `智能体 内容` | 正常对话 |
2277| `~智能体 内容` | 临时对话 (一次性) |
2278| `"智能体 内容` | 文本回复对话 |
2279| `&智能体 内容` | 私有历史对话 |
2280| `智能体~` | 重新生成上一条 |
2281| `智能体!` | 停止生成 |
2282
2283## 历史管理
2284| 指令 | 功能 |
2285|------|------|
2286| `智能体/*` | 查看所有 |
2287| `智能体/1` | 查看第1条 |
2288| `智能体/1-5` | 查看范围 |
2289| `智能体_*` | 导出(.txt) |
2290| `智能体'1 内容` | 编辑第1条 |
2291| `智能体-1` | 删除第1条 |
2292| `智能体-1,3` | 删除多条 |
2293| `智能体-*` | 清空历史 |
2294
2295> 所有符号支持半角/全角兼容 (如 ~, #, =)
2296> 加 `&` 前缀可操作私有历史: `&智能体/*`
2297
2298## 危险操作
2299| 指令 | 功能 |
2300|------|------|
2301| `-*` | 清空所有智能体公有历史 |
2302| `-*!` | 清空数据库所有历史 |
2303
2304## API 配置
2305更新指令: `oai API地址 API密钥`
2306"#;
2307                reply(event, help, cmd.text_mode, "🤖 OAI 符号指令帮助").await;
2308            }
2309
2310            Action::AutoFillDescriptions(model_ref) => {
2311                let (target_agents, api_config, use_model) = {
2312                    let c = mgr.config.read().await;
2313
2314                    // 1. 确定使用的模型
2315                    let models = c.models.clone();
2316                    let resolved_model = if model_ref.is_empty() {
2317                        c.default_model.clone()
2318                    } else {
2319                        mgr.resolve_model(&model_ref, &models).unwrap_or(model_ref)
2320                    };
2321
2322                    // 2. 筛选需要生成的智能体 (描述为空 或 仅仅是"新建智能体")
2323                    let targets: Vec<(String, String)> = c
2324                        .agents
2325                        .iter()
2326                        .filter(|a| a.description.is_empty() || a.description == "新建智能体")
2327                        .map(|a| (a.name.clone(), a.system_prompt.clone()))
2328                        .collect();
2329
2330                    (
2331                        targets,
2332                        (c.api_base.clone(), c.api_key.clone()),
2333                        resolved_model,
2334                    )
2335                };
2336
2337                if target_agents.is_empty() {
2338                    reply_text(event, "✅ 所有智能体均已有描述,无需处理。");
2339                    return;
2340                }
2341
2342                if api_config.0.is_empty() || api_config.1.is_empty() {
2343                    reply_text(event, "❌ API 未配置");
2344                    return;
2345                }
2346
2347                reply_text(
2348                    event,
2349                    format!(
2350                        "🤖 开始使用 [{}] 为 {} 个智能体生成描述,请稍候...",
2351                        use_model,
2352                        target_agents.len()
2353                    ),
2354                );
2355
2356                let client = Client::with_config(
2357                    OpenAIConfig::new()
2358                        .with_api_base(api_config.0)
2359                        .with_api_key(api_config.1),
2360                );
2361
2362                let mut success_count = 0;
2363
2364                for (name, prompt) in target_agents {
2365                    // 这里的 Prompt 专门用于生成简短描述
2366                    let gen_prompt = format!(
2367                        "请阅读以下角色的 System Prompt,为其生成一个极简短的中文功能描述(Role/Tag)。\n\
2368                                    要求:\n1. 必须控制在 10 个字以内\n2. 不要包含任何标点符号\n3. 直接输出描述内容,不要解释\n\n\
2369                                    System Prompt:\n{}",
2370                        prompt
2371                    );
2372
2373                    let req = CreateChatCompletionRequestArgs::default()
2374                        .model(&use_model)
2375                        .messages(vec![
2376                            ChatCompletionRequestUserMessageArgs::default()
2377                                .content(gen_prompt)
2378                                .build()
2379                                .unwrap()
2380                                .into(),
2381                        ])
2382                        .build();
2383
2384                    if let Ok(req) = req
2385                        && let Ok(res) = client.chat().create(req).await
2386                        && let Some(choice) = res.choices.first()
2387                        && let Some(content) = &choice.message.content
2388                    {
2389                        let new_desc = content.trim().replace(['"', '“', '”', '。', '.'], ""); // 简单清洗
2390
2391                        // 获取写锁更新数据
2392                        let mut c = mgr.config.write().await;
2393                        if let Some(a) = c.agents.iter_mut().find(|a| a.name == name) {
2394                            a.description = new_desc.clone();
2395                            mgr.save(&c);
2396                            success_count += 1;
2397                        }
2398                    }
2399
2400                    // 小停顿,避免并发过高 (100毫秒)
2401                    kovi::tokio::time::sleep(std::time::Duration::from_millis(100)).await;
2402                }
2403
2404                reply_text(
2405                    event,
2406                    format!("✅ 批量处理完成,已更新 {} 个智能体的描述。", success_count),
2407                );
2408            }
2409
2410            Action::Create => {}
2411        }
2412    }
2413
2414    pub async fn handle_create(
2415        name: &str,
2416        desc: &str,
2417        model: &str,
2418        prompt: &str,
2419        event: &Arc<kovi::MsgEvent>,
2420        mgr: &Arc<Manager>,
2421    ) {
2422        let mut c = mgr.config.write().await;
2423        let models = c.models.clone();
2424
2425        let model = mgr
2426            .resolve_model(model, &models)
2427            .unwrap_or_else(|| model.to_string());
2428
2429        let prompt = if prompt.is_empty() && !c.agents.iter().any(|a| a.name == name) {
2430            c.default_prompt.clone()
2431        } else {
2432            prompt.to_string()
2433        };
2434
2435        if let Some(a) = c.agents.iter_mut().find(|a| a.name == name) {
2436            if !model.is_empty() {
2437                a.model = model.clone();
2438            }
2439            a.system_prompt = prompt;
2440            if !desc.is_empty() {
2441                a.description = desc.to_string();
2442            }
2443            let updated_model = a.model.clone();
2444            mgr.save(&c);
2445            reply_text(
2446                event,
2447                format!("📝 已更新 {} (模型: {})", name, updated_model),
2448            );
2449        } else {
2450            let description = if desc.is_empty() {
2451                "新建智能体".to_string()
2452            } else {
2453                desc.to_string()
2454            };
2455            c.agents
2456                .push(Agent::new(name, &model, &prompt, &description));
2457            mgr.save(&c);
2458            reply_text(event, format!("🤖 已创建 {} (模型: {})", name, model));
2459        }
2460    }
2461}
2462
2463// --- 入口 ---
2464use cdp_html_shot::Browser;
2465use kovi::PluginBuilder;
2466use std::sync::Arc;
2467
2468#[kovi::plugin]
2469async fn main() {
2470    let bot = PluginBuilder::get_runtime_bot();
2471    let mgr = Arc::new(data::Manager::new(bot.get_data_path()));
2472
2473    let m = mgr.clone();
2474    kovi::tokio::spawn(async move {
2475        let _ = m.fetch_models().await;
2476    });
2477
2478    let mgr_clone = mgr.clone();
2479    PluginBuilder::on_msg(move |event| {
2480        let mgr = mgr_clone.clone();
2481        let bot = bot.clone();
2482        async move {
2483            let raw = match event.borrow_text() {
2484                Some(v) => v,
2485                None => return,
2486            };
2487
2488            if let Some(cmd) = parser::parse_global(raw) {
2489                logic::execute(cmd, String::new(), vec![], &event, &mgr, &bot).await;
2490                return;
2491            }
2492
2493            if let Some((name, desc, model, prompt)) = parser::parse_create(raw) {
2494                logic::handle_create(&name, &desc, &model, &prompt, &event, &mgr).await;
2495                return;
2496            }
2497
2498            let agents = mgr.agent_names().await;
2499            if let Some(name) = parser::parse_delete_agent(raw, &agents) {
2500                let cmd = parser::Command::new(&name, parser::Action::Delete);
2501                logic::execute(cmd, String::new(), vec![], &event, &mgr, &bot).await;
2502                return;
2503            }
2504
2505            if let Some(cmd) = parser::parse_agent_cmd(raw, &agents) {
2506                let (quote, imgs) = utils::get_full_content(&event, &bot, Some(&cmd.agent)).await;
2507
2508                // 拼接提示词:引用 + 用户输入参数
2509                let prompt = if matches!(
2510                    cmd.action,
2511                    parser::Action::Chat | parser::Action::Regenerate
2512                ) {
2513                    format!("{}{}", quote, cmd.args).trim().to_string()
2514                } else {
2515                    cmd.args.clone()
2516                };
2517
2518                logic::execute(cmd, prompt, imgs, &event, &mgr, &bot).await;
2519            }
2520        }
2521    });
2522
2523    let mgr_drop = mgr.clone();
2524    PluginBuilder::drop({
2525        move || {
2526            let mgr = mgr_drop.clone();
2527            async move {
2528                // 保存配置
2529                let c = mgr.config.read().await;
2530                mgr.save(&c);
2531                // 关闭全局浏览器实例
2532                // Browser::instance().await.close_async().await.unwrap();
2533                Browser::shutdown_global().await;
2534            }
2535        }
2536    });
2537}