1use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant};
12
13use robit_agent::error::{AgentError, Result};
14use robit_agent::tool::ToolCallInfo;
15use tokio::sync::oneshot;
16
17use crate::frontend::PlatformSender;
18
19#[derive(Debug, Clone)]
21pub struct ConfirmKeywords {
22 pub approve: Vec<String>,
23 pub reject: Vec<String>,
24}
25
26impl Default for ConfirmKeywords {
27 fn default() -> Self {
28 Self {
29 approve: vec![
30 "确认".into(),
31 "同意".into(),
32 "yes".into(),
33 "y".into(),
34 "approve".into(),
35 "ok".into(),
36 "允许".into(),
37 ],
38 reject: vec![
39 "取消".into(),
40 "拒绝".into(),
41 "no".into(),
42 "n".into(),
43 "reject".into(),
44 "cancel".into(),
45 "deny".into(),
46 ],
47 }
48 }
49}
50
51struct PendingConfirmation {
53 sender: Option<oneshot::Sender<bool>>,
56 created_at: Instant,
57 #[allow(dead_code)]
58 chat_id: String,
59 #[allow(dead_code)]
60 tool_call_id: String,
61 #[allow(dead_code)]
62 tool_name: String,
63 #[allow(dead_code)]
64 arguments: String,
65}
66
67pub struct Confirmer {
72 pending: Mutex<HashMap<String, PendingConfirmation>>,
74 platform_sender: Arc<dyn PlatformSender>,
75 timeout: Duration,
76 keywords: ConfirmKeywords,
77}
78
79impl Confirmer {
80 pub fn new(platform_sender: Arc<dyn PlatformSender>, timeout: Duration) -> Self {
82 Self {
83 pending: Mutex::new(HashMap::new()),
84 platform_sender,
85 timeout,
86 keywords: ConfirmKeywords::default(),
87 }
88 }
89
90 pub fn with_keywords(
92 platform_sender: Arc<dyn PlatformSender>,
93 timeout: Duration,
94 keywords: ConfirmKeywords,
95 ) -> Self {
96 Self {
97 pending: Mutex::new(HashMap::new()),
98 platform_sender,
99 timeout,
100 keywords,
101 }
102 }
103
104 pub async fn request(
110 &self,
111 chat_id: &str,
112 info: &ToolCallInfo,
113 auto_approve: bool,
114 ) -> Result<bool> {
115 if auto_approve {
116 return Ok(true);
117 }
118
119 let key = format!("{}:{}", chat_id, info.id);
120
121 let prompt = format_confirmation_prompt(
123 &info.name,
124 &info.arguments,
125 self.timeout,
126 &self.keywords,
127 );
128 if let Err(e) = self.platform_sender.send(chat_id, &prompt).await {
129 tracing::warn!("Failed to send confirmation prompt: {}", e);
130 }
131
132 let (tx, rx) = oneshot::channel::<bool>();
133 {
134 let mut pending = self.pending.lock().unwrap();
135 pending.insert(
136 key.clone(),
137 PendingConfirmation {
138 sender: Some(tx),
139 created_at: Instant::now(),
140 chat_id: chat_id.to_string(),
141 tool_call_id: info.id.clone(),
142 tool_name: info.name.clone(),
143 arguments: info.arguments.clone(),
144 },
145 );
146 }
147
148 let decision = match tokio::time::timeout(self.timeout, rx).await {
150 Ok(Ok(approved)) => approved,
151 Ok(Err(_)) => {
152 false
154 }
155 Err(_) => {
156 self.remove_pending(&key);
158 let _ = self
159 .platform_sender
160 .send(chat_id, "⏰ 已超时,操作已取消")
161 .await;
162 return Ok(false);
163 }
164 };
165
166 self.remove_pending(&key);
168
169 let notice = if decision { "✅ 已确认" } else { "❌ 已取消" };
170 let _ = self.platform_sender.send(chat_id, notice).await;
171 Ok(decision)
172 }
173
174 pub fn check_confirmation_response(&self, chat_id: &str, text: &str) -> Option<bool> {
182 let text_lower = text.trim().to_lowercase();
183 let is_approve = self.keywords.approve.contains(&text_lower);
184 let is_reject = self.keywords.reject.contains(&text_lower);
185 if !is_approve && !is_reject {
186 return None;
187 }
188
189 let mut pending = self.pending.lock().unwrap();
190 let key = pending
192 .keys()
193 .find(|k| k.starts_with(&format!("{}:", chat_id)))
194 .cloned()?;
195 let entry = pending.remove(&key)?;
196 let sender = entry.sender?;
197 let _ = sender.send(is_approve);
198 Some(is_approve)
199 }
200
201 pub fn cleanup_expired(&self) {
206 let now = Instant::now();
207 let mut pending = self.pending.lock().unwrap();
208 pending.retain(|_, entry| now.duration_since(entry.created_at) < self.timeout);
209 }
210
211 fn remove_pending(&self, key: &str) {
212 let mut pending = self.pending.lock().unwrap();
213 pending.remove(key);
214 }
215}
216
217fn format_confirmation_prompt(
219 tool_name: &str,
220 arguments: &str,
221 timeout: Duration,
222 keywords: &ConfirmKeywords,
223) -> String {
224 let secs = timeout.as_secs();
225 let approve_hint = keywords.approve.first().cloned().unwrap_or_else(|| "确认".into());
226 let reject_hint = keywords.reject.first().cloned().unwrap_or_else(|| "取消".into());
227 let pretty_args = serde_json::from_str::<serde_json::Value>(arguments)
229 .ok()
230 .and_then(|v| serde_json::to_string_pretty(&v).ok())
231 .unwrap_or_else(|| arguments.to_string());
232
233 format!(
234 "⚠️ 需要确认工具调用\n\n工具: {}\n参数: {}\n\n回复 \"{}\" 或 \"{}\"\n({}秒内有效)",
235 tool_name, pretty_args, approve_hint, reject_hint, secs
236 )
237}
238
239#[allow(dead_code)]
240fn _ensure_agent_error_used() -> AgentError {
241 AgentError::InternalError(String::new())
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::adapter::{MarkdownFeatures, PlatformCaps, SendResult, UploadResult};
249 use async_trait::async_trait;
250 use std::sync::Mutex as StdMutex;
251 use tokio::sync::Mutex;
252
253 struct MockSender {
254 sent: StdMutex<Vec<(String, String)>>,
255 caps: PlatformCaps,
256 }
257
258 impl MockSender {
259 fn new() -> Arc<Self> {
260 Arc::new(Self {
261 sent: StdMutex::new(Vec::new()),
262 caps: PlatformCaps {
263 supports_edit: true,
264 returns_msg_id: true,
265 supports_markdown: true,
266 markdown_features: MarkdownFeatures::qq(),
267 max_message_length: 2000,
268 supports_images: true,
269 supports_files: true,
270 max_upload_size: 20 * 1024 * 1024,
271 },
272 })
273 }
274
275 fn messages(&self) -> Vec<(String, String)> {
276 self.sent.lock().unwrap().clone()
277 }
278 }
279
280 #[async_trait]
281 impl PlatformSender for MockSender {
282 async fn send(&self, chat_id: &str, text: &str) -> Result<SendResult> {
283 self.sent
284 .lock()
285 .unwrap()
286 .push((chat_id.to_string(), text.to_string()));
287 Ok(SendResult {
288 msg_id: "mock-id".to_string(),
289 })
290 }
291 async fn edit(&self, _chat_id: &str, _msg_id: &str, _text: &str) -> Result<()> {
292 Ok(())
293 }
294 async fn upload_file(&self, _chat_id: &str, _file_path: &str, _media_type: &str) -> Result<UploadResult> {
295 Ok(UploadResult {
296 file_id: "mock-file-id".into(),
297 url: "/mock/upload.png".into(),
298 })
299 }
300 async fn send_media_message(
301 &self,
302 _chat_id: &str,
303 _file_url: &str,
304 _file_name: &str,
305 _media_type: &str,
306 ) -> Result<SendResult> {
307 Ok(SendResult {
308 msg_id: "mock-media-id".to_string(),
309 })
310 }
311 fn capabilities(&self) -> PlatformCaps {
312 self.caps.clone()
313 }
314 }
315
316 fn tool_info(id: &str, name: &str) -> ToolCallInfo {
317 ToolCallInfo {
318 id: id.to_string(),
319 name: name.to_string(),
320 arguments: "{}".to_string(),
321 }
322 }
323
324 #[tokio::test]
325 async fn auto_approve_skips_prompt() {
326 let sender = MockSender::new();
327 let confirmer = Confirmer::new(sender.clone(), Duration::from_secs(60));
328 let info = tool_info("tc1", "bash");
329 let approved = confirmer.request("group:1", &info, true).await.unwrap();
330 assert!(approved);
331 assert!(sender.messages().is_empty());
333 }
334
335 #[tokio::test]
336 async fn approve_keyword_resolves_pending() {
337 let sender = MockSender::new();
338 let confirmer = Arc::new(Confirmer::new(sender.clone(), Duration::from_secs(5)));
339 let info = tool_info("tc2", "write");
340
341 let confirmer_clone = confirmer.clone();
342 let handle = tokio::spawn(async move {
343 confirmer_clone.request("group:1", &info, false).await
344 });
345
346 tokio::time::sleep(Duration::from_millis(50)).await;
348
349 let result = confirmer.check_confirmation_response("group:1", "确认");
351 assert_eq!(result, Some(true));
352
353 let approved = handle.await.unwrap().unwrap();
354 assert!(approved);
355 let msgs = sender.messages();
357 assert!(msgs.iter().any(|(_, t)| t.contains("需要确认")));
358 assert!(msgs.iter().any(|(_, t)| t.contains("已确认")));
359 }
360
361 #[tokio::test]
362 async fn reject_keyword_resolves_pending() {
363 let sender = MockSender::new();
364 let confirmer = Arc::new(Confirmer::new(sender.clone(), Duration::from_secs(5)));
365 let info = tool_info("tc3", "bash");
366
367 let confirmer_clone = confirmer.clone();
368 let handle = tokio::spawn(async move {
369 confirmer_clone.request("group:2", &info, false).await
370 });
371
372 tokio::time::sleep(Duration::from_millis(50)).await;
373 let result = confirmer.check_confirmation_response("group:2", "取消");
374 assert_eq!(result, Some(false));
375
376 let approved = handle.await.unwrap().unwrap();
377 assert!(!approved);
378 }
379
380 #[tokio::test]
381 async fn non_keyword_message_returns_none() {
382 let sender = MockSender::new();
383 let confirmer = Arc::new(Confirmer::new(sender.clone(), Duration::from_secs(60)));
384 let info = tool_info("tc4", "bash");
385
386 let confirmer_clone = confirmer.clone();
388 let handle = tokio::spawn(async move {
389 confirmer_clone.request("group:3", &info, false).await
392 });
393 tokio::time::sleep(Duration::from_millis(50)).await;
394
395 assert_eq!(confirmer.check_confirmation_response("group:3", "hello world"), None);
397
398 assert_eq!(confirmer.check_confirmation_response("group:3", "yes"), Some(true));
400 let _ = handle.await.unwrap();
401 }
402
403 #[tokio::test]
404 async fn no_pending_returns_none() {
405 let sender = MockSender::new();
406 let confirmer = Confirmer::new(sender, Duration::from_secs(5));
407 assert_eq!(confirmer.check_confirmation_response("group:9", "确认"), None);
409 }
410
411 #[tokio::test]
412 async fn timeout_returns_false() {
413 let sender = MockSender::new();
414 let confirmer = Confirmer::new(sender.clone(), Duration::from_millis(80));
415 let info = tool_info("tc5", "bash");
416
417 let approved = confirmer.request("group:4", &info, false).await.unwrap();
418 assert!(!approved);
419 assert!(sender
421 .messages()
422 .iter()
423 .any(|(_, t)| t.contains("超时")));
424 }
425
426 #[tokio::test]
427 async fn only_first_pending_per_chat_resolved() {
428 let sender = MockSender::new();
429 let confirmer = Arc::new(Confirmer::new(sender.clone(), Duration::from_secs(5)));
430
431 let info1 = tool_info("a", "bash");
433 let info2 = tool_info("b", "write");
434 let c1 = confirmer.clone();
435 let c2 = confirmer.clone();
436 let h1 = tokio::spawn(async move { c1.request("group:5", &info1, false).await });
437 let h2 = tokio::spawn(async move { c2.request("group:5", &info2, false).await });
438 tokio::time::sleep(Duration::from_millis(50)).await;
439
440 assert!(confirmer.check_confirmation_response("group:5", "确认").is_some());
445 assert!(confirmer.check_confirmation_response("group:5", "取消").is_some());
446
447 let r1 = h1.await.unwrap().unwrap();
448 let r2 = h2.await.unwrap().unwrap();
449 assert_ne!(r1, r2);
451 }
452
453 #[test]
454 fn cleanup_expired_removes_old_entries() {
455 let sender = MockSender::new();
459 let confirmer = Confirmer::new(sender, Duration::from_secs(1));
460 confirmer.cleanup_expired();
461 }
462
463 #[allow(dead_code)]
465 fn _use_mutex() -> Mutex<()> {
466 Mutex::new(())
467 }
468}