myagent-ai 1.25.2 → 1.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -539,7 +539,7 @@ class ToolDispatcher:
539
539
  ) -> Dict:
540
540
  """网页控制器 — 浏览器面板"""
541
541
  try:
542
- from core.web_control import get_web_control_manager
542
+ from core.web_control import get_web_control_manager, LOGIN_URLS
543
543
  wc_mgr = get_web_control_manager()
544
544
  action = params.get("action", "open")
545
545
  session_id = params.get("session_id", "").strip()
@@ -604,7 +604,7 @@ class ToolDispatcher:
604
604
 
605
605
  elif action in ("get_content", "click", "fill", "scroll", "evaluate", "wait", "screenshot"):
606
606
  # 这些 action 通过命令队列由浏览器面板执行
607
- cmd_result = await session.execute_command(action, params)
607
+ cmd_result = await wc_mgr.queue_command(session_id, action, params, timeout=30)
608
608
  return {
609
609
  "success": cmd_result.get("success", False),
610
610
  "output": cmd_result.get("output", cmd_result.get("result", "")),
@@ -612,6 +612,314 @@ class ToolDispatcher:
612
612
  "data": cmd_result.get("data"),
613
613
  }
614
614
 
615
+ elif action == "human_interact":
616
+ # [v1.25.3] 人机交互模式 — Agent 暂停, 用户手动操作
617
+ prompt = params.get("prompt", "请在上方网页中完成登录或验证操作")
618
+ platform = params.get("platform", "")
619
+ timeout = int(params.get("timeout", 0)) or 300 # 默认5分钟
620
+ auto_save = params.get("auto_save_cookies", True)
621
+
622
+ # 1. 向面板发送 human_interact 命令(切换到人机模式)
623
+ session.human_mode = True
624
+ cmd_result = await wc_mgr.queue_command(
625
+ session_id, "human_interact",
626
+ {"prompt": prompt, "platform": platform},
627
+ timeout=15
628
+ )
629
+
630
+ # 2. 通知前端显示人机交互 UI
631
+ if stream_callback:
632
+ await self._emit_sse("v2_web_control", {
633
+ "action": "human_interact",
634
+ "session_id": session_id,
635
+ "prompt": prompt,
636
+ "platform": platform,
637
+ "timeout": timeout,
638
+ }, stream_callback)
639
+
640
+ # 3. 创建 human_event 并等待用户完成
641
+ loop = asyncio.get_event_loop()
642
+ session.human_event = asyncio.Event()
643
+ session.human_result = None
644
+
645
+ try:
646
+ await asyncio.wait_for(session.human_event.wait(), timeout=timeout)
647
+ except asyncio.TimeoutError:
648
+ session.human_mode = False
649
+ # 恢复 agent 模式
650
+ try:
651
+ await wc_mgr.queue_command(session_id, "agent_mode", {}, timeout=5)
652
+ except Exception:
653
+ pass
654
+ if stream_callback:
655
+ await self._emit_sse("v2_web_control", {
656
+ "action": "human_done",
657
+ "session_id": session_id,
658
+ "timed_out": True,
659
+ }, stream_callback)
660
+ return {"success": False, "error": f"人机交互超时 ({timeout}s)"}
661
+
662
+ # 4. 用户完成, 恢复 agent 模式
663
+ session.human_mode = False
664
+ try:
665
+ await wc_mgr.queue_command(session_id, "agent_mode", {}, timeout=5)
666
+ except Exception:
667
+ pass
668
+
669
+ # 5. 自动保存 cookies
670
+ cookie_file = ""
671
+ if auto_save:
672
+ try:
673
+ cookie_file = session.save_cookies_to_file()
674
+ except Exception as e:
675
+ cookie_file = f"(保存失败: {e})"
676
+
677
+ # 6. 获取当前页面信息
678
+ page_info = ""
679
+ try:
680
+ info_result = await wc_mgr.queue_command(session_id, "get_content", {"what": "url,title"}, timeout=10)
681
+ if info_result.get("success"):
682
+ page_info = info_result.get("result", "")
683
+ except Exception:
684
+ pass
685
+
686
+ result_data = session.human_result or {}
687
+ if stream_callback:
688
+ await self._emit_sse("v2_web_control", {
689
+ "action": "human_done",
690
+ "session_id": session_id,
691
+ }, stream_callback)
692
+
693
+ output_parts = [f"用户已完成人机交互操作"]
694
+ if page_info:
695
+ output_parts.append(f"当前页面: {page_info}")
696
+ if cookie_file:
697
+ output_parts.append(f"Cookies 已保存: {cookie_file}")
698
+ if result_data.get("note"):
699
+ output_parts.append(f"用户备注: {result_data['note']}")
700
+
701
+ return {
702
+ "success": True,
703
+ "output": "\n".join(output_parts),
704
+ "session_id": session_id,
705
+ "cookies_saved": bool(cookie_file) and not cookie_file.startswith("("),
706
+ "cookie_file": cookie_file,
707
+ "data": result_data,
708
+ }
709
+
710
+ elif action == "login":
711
+ # [v1.25.4] 一站式登录流程: 导航 → 人机交互 → 自动保存凭证
712
+ platform = params.get("platform", "").strip().lower()
713
+ login_url = params.get("url", "").strip()
714
+ prompt = params.get("prompt", "")
715
+ timeout = int(params.get("timeout", 0)) or 300
716
+ label = params.get("label", "")
717
+
718
+ # 查找平台登录 URL
719
+ if not login_url and platform:
720
+ login_url = LOGIN_URLS.get(platform, "")
721
+ if not login_url:
722
+ return {"success": False, "error": f"未知平台 '{platform}',未找到预置登录 URL,请通过 url 参数指定登录页地址"}
723
+
724
+ if not login_url:
725
+ return {"success": False, "error": "请提供 platform (平台名称) 或 url (登录页地址) 参数"}
726
+
727
+ # 构建默认提示
728
+ platform_names = {
729
+ "qq": "QQ", "qq_mail": "QQ邮箱", "wechat_work": "企业微信",
730
+ "wechat_mp": "微信公众号", "telegram": "Telegram", "telegram_web": "Telegram Web",
731
+ "discord": "Discord", "feishu": "飞书", "feishu_admin": "飞书管理后台",
732
+ "lark": "Lark", "dingtalk": "钉钉", "github": "GitHub",
733
+ "google": "Google", "bilibili": "B站", "taobao": "淘宝",
734
+ "zhihu": "知乎", "weibo": "微博",
735
+ }
736
+ display_name = platform_names.get(platform, platform) if platform else ""
737
+ if not prompt:
738
+ prompt = f"请在上方页面完成 {display_name} 登录操作" if display_name else "请在上方页面完成登录操作"
739
+ # 添加平台特定提示
740
+ if platform in ("qq", "wechat_mp", "telegram", "discord", "dingtalk"):
741
+ prompt += "(可能需要扫码或输入账号密码)"
742
+ elif platform in ("wechat_work", "feishu", "feishu_admin", "lark"):
743
+ prompt += "(请使用管理员账号扫码登录)"
744
+
745
+ # Step 1: 打开面板
746
+ session.current_url = login_url
747
+ if stream_callback:
748
+ await self._emit_sse("v2_web_control", {
749
+ "action": "login",
750
+ "session_id": session_id,
751
+ "url": login_url,
752
+ "panel_url": f"/api/web_control/panel?sid={session_id}",
753
+ "platform": platform,
754
+ "prompt": prompt,
755
+ "timeout": timeout,
756
+ }, stream_callback)
757
+
758
+ # Step 2: 等待面板打开
759
+ if not session.is_panel_open:
760
+ waited = 0
761
+ while waited < 15:
762
+ await asyncio.sleep(1)
763
+ waited += 1
764
+ if session.is_panel_open:
765
+ break
766
+ if session._closed:
767
+ return {"success": False, "error": "会话已关闭"}
768
+
769
+ # Step 3: 切换人机交互模式
770
+ session.human_mode = True
771
+ try:
772
+ await wc_mgr.queue_command(
773
+ session_id, "human_interact",
774
+ {"prompt": prompt, "platform": display_name},
775
+ timeout=15
776
+ )
777
+ except Exception as e:
778
+ logger.warning(f"[WebControl] human_interact 命令发送失败: {e}")
779
+
780
+ # Step 4: 等待用户完成
781
+ loop = asyncio.get_event_loop()
782
+ session.human_event = asyncio.Event()
783
+ session.human_result = None
784
+
785
+ try:
786
+ await asyncio.wait_for(session.human_event.wait(), timeout=timeout)
787
+ except asyncio.TimeoutError:
788
+ session.human_mode = False
789
+ try:
790
+ await wc_mgr.queue_command(session_id, "agent_mode", {}, timeout=5)
791
+ except Exception:
792
+ pass
793
+ if stream_callback:
794
+ await self._emit_sse("v2_web_control", {
795
+ "action": "human_done", "session_id": session_id, "timed_out": True,
796
+ }, stream_callback)
797
+ return {"success": False, "error": f"登录超时 ({timeout}s),请重试"}
798
+
799
+ # Step 5: 用户完成,恢复 agent 模式
800
+ session.human_mode = False
801
+ try:
802
+ await wc_mgr.queue_command(session_id, "agent_mode", {}, timeout=5)
803
+ except Exception:
804
+ pass
805
+
806
+ # Step 6: 获取当前页面信息
807
+ page_info = ""
808
+ try:
809
+ info_result = await wc_mgr.queue_command(session_id, "get_content", {"what": "url,title"}, timeout=10)
810
+ if info_result.get("success"):
811
+ page_info = info_result.get("result", "")
812
+ except Exception:
813
+ pass
814
+
815
+ # Step 7: 自动保存 cookies + 凭证
816
+ cookie_file = ""
817
+ cred_file = ""
818
+ result_data = session.human_result or {}
819
+
820
+ try:
821
+ cookie_file = session.save_cookies_to_file()
822
+ except Exception as e:
823
+ cookie_file = f"(保存失败: {e})"
824
+
825
+ # 保存到凭证库
826
+ if platform:
827
+ try:
828
+ extra = {"note": result_data.get("note", ""), "login_url": login_url}
829
+ cred_file = session.save_credentials_to_file(platform, label=label, extra=extra)
830
+ except Exception as e:
831
+ cred_file = f"(保存失败: {e})"
832
+
833
+ # 检测是否登录成功 (URL 发生了变化)
834
+ login_success = False
835
+ if page_info and login_url:
836
+ current_url = page_info.split(",")[0].strip() if "," in page_info else page_info
837
+ login_success = (current_url != login_url and "login" not in current_url.lower())
838
+
839
+ if stream_callback:
840
+ await self._emit_sse("v2_web_control", {
841
+ "action": "login_done",
842
+ "session_id": session_id,
843
+ "platform": platform,
844
+ "login_success": login_success,
845
+ }, stream_callback)
846
+
847
+ output_parts = []
848
+ if login_success:
849
+ output_parts.append(f"{display_name} 登录成功")
850
+ else:
851
+ output_parts.append(f"用户已完成 {display_name} 登录操作")
852
+ if page_info:
853
+ output_parts.append(f"当前页面: {page_info}")
854
+ if cookie_file and not cookie_file.startswith("("):
855
+ output_parts.append(f"Cookies 已保存: {cookie_file}")
856
+ if cred_file and not cred_file.startswith("("):
857
+ output_parts.append(f"凭证已保存: {cred_file}")
858
+ if result_data.get("note"):
859
+ output_parts.append(f"用户备注: {result_data['note']}")
860
+
861
+ return {
862
+ "success": True,
863
+ "output": "\n".join(output_parts),
864
+ "session_id": session_id,
865
+ "login_success": login_success,
866
+ "platform": platform,
867
+ "cookie_file": cookie_file,
868
+ "credential_file": cred_file,
869
+ "data": result_data,
870
+ }
871
+
872
+ elif action == "save_credentials":
873
+ # [v1.25.4] 保存凭证到凭证库
874
+ platform = params.get("platform", "").strip()
875
+ label = params.get("label", "").strip()
876
+ if not platform:
877
+ return {"success": False, "error": "请提供 platform 参数 (平台名称)"}
878
+ try:
879
+ filepath = session.save_credentials_to_file(platform, label=label)
880
+ return {"success": True, "output": f"凭证已保存到: {filepath}", "credential_file": filepath}
881
+ except Exception as e:
882
+ return {"success": False, "error": f"保存凭证失败: {e}"}
883
+
884
+ elif action == "list_credentials":
885
+ # [v1.25.4] 列出所有已保存的凭证
886
+ creds = WebControlSession.list_credentials()
887
+ if not creds:
888
+ return {"success": True, "output": "暂无已保存的登录凭证", "data": []}
889
+ lines = [f"已保存 {len(creds)} 个凭证:"]
890
+ for c in creds:
891
+ lines.append(f" - {c['platform']} ({c.get('label', '无标签')}) | 保存时间: {c['saved_at']} | Cookies: {c['cookie_count']}个")
892
+ return {"success": True, "output": "\n".join(lines), "data": creds}
893
+
894
+ elif action == "delete_credentials":
895
+ # [v1.25.4] 删除凭证
896
+ platform = params.get("platform", "").strip()
897
+ if not platform:
898
+ return {"success": False, "error": "请提供 platform 参数"}
899
+ deleted = WebControlSession.delete_credentials(platform)
900
+ if deleted:
901
+ return {"success": True, "output": f"已删除 {platform} 的凭证"}
902
+ else:
903
+ return {"success": False, "error": f"未找到 {platform} 的凭证"}
904
+
905
+ elif action == "save_cookies":
906
+ # [v1.25.3] 保存 cookies 到文件
907
+ label = params.get("label", "")
908
+ try:
909
+ filepath = session.save_cookies_to_file(label)
910
+ return {"success": True, "output": f"Cookies 已保存到: {filepath} ({len(session.cookies)} 个)", "cookie_file": filepath}
911
+ except Exception as e:
912
+ return {"success": False, "error": f"保存 cookies 失败: {e}"}
913
+
914
+ elif action == "load_cookies":
915
+ # [v1.25.3] 从文件加载 cookies
916
+ label = params.get("label", "")
917
+ count = session.load_cookies_from_file(label)
918
+ if count > 0:
919
+ return {"success": True, "output": f"已加载 {count} 个 Cookies"}
920
+ else:
921
+ return {"success": False, "error": f"未找到匹配的 Cookie 文件 (label: {label or '自动'})"}
922
+
615
923
  else:
616
924
  return {"success": False, "error": f"未知 web_control action: {action}"}
617
925
 
@@ -1,27 +1,57 @@
1
1
  """
2
- [v1.21.0] Web Control — 聊天内嵌网页控制器
2
+ [v1.25.4] Web Control — 聊天内嵌网页控制器
3
3
 
4
4
  架构说明:
5
5
  - 服务端代理: 通过 /api/web_control/proxy 获取并改写网页内容, 注入控制脚本
6
6
  - 会话管理: 每个聊天会话可创建一个 web_control 会话, 维护 cookie/命令队列/结果 Future
7
7
  - 双向通信: Agent → 命令队列 → 客户端轮询 → 执行 → POST 结果 → Agent 阻塞等待
8
8
  - 前端面板: 基础JS框架 + 动态容器(iframe), 服务端下发控制脚本在容器内执行
9
+ - [v1.25.3] 人机交互: human_interact action 允许 Agent 暂停控制, 用户手动登录后继续
10
+ - [v1.25.4] 登录流程: login action 提供一站式登录编排 + 凭证管理
9
11
 
10
12
  工具 web_control 支持的 action:
11
- open — 打开 URL (发送 v2_web_control SSE 事件, 前端打开面板)
12
- navigate — 在已打开的面板中导航到新 URL
13
- click — 点击元素 (selector: CSS 选择器)
14
- fill — 填写输入框 (selector + value)
15
- scroll — 滚动页面 (direction: up/down/top/bottom, distance: px)
16
- evaluate — 执行 JavaScript (script: JS 代码)
17
- get_content — 获取页面内容 (what: text/html/url/title/cookies/links)
18
- set_cookies — 设置 cookie (cookies: [{name, value, domain, path}])
19
- get_cookies — 获取当前 cookie
20
- wait — 等待 (time: 毫秒 或 selector: CSS选择器, timeout: 秒)
21
- screenshot — 截图 (返回 base64 PNG)
22
- close 关闭 web_control 面板
13
+ open — 打开 URL (发送 v2_web_control SSE 事件, 前端打开面板)
14
+ navigate — 在已打开的面板中导航到新 URL
15
+ click — 点击元素 (selector: CSS 选择器)
16
+ fill — 填写输入框 (selector + value)
17
+ scroll — 滚动页面 (direction: up/down/top/bottom, distance: px)
18
+ evaluate — 执行 JavaScript (script: JS 代码)
19
+ get_content — 获取页面内容 (what: text/html/url/title/cookies/links)
20
+ set_cookies — 设置 cookie (cookies: [{name, value, domain, path}])
21
+ get_cookies — 获取当前 cookie
22
+ wait — 等待 (time: 毫秒 或 selector: CSS选择器, timeout: 秒)
23
+ screenshot — 截图 (返回 base64 PNG)
24
+ human_interact [v1.25.3] 切换为人机交互模式, 用户可手动操作页面 (登录/验证码等)
25
+ save_cookies — [v1.25.3] 保存当前会话 cookies 到持久化文件
26
+ load_cookies — [v1.25.3] 从持久化文件加载 cookies
27
+ login — [v1.25.4] 一站式登录流程: 导航到登录页 → 人机交互 → 自动保存凭证
28
+ save_credentials — [v1.25.4] 保存登录凭证 (cookies + 元信息) 到凭证库
29
+ list_credentials — [v1.25.4] 列出已保存的凭证
30
+ delete_credentials — [v1.25.4] 删除指定凭证
31
+ close — 关闭 web_control 面板
23
32
  """
24
33
 
34
+ # [v1.25.4] 常用平台登录 URL 模板
35
+ LOGIN_URLS = {
36
+ "qq": "https://qun.qq.com/",
37
+ "qq_mail": "https://mail.qq.com/",
38
+ "wechat_work": "https://work.weixin.qq.com/wework_admin/frame",
39
+ "wechat_mp": "https://mp.weixin.qq.com/",
40
+ "telegram": "https://web.telegram.org/",
41
+ "telegram_web": "https://web.telegram.org/",
42
+ "discord": "https://discord.com/login",
43
+ "feishu": "https://open.feishu.cn/",
44
+ "feishu_admin": "https://feishu.cn/admin",
45
+ "lark": "https://open.larksuite.com/",
46
+ "dingtalk": "https://login.dingtalk.com/",
47
+ "github": "https://github.com/login",
48
+ "google": "https://accounts.google.com/",
49
+ "bilibili": "https://passport.bilibili.com/login",
50
+ "taobao": "https://login.taobao.com/",
51
+ "zhihu": "https://www.zhihu.com/signin",
52
+ "weibo": "https://passport.weibo.com/sso/signin",
53
+ }
54
+
25
55
  import asyncio
26
56
  import json
27
57
  import os
@@ -107,6 +137,19 @@ CONTROL_SCRIPT = """
107
137
  case 'screenshot':
108
138
  result = await doScreenshot(cmd.params || {});
109
139
  break;
140
+ case 'human_interact':
141
+ // [v1.25.3] 切换人机模式
142
+ window.__webControlHumanMode = true;
143
+ // 显示人机模式提示横幅
144
+ showHumanModeBanner(cmd.params || {});
145
+ result = { success: true, human_mode: true, message: '已切换到人机交互模式, 用户可自由操作页面' };
146
+ break;
147
+ case 'agent_mode':
148
+ // [v1.25.3] 恢复 Agent 控制模式
149
+ window.__webControlHumanMode = false;
150
+ hideHumanModeBanner();
151
+ result = { success: true, human_mode: false, message: '已恢复 Agent 控制模式' };
152
+ break;
110
153
  default:
111
154
  result = { success: false, error: 'Unknown action: ' + cmd.action };
112
155
  }
@@ -285,6 +328,40 @@ CONTROL_SCRIPT = """
285
328
  });
286
329
  }
287
330
 
331
+ // [v1.25.3] 人机模式横幅
332
+ function showHumanModeBanner(p) {
333
+ var banner = document.createElement('div');
334
+ banner.id = '__wcHumanBanner';
335
+ var promptText = (p && p.prompt) ? p.prompt : '人机交互模式 — 请在此页面完成登录或验证操作';
336
+ var platformText = (p && p.platform) ? ' (' + p.platform + ')' : '';
337
+ banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;padding:10px 16px;' +
338
+ 'background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;font-size:14px;font-weight:600;' +
339
+ 'display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 12px rgba(0,0,0,0.3);font-family:-apple-system,BlinkMacSystemFont,sans-serif;';
340
+ banner.innerHTML = '<span>\u{1F3AF} ' + promptText + platformText + '</span>';
341
+ document.body.appendChild(banner);
342
+ // 为页面内容添加顶部间距
343
+ document.body.style.paddingTop = '44px';
344
+ // [v1.25.4] 记录进入人机模式时的 URL, 用于检测登录成功
345
+ window.__webControlLoginStartUrl = window.location.href;
346
+ // [v1.25.4] 监听 URL 变化 (登录成功通常伴随跳转)
347
+ window.__webControlLastNotifiedUrl = window.location.href;
348
+ var _urlCheckInterval = setInterval(function() {
349
+ if (!window.__webControlHumanMode) { clearInterval(_urlCheckInterval); return; }
350
+ var currentUrl = window.location.href;
351
+ if (currentUrl !== window.__webControlLastNotifiedUrl) {
352
+ window.__webControlLastNotifiedUrl = currentUrl;
353
+ // 通知父窗口 URL 发生了变化 (可能登录成功)
354
+ notifyParent('url_change', { url: currentUrl, previous_url: window.__webControlLoginStartUrl });
355
+ }
356
+ }, 1000);
357
+ }
358
+ function hideHumanModeBanner() {
359
+ var banner = document.getElementById('__wcHumanBanner');
360
+ if (banner) banner.remove();
361
+ document.body.style.paddingTop = '';
362
+ window.__webControlLoginStartUrl = null;
363
+ }
364
+
288
365
  function doScreenshot(p) {
289
366
  // html2canvas 未加载时返回提示
290
367
  if (typeof html2canvas === 'undefined') {
@@ -296,8 +373,12 @@ CONTROL_SCRIPT = """
296
373
  });
297
374
  }
298
375
 
299
- // 拦截链接点击, 重定向到代理
376
+ // [v1.25.3] 人机交互模式标志
377
+ window.__webControlHumanMode = false;
378
+
379
+ // 拦截链接点击, 重定向到代理(人机模式下跳过拦截)
300
380
  document.addEventListener('click', function(e) {
381
+ if (window.__webControlHumanMode) return; // 人机模式不拦截
301
382
  var link = e.target.closest('a');
302
383
  if (link && link.href) {
303
384
  // 允许 target=_blank 和特殊链接
@@ -312,8 +393,9 @@ CONTROL_SCRIPT = """
312
393
  }
313
394
  }, true);
314
395
 
315
- // 拦截表单提交
396
+ // 拦截表单提交(人机模式下跳过拦截)
316
397
  document.addEventListener('submit', function(e) {
398
+ if (window.__webControlHumanMode) return; // 人机模式不拦截
317
399
  var form = e.target;
318
400
  if (form.method && form.method.toLowerCase() === 'post') {
319
401
  // POST 表单暂不代理, 仅阻止默认行为
@@ -354,6 +436,10 @@ class WebControlSession:
354
436
  self.pending_results: Dict[str, asyncio.Future] = {}
355
437
  self.is_panel_open = False
356
438
  self._closed = False
439
+ # [v1.25.3] 人机交互模式
440
+ self.human_mode = False
441
+ self.human_event: Optional[asyncio.Event] = None # Agent 等待用户完成操作
442
+ self.human_result: Optional[Dict[str, Any]] = None
357
443
 
358
444
  def is_alive(self) -> bool:
359
445
  if self._closed:
@@ -371,6 +457,123 @@ class WebControlSession:
371
457
  if not future.done():
372
458
  future.set_result({"success": False, "error": "Session closed"})
373
459
  self.pending_results.clear()
460
+ # 唤醒等待 human_event 的协程
461
+ if self.human_event and not self.human_event.is_set():
462
+ self.human_event.set()
463
+
464
+ # [v1.25.3] Cookie 持久化
465
+ def save_cookies_to_file(self, label: str = "") -> str:
466
+ """将当前 cookies 保存到文件, 返回文件路径"""
467
+ import json as _json
468
+ from pathlib import Path
469
+ cookie_dir = Path.home() / ".myagent" / "data" / "web_cookies"
470
+ cookie_dir.mkdir(parents=True, exist_ok=True)
471
+ if not label:
472
+ # 从 current_url 提取域名作为 label
473
+ parsed = urlparse(self.current_url)
474
+ label = parsed.hostname or "default"
475
+ label = re.sub(r'[^a-zA-Z0-9._-]', '_', label)
476
+ filepath = cookie_dir / f"{label}.json"
477
+ data = {
478
+ "url": self.current_url,
479
+ "saved_at": time.strftime("%Y-%m-%d %H:%M:%S"),
480
+ "cookies": [
481
+ {"key": k, "value": v} for k, v in self.cookies.items()
482
+ ]
483
+ }
484
+ filepath.write_text(_json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
485
+ logger.info(f"[WebControl] Cookies 已保存: {filepath} ({len(self.cookies)} 个)")
486
+ return str(filepath)
487
+
488
+ def load_cookies_from_file(self, label: str = "") -> int:
489
+ """从文件加载 cookies, 返回加载的数量"""
490
+ import json as _json
491
+ from pathlib import Path
492
+ cookie_dir = Path.home() / ".myagent" / "data" / "web_cookies"
493
+ if not label:
494
+ parsed = urlparse(self.current_url)
495
+ label = parsed.hostname or "default"
496
+ label = re.sub(r'[^a-zA-Z0-9._-]', '_', label)
497
+ filepath = cookie_dir / f"{label}.json"
498
+ if not filepath.exists():
499
+ return 0
500
+ try:
501
+ data = _json.loads(filepath.read_text(encoding='utf-8'))
502
+ count = 0
503
+ for item in data.get("cookies", []):
504
+ self.cookies[item["key"]] = item["value"]
505
+ count += 1
506
+ logger.info(f"[WebControl] Cookies 已加载: {filepath} ({count} 个)")
507
+ return count
508
+ except Exception as e:
509
+ logger.error(f"[WebControl] 加载 cookies 失败: {e}")
510
+ return 0
511
+
512
+ # [v1.25.4] 凭证管理 — 保存/列出/删除登录凭证
513
+ def save_credentials_to_file(self, platform: str, label: str = "", extra: Dict = None) -> str:
514
+ """保存完整登录凭证 (cookies + 元信息) 到凭证库, 返回文件路径"""
515
+ import json as _json
516
+ from pathlib import Path
517
+ cred_dir = Path.home() / ".myagent" / "data" / "credentials"
518
+ cred_dir.mkdir(parents=True, exist_ok=True)
519
+ # platform 作为文件名
520
+ safe_platform = re.sub(r'[^a-zA-Z0-9._-]', '_', platform).lower()
521
+ if not safe_platform:
522
+ safe_platform = "default"
523
+ filepath = cred_dir / f"{safe_platform}.json"
524
+ data = {
525
+ "platform": platform,
526
+ "url": self.current_url,
527
+ "saved_at": time.strftime("%Y-%m-%d %H:%M:%S"),
528
+ "cookie_count": len(self.cookies),
529
+ "cookies": [
530
+ {"key": k, "value": v} for k, v in self.cookies.items()
531
+ ],
532
+ }
533
+ if extra:
534
+ data["extra"] = extra
535
+ if label:
536
+ data["label"] = label
537
+ filepath.write_text(_json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
538
+ logger.info(f"[WebControl] 凭证已保存: {filepath} (平台: {platform}, cookies: {len(self.cookies)})")
539
+ return str(filepath)
540
+
541
+ @staticmethod
542
+ def list_credentials() -> List[Dict]:
543
+ """列出所有已保存的凭证"""
544
+ import json as _json
545
+ from pathlib import Path
546
+ cred_dir = Path.home() / ".myagent" / "data" / "credentials"
547
+ if not cred_dir.exists():
548
+ return []
549
+ results = []
550
+ for f in sorted(cred_dir.glob("*.json")):
551
+ try:
552
+ data = _json.loads(f.read_text(encoding='utf-8'))
553
+ results.append({
554
+ "platform": data.get("platform", f.stem),
555
+ "label": data.get("label", ""),
556
+ "url": data.get("url", ""),
557
+ "saved_at": data.get("saved_at", ""),
558
+ "cookie_count": data.get("cookie_count", 0),
559
+ "file": str(f),
560
+ })
561
+ except Exception:
562
+ pass
563
+ return results
564
+
565
+ @staticmethod
566
+ def delete_credentials(platform: str) -> bool:
567
+ """删除指定平台的凭证"""
568
+ from pathlib import Path
569
+ cred_dir = Path.home() / ".myagent" / "data" / "credentials"
570
+ safe_platform = re.sub(r'[^a-zA-Z0-9._-]', '_', platform).lower()
571
+ filepath = cred_dir / f"{safe_platform}.json"
572
+ if filepath.exists():
573
+ filepath.unlink()
574
+ logger.info(f"[WebControl] 凭证已删除: {filepath} (平台: {platform})")
575
+ return True
576
+ return False
374
577
 
375
578
 
376
579
  class WebControlManager:
@@ -192,7 +192,11 @@ class MainAgent(BaseAgent):
192
192
  - 截图: {"action": "screenshot", "session_id": "xxx"}
193
193
  - 等待: {"action": "wait", "time": 1000} 或 {"action": "wait", "selector": ".result", "timeout": 10}
194
194
  - Cookie: {"action": "set_cookies", "cookies": [...], "session_id": "xxx"} 或 {"action": "get_cookies", "session_id": "xxx"}
195
+ - 人机交互: {"action": "human_interact", "session_id": "xxx", "prompt": "请在页面完成登录", "timeout": 300} — 暂停Agent控制,让用户手动操作页面(登录/验证码/滑块等),完成后自动捕获Cookies
196
+ - 保存Cookie: {"action": "save_cookies", "session_id": "xxx"} — 将当前Cookies持久化到文件(自动以域名命名)
197
+ - 加载Cookie: {"action": "load_cookies", "session_id": "xxx", "label": "域名"} — 从文件加载已保存的Cookies
195
198
  - 关闭: {"action": "close", "session_id": "xxx"}
199
+ - 人机交互使用场景: 当网站需要扫码登录、短信验证码、图形验证码、滑块验证等人工操作时,使用 human_interact 让用户在浏览器面板中完成。完成后Cookies会自动保存,下次可直接load_cookies复用。
196
200
 
197
201
  专业技能指令: 系统内置了丰富的专业技能指南(PDF/DOCX/XLSX/PPT 生成、图表绘制、前端开发等),通过 <get_knowledge> 请求相关技能指令。
198
202
  """