1mod 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 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 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
176mod 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 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 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 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 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 match kovi::serde_json::to_string(s) {
275 Ok(escaped) => {
276 let trimmed = escaped.trim_matches('"');
277 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 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 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 let viewport = Viewport::new(width, body_height + 100).with_device_scale_factor(2.0);
370 tab.set_viewport(&viewport).await?;
371
372 time::sleep(Duration::from_millis(100)).await;
374
375 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 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 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 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 imgs.push(u.to_string());
458 } else if seg.type_ == "video" {
459 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 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 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 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 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!("", 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 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 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 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
657mod 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 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 let mut agent_name = String::new();
856 let mut match_char_len = 0;
857 let mut sorted = agents.to_vec();
858 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 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 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(); let r = raw.trim(); if s.is_empty() {
913 return (Action::Chat, r.to_string(), vec![]);
914 }
915
916 if s == "!" {
918 return (Action::Stop, String::new(), vec![]);
919 }
920
921 if s.starts_with("~#") {
924 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 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 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 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 if s.starts_with('%') {
978 let arg = r.get(1..).unwrap_or("").trim();
979 return (Action::SetModel, arg.to_string(), vec![]);
980 }
981
982 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 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 if clean.starts_with('\'') {
1023 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 (Action::Chat, r.to_string(), vec![])
1046 }
1047}
1048
1049mod 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 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
1151mod 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 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 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 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 let gen_id = if temp_mode {
1318 0 } 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 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 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 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 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 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 let re =
1534 Regex::new(r"!\[.*?\]\(((?:https?://|data:image/)[^\s\)]+)\)")
1535 .unwrap();
1536 re.replace_all(content, |caps: ®ex::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 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 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 use std::collections::BTreeMap;
1810 let mut groups: BTreeMap<String, Vec<(usize, &Agent)>> = BTreeMap::new();
1811
1812 for (i, a) in c.agents.iter().enumerate() {
1814 groups.entry(a.model.clone()).or_default().push((i + 1, a));
1815 }
1816
1817 let mut html_parts = Vec::new();
1819
1820 for (model, mut agents) in groups {
1822 agents.sort_by(|a, b| a.1.name.to_lowercase().cmp(&b.1.name.to_lowercase()));
1824
1825 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 for (real_idx, a) in agents {
1833 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 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 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 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 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 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 let mut html = String::new();
1931
1932 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 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 if !other_models.is_empty() {
1966 html.push_str(&render_group("Other Models", &other_models));
1967 }
1968
1969 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: ®ex::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", 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 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 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 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(['"', '“', '”', '。', '.'], ""); 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 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
2463use 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 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 let c = mgr.config.read().await;
2530 mgr.save(&c);
2531 Browser::shutdown_global().await;
2534 }
2535 }
2536 });
2537}