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