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.
- package/core/tool_dispatcher.py +310 -2
- package/core/web_control.py +218 -15
- package/myagent/agents/main_agent.py +4 -0
- package/myagent/core/tool_dispatcher.py +310 -2
- package/myagent/core/web_control.py +218 -15
- package/myagent/skills/registry.py +12 -6
- package/myagent/web/api_server.py +227 -0
- package/myagent/web/ui/chat/chat_main.js +36 -0
- package/package.json +1 -1
- package/worklog.md +20 -0
package/core/tool_dispatcher.py
CHANGED
|
@@ -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
|
|
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
|
|
package/core/web_control.py
CHANGED
|
@@ -1,27 +1,57 @@
|
|
|
1
1
|
"""
|
|
2
|
-
[v1.
|
|
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
|
|
12
|
-
navigate
|
|
13
|
-
click
|
|
14
|
-
fill
|
|
15
|
-
scroll
|
|
16
|
-
evaluate
|
|
17
|
-
get_content
|
|
18
|
-
set_cookies
|
|
19
|
-
get_cookies
|
|
20
|
-
wait
|
|
21
|
-
screenshot
|
|
22
|
-
|
|
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
|
"""
|