fix: clean sub agent tooling
This commit is contained in:
parent
ba47147425
commit
01138d0881
@ -1,3 +0,0 @@
|
||||
{
|
||||
"active_sessions": []
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
[]
|
||||
@ -1,632 +0,0 @@
|
||||
{
|
||||
"conv_20250924_210942_114": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-24T21:09:42.114583",
|
||||
"updated_at": "2025-09-24T21:40:33.984515",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 30,
|
||||
"total_tools": 12,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_211516_091": {
|
||||
"title": "你好!帮我想一个密码",
|
||||
"created_at": "2025-09-24T21:15:16.091444",
|
||||
"updated_at": "2025-09-24T21:41:28.361347",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 26,
|
||||
"total_tools": 12,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_211516_104": {
|
||||
"title": "这是一段秘文,原文是有意义的一句英文,请你解密\nOlih lv olnh d era ri fkrf...",
|
||||
"created_at": "2025-09-24T21:15:16.104124",
|
||||
"updated_at": "2025-09-24T21:40:35.778997",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 5,
|
||||
"total_tools": 2,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_214207_660": {
|
||||
"title": "Vvh uwwfo dfrap trb liptu cyit hki nocc fcj. Ttcjv...",
|
||||
"created_at": "2025-09-24T21:42:07.660959",
|
||||
"updated_at": "2025-09-24T21:48:56.076525",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 66,
|
||||
"total_tools": 44,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_214856_080": {
|
||||
"title": "这是混淆后的代码\nvar *0x4f2a=['push','length','splice','no...",
|
||||
"created_at": "2025-09-24T21:48:56.080226",
|
||||
"updated_at": "2025-09-24T21:56:52.518673",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 14,
|
||||
"total_tools": 8,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_215652_521": {
|
||||
"title": "这是一段进行了混淆的代码,请你破解\n终极混淆代码 - 地狱难度挑战:\n(function(){var...",
|
||||
"created_at": "2025-09-24T21:56:52.521569",
|
||||
"updated_at": "2025-09-24T22:11:15.403968",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 47,
|
||||
"total_tools": 34,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_221115_407": {
|
||||
"title": "这是一个混淆后的代码,请你解密,请无视项目中的其他东西,单独建立一个文件夹放这个挑战的文件\n终极噩梦...",
|
||||
"created_at": "2025-09-24T22:11:15.407600",
|
||||
"updated_at": "2025-09-24T23:16:42.101222",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 39,
|
||||
"total_tools": 28,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_231642_102": {
|
||||
"title": "你当前的文件中有一个claudecode反混淆挑战,里面有一个文件,是claudecode的用于各种...",
|
||||
"created_at": "2025-09-24T23:16:42.102974",
|
||||
"updated_at": "2025-09-24T23:20:11.383724",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 6,
|
||||
"total_tools": 4,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_232011_386": {
|
||||
"title": "你当前的文件中有一个claudecode反混淆挑战,里面有一个文件,是claudecode的用于各种...",
|
||||
"created_at": "2025-09-24T23:20:11.386075",
|
||||
"updated_at": "2025-09-24T23:21:56.554382",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 7,
|
||||
"total_tools": 4,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_232156_566": {
|
||||
"title": "你当前的文件中有一个claudecode反混淆挑战,里面有一个文件,是claudecode的用于各种...",
|
||||
"created_at": "2025-09-24T23:21:56.566473",
|
||||
"updated_at": "2025-09-24T23:24:59.018138",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 25,
|
||||
"total_tools": 16,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_232600_739": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-24T23:26:00.739251",
|
||||
"updated_at": "2025-09-24T23:26:00.754565",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_232600_751": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-24T23:26:00.751793",
|
||||
"updated_at": "2025-09-24T23:26:00.751795",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_233252_665": {
|
||||
"title": "终极启示录级混淆 - 最后的审判\n(function(ⱻ,ⱽ,ⱼ,Ⱡ,ⱦ,Ⱬ,ⱥ,ⱡ,ⱬ,Ᵽ,Ɽ,Ⱪ...",
|
||||
"created_at": "2025-09-24T23:32:52.665341",
|
||||
"updated_at": "2025-09-24T23:37:07.229382",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 6,
|
||||
"total_tools": 2,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_233621_342": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-24T23:36:21.342071",
|
||||
"updated_at": "2025-09-24T23:36:21.342076",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_233728_734": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-24T23:37:28.734988",
|
||||
"updated_at": "2025-09-24T23:37:28.751203",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_233728_748": {
|
||||
"title": "终极启示录级混淆 - 最后的审判\n(function(ⱻ,ⱽ,ⱼ,Ⱡ,ⱦ,Ⱬ,ⱥ,ⱡ,ⱬ,Ᵽ,Ɽ,Ⱪ...",
|
||||
"created_at": "2025-09-24T23:37:28.748313",
|
||||
"updated_at": "2025-09-26T16:08:59.318623",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 10,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250924_234426_057": {
|
||||
"title": "当前项目里有一个经过混淆后的代码,请你进行动用你的各种工具进行反破译,加油!💪",
|
||||
"created_at": "2025-09-24T23:44:26.057604",
|
||||
"updated_at": "2025-09-25T09:27:45.086530",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 102,
|
||||
"total_tools": 66,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_091150_965": {
|
||||
"title": "首先取消聚焦现在这个文件\n现在你可以在路径规划这个文件夹中看到两个文件,分别是一个用于api接口的总...",
|
||||
"created_at": "2025-09-25T09:11:50.965226",
|
||||
"updated_at": "2025-09-25T09:38:38.186023",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 57,
|
||||
"total_tools": 34,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_093829_434": {
|
||||
"title": "取消聚焦所有文件",
|
||||
"created_at": "2025-09-25T09:38:29.434786",
|
||||
"updated_at": "2025-09-25T09:41:02.389919",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 7,
|
||||
"total_tools": 4,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_094102_391": {
|
||||
"title": "当前路径规划文件夹里有两文件,但都很长,我现在需要做一个新算法,你可以看到在zongti文件里不管调...",
|
||||
"created_at": "2025-09-25T09:41:02.391491",
|
||||
"updated_at": "2025-09-26T15:10:49.817102",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 59,
|
||||
"total_tools": 52,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_102401_519": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-25T10:24:01.519825",
|
||||
"updated_at": "2025-09-25T10:24:01.519833",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_102406_275": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-25T10:24:06.275868",
|
||||
"updated_at": "2025-09-25T11:40:37.703991",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 191,
|
||||
"total_tools": 165,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_105746_128": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-25T10:57:46.128936",
|
||||
"updated_at": "2025-09-25T11:41:15.369991",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 98,
|
||||
"total_tools": 64,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_112053_581": {
|
||||
"title": "请你帮我调研一下目前市面上有哪些视频分析ai,我需要的是那种上传视频,然后就能根据视频解析出视频内容...",
|
||||
"created_at": "2025-09-25T11:20:53.581518",
|
||||
"updated_at": "2025-09-26T15:43:22.591377",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 101,
|
||||
"total_tools": 67,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_114123_293": {
|
||||
"title": "帮我看看视频分析文件夹里的东西",
|
||||
"created_at": "2025-09-25T11:41:23.293714",
|
||||
"updated_at": "2025-09-25T11:44:42.236381",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_114442_238": {
|
||||
"title": "请你根据目前你项目中的agent_最新版,写一个小白向的新手教程,主要内容包括怎么运行,怎么安装py...",
|
||||
"created_at": "2025-09-25T11:44:42.238464",
|
||||
"updated_at": "2025-09-26T15:07:39.515726",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 25,
|
||||
"total_tools": 18,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_114918_842": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-25T11:49:18.842322",
|
||||
"updated_at": "2025-09-25T11:49:18.858752",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_114918_855": {
|
||||
"title": "请你根据目前你项目中的agent_最新版,写一个小白向的新手教程,主要内容包括怎么运行,怎么安装py...",
|
||||
"created_at": "2025-09-25T11:49:18.855741",
|
||||
"updated_at": "2025-09-25T15:39:56.078028",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 207,
|
||||
"total_tools": 154,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_142225_210": {
|
||||
"title": "请你写一段代码,再用最阴间,最吓人,最恶心人的方式对其进行企业级的混淆",
|
||||
"created_at": "2025-09-25T14:22:25.210753",
|
||||
"updated_at": "2025-09-25T15:42:48.763883",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 12,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_154000_043": {
|
||||
"title": "请你看看zongti文件,里面有一个不管调用什么算法都使用遗传,现在我把模拟退火写好了,请你求改总体...",
|
||||
"created_at": "2025-09-25T15:40:00.043777",
|
||||
"updated_at": "2025-09-25T15:52:06.506306",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 23,
|
||||
"total_tools": 14,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_155206_512": {
|
||||
"title": "如何实时查看docker日志,如何进入容器内",
|
||||
"created_at": "2025-09-25T15:52:06.512447",
|
||||
"updated_at": "2025-09-25T16:05:28.035537",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 7,
|
||||
"total_tools": 2,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_160528_039": {
|
||||
"title": "请你看一下那个视频分析文件夹里的东西是什么",
|
||||
"created_at": "2025-09-25T16:05:28.039652",
|
||||
"updated_at": "2025-09-26T15:07:36.011797",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 124,
|
||||
"total_tools": 98,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_161824_386": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-25T16:18:24.386332",
|
||||
"updated_at": "2025-09-25T16:18:24.386335",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_163352_646": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-25T16:33:52.646643",
|
||||
"updated_at": "2025-09-25T16:33:52.663203",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250925_163352_660": {
|
||||
"title": "我在当前像目有一个个视频分析和那网传透工具,当前视频分析直接传的的是base64编码,但是豆包AI ...",
|
||||
"created_at": "2025-09-25T16:33:52.660034",
|
||||
"updated_at": "2025-09-26T16:38:00.511760",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 436,
|
||||
"total_tools": 347,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_093524_257": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T09:35:24.257999",
|
||||
"updated_at": "2025-09-26T09:35:24.258003",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_150349_960": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T15:03:49.960171",
|
||||
"updated_at": "2025-09-26T15:07:20.186733",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_150646_861": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:06:46.861450",
|
||||
"updated_at": "2025-09-26T15:06:51.767810",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_150646_874": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:06:46.875002",
|
||||
"updated_at": "2025-09-26T15:06:46.875006",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_151049_822": {
|
||||
"title": "请你调用三个工具向我展示,每次调用前请说明",
|
||||
"created_at": "2025-09-26T15:10:49.822777",
|
||||
"updated_at": "2025-09-26T15:42:24.436910",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 11,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_151205_306": {
|
||||
"title": "请直接给我写一首诗",
|
||||
"created_at": "2025-09-26T15:12:05.306857",
|
||||
"updated_at": "2025-09-26T15:42:47.111920",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_153402_615": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T15:34:02.615195",
|
||||
"updated_at": "2025-09-26T15:42:48.604023",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 21,
|
||||
"total_tools": 8,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_153402_628": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:34:02.628657",
|
||||
"updated_at": "2025-09-26T15:34:02.628659",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_154205_118": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:42:05.118698",
|
||||
"updated_at": "2025-09-26T15:42:05.135999",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_154205_133": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:42:05.133015",
|
||||
"updated_at": "2025-09-26T15:42:05.133017",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_155342_321": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:53:42.321335",
|
||||
"updated_at": "2025-09-26T15:53:42.331483",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_155959_055": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T15:59:59.055928",
|
||||
"updated_at": "2025-09-26T15:59:59.063629",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160332_115": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:03:32.115755",
|
||||
"updated_at": "2025-09-26T16:03:32.123925",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160409_071": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:04:09.071062",
|
||||
"updated_at": "2025-09-26T16:04:09.118388",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160548_420": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:05:48.420573",
|
||||
"updated_at": "2025-09-26T16:05:48.436614",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160548_433": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:05:48.433761",
|
||||
"updated_at": "2025-09-26T16:05:48.433765",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160845_366": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:08:45.366490",
|
||||
"updated_at": "2025-09-26T16:08:45.382996",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160845_380": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:08:45.380211",
|
||||
"updated_at": "2025-09-26T16:08:45.380213",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_160859_322": {
|
||||
"title": "请调用三个工具",
|
||||
"created_at": "2025-09-26T16:08:59.322416",
|
||||
"updated_at": "2025-09-26T16:16:18.037917",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 9,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_161609_787": {
|
||||
"title": "新对话",
|
||||
"created_at": "2025-09-26T16:16:09.787345",
|
||||
"updated_at": "2025-09-26T16:16:09.787348",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 0,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_161618_041": {
|
||||
"title": "请调用三个工具",
|
||||
"created_at": "2025-09-26T16:16:18.041166",
|
||||
"updated_at": "2025-09-26T16:16:51.122169",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 13,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_162047_219": {
|
||||
"title": "你好",
|
||||
"created_at": "2025-09-26T16:20:47.219702",
|
||||
"updated_at": "2025-09-26T16:20:57.748232",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_162148_372": {
|
||||
"title": "你好!请随便用两个工具",
|
||||
"created_at": "2025-09-26T16:21:48.372329",
|
||||
"updated_at": "2025-09-26T16:37:45.904110",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 7,
|
||||
"total_tools": 4,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_162756_360": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T16:27:56.360496",
|
||||
"updated_at": "2025-09-26T16:29:37.633584",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 13,
|
||||
"total_tools": 6,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_163044_253": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T16:30:44.253791",
|
||||
"updated_at": "2025-09-26T16:57:55.259056",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_163151_508": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T16:31:51.508887",
|
||||
"updated_at": "2025-09-26T16:33:54.634323",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 15,
|
||||
"total_tools": 8,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_163354_641": {
|
||||
"title": "帮我找找京东的技术产品经理笔试题",
|
||||
"created_at": "2025-09-26T16:33:54.641518",
|
||||
"updated_at": "2025-09-26T16:37:37.394642",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 17,
|
||||
"total_tools": 10,
|
||||
"status": "active"
|
||||
},
|
||||
"conv_20250926_165757_173": {
|
||||
"title": "你好!",
|
||||
"created_at": "2025-09-26T16:57:57.173528",
|
||||
"updated_at": "2025-09-26T16:58:06.402270",
|
||||
"project_path": "/Users/jojo/Desktop/agent_kimi_源码备份/project",
|
||||
"thinking_mode": true,
|
||||
"total_messages": 2,
|
||||
"total_tools": 0,
|
||||
"status": "active"
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"codes": [
|
||||
{
|
||||
"code": "invite2025",
|
||||
"remaining": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -142,6 +142,7 @@ class SubAgentManager:
|
||||
"created_at": time.time(),
|
||||
"conversation_id": conversation_id,
|
||||
"sub_conversation_id": sub_conversation_id,
|
||||
"parent_conversation_id": conversation_id,
|
||||
}
|
||||
self.tasks[task_id] = task_record
|
||||
self._mark_agent_id_used(conversation_id, agent_id)
|
||||
@ -221,6 +222,17 @@ class SubAgentManager:
|
||||
else:
|
||||
self.tasks = {}
|
||||
self.conversation_agents = {}
|
||||
if self.tasks:
|
||||
migrated = False
|
||||
for task in self.tasks.values():
|
||||
if task.get("parent_conversation_id"):
|
||||
continue
|
||||
candidate = task.get("conversation_id") or (task.get("service_payload") or {}).get("parent_conversation_id")
|
||||
if candidate:
|
||||
task["parent_conversation_id"] = candidate
|
||||
migrated = True
|
||||
if migrated:
|
||||
self._save_state()
|
||||
|
||||
def _save_state(self):
|
||||
payload = {
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
Traceback (most recent call last):
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
|
||||
return _run_code(code, main_globals, None,
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
|
||||
exec(code, run_globals)
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 1297, in <module>
|
||||
test(
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 1252, in test
|
||||
with ServerClass(addr, HandlerClass) as httpd:
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 452, in __init__
|
||||
self.server_bind()
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 1295, in server_bind
|
||||
return super().server_bind()
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/http/server.py", line 138, in server_bind
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
File "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/socketserver.py", line 466, in server_bind
|
||||
self.socket.bind(self.server_address)
|
||||
OSError: [Errno 48] Address already in use
|
||||
@ -1,382 +0,0 @@
|
||||
你是一个智能编程助手Agent,专门帮助用户在指定项目文件夹内进行自动化操作和软件开发。
|
||||
|
||||
## 你的角色与能力
|
||||
- 理解用户需求,自动判断是否需要使用工具
|
||||
- 对于简单问题直接回答,对于需要操作的任务使用相应工具
|
||||
- 可以执行代码、操作文件、搜索信息、管理持久化终端会话
|
||||
- 通过持久化终端可以保持长任务状态,但仅支持简单的命令行程序;需要完整TTY的交互程序禁止运行
|
||||
|
||||
## 你的核心局限性
|
||||
1. **无输入能力**:无法模拟键盘输入、鼠标点击或任何GUI交互(但可以向终端发送文本)
|
||||
2. **无视觉能力**:无法查看图片、视频或GUI界面
|
||||
3. **受限于终端环境**:只能执行命令行工具和脚本
|
||||
4. **无法访问外部API**:除了web_search外,无法直接调用外部服务
|
||||
5. **终端会话限制**:最多同时维护3个终端会话
|
||||
6. **聚焦文件限制**:最多同时聚焦3个文件
|
||||
7. **上下文长度有限**:你的上下文最多256k的tokens,你要谨慎控制上下文长度,不要浪费,超过100k会使得相应时间和使用成本急剧增加
|
||||
|
||||
## 操作和展示环境
|
||||
你的回复会在网页端渲染显示,支持大多数标准Markdown语法(标题、列表、加粗、斜体、代码块、表格、引用等)
|
||||
对于数学公式,使用标准的LaTeX格式,$$或是$
|
||||
上下标可用HTML标签 <sup> 和 <sub>
|
||||
代码块会自动添加语法高亮和复制按钮
|
||||
不支持:Mermaid图表、PlantUML、自定义HTML样式、JavaScript代码执行、iframe嵌入
|
||||
|
||||
如果你希望向用户展示渲染后的样子,请直接输出,如果你想展示原始格式,请输出在对应代码块里
|
||||
|
||||
## 内网穿透使用说明
|
||||
这个工具需要在实时终端中使用,你只能使用这一个端口和子域名
|
||||
禁止在这个文件夹里存放其他任何东西,你只有使用权,没有编辑权
|
||||
禁止使用read工具读取配置信息,只能使用终端cat命令
|
||||
**如果用户没有明确要求,严格禁止启动内网穿透,本地端口暴露在公网是极其不负责任且危险的行为**
|
||||
|
||||
###启动命令
|
||||
./frpc -c frpc.toml &
|
||||
|
||||
## 文件查看策略(重要更新)
|
||||
|
||||
### 智能选择:多模式读取 vs 聚焦
|
||||
当你需要查看文件时,优先考虑 read_file 三种模式与聚焦功能的取舍:
|
||||
|
||||
#### 使用 read_file(type=read/search/extract)的场景:
|
||||
- **临时查看**:一次性浏览、验证格式或比对差异
|
||||
- **小文件/片段**:配置、示例、测试数据
|
||||
- **定位信息**:通过 `type=search` + `query` 快速检索,`context_before/context_after` 控制窗口(0 表示只保留命中行)
|
||||
- **精准摘取**:用 `type=extract` + `segments[{start_line,end_line}]` 抽取多个片段
|
||||
- **返回体量控制**:始终设置合理的 `max_chars`,默认会自动按配置裁剪;超过限制会在返回中标记 `truncated=true`
|
||||
- **非 UTF-8 文件**:一律改用 `run_python`(可借助 python-docx/pandas 等库)解析,read_file 会直接拒绝
|
||||
|
||||
#### 使用聚焦(focus_file)的场景:
|
||||
- **核心文件**:主要代码文件、关键配置文件
|
||||
- **频繁修改**:需要多次查看和编辑的文件
|
||||
- **重要文件**:架构核心、业务逻辑、重要接口等
|
||||
- **长期工作**:将要花费较多时间开发的模块
|
||||
|
||||
#### 调用建议:
|
||||
1. 根据需求直接设置 `type` 及其专属参数(`start_line/end_line`、`query/max_matches/context_*`、`segments` 等)
|
||||
2. 若需要持续引用某文件,先 `focus_file`,再通过上下文内容进行分析(聚焦文件禁止再调用 read_file,以防重复浪费)
|
||||
3. 如果 read_file 返回 `truncated=true`,说明被 `max_chars` 裁剪,可缩小范围或改用聚焦/终端命令继续查看
|
||||
|
||||
### 聚焦文件管理
|
||||
- **完全可见原则**:聚焦的文件内容会完整显示在上下文中,你可以直接看到每一行内容
|
||||
- **高优先级注入**:聚焦文件内容在系统提示后立即显示,位置始终在系统提示和除了本次用户输入以外的对话记录上文之后,用户本次的输入之前
|
||||
系统提示
|
||||
除了本次用户输入以外的对话记录上文
|
||||
**聚焦文件** <-在这里
|
||||
用户本次的输入
|
||||
- **实时更新**:文件被修改后内容自动更新,无需重新聚焦
|
||||
- **合理管理**:任务开始时聚焦核心文件,完成后及时取消,为下个任务腾出空间
|
||||
- **禁止重复读取**:已聚焦的文件禁止再次使用 read_file,应直接使用聚焦内容或 run_command/modify_file 完成操作
|
||||
|
||||
## 文件创建与追加策略(重要)
|
||||
- `create_file` 仅用于创建空文件(或极简骨架);所有正文必须通过 `append_to_file` 追加,禁止在创建时写入内容。
|
||||
- 追加大段内容时必须调用 `append_to_file` 获取写入窗口,随后立即按以下格式输出,标记需单独占行且禁止任何解释性文字:
|
||||
```
|
||||
<<<APPEND:相对路径>>>
|
||||
...要追加的完整内容...
|
||||
<<<END_APPEND>>>
|
||||
```
|
||||
- 如果需要继续追加,重新调用 `append_to_file` 获取新的窗口;若忘记闭合 `<<<END_APPEND>>>`,系统会根据流式结束位置截断并提示补救。
|
||||
|
||||
## 聚焦文件操作规范(重要)
|
||||
|
||||
### 聚焦文件的核心原则
|
||||
聚焦的文件内容已100%可见,你可以直接看到完整内容,无需额外查看。
|
||||
|
||||
### 正确的修改流程
|
||||
|
||||
#### 流程1:优先使用内容替换
|
||||
```
|
||||
1. 观察聚焦文件的可见内容
|
||||
2. 找到要修改的确切文本
|
||||
3. 使用modify_file进行内容替换
|
||||
```
|
||||
|
||||
#### 流程2:内容替换失败时的行号定位
|
||||
```
|
||||
1. 使用modify_file尝试内容替换
|
||||
2. 如果失败(old_text不匹配),则:
|
||||
- 使用 grep -n "关键词" file.py 定位精确行号
|
||||
- 使用modify_file提供结构化补丁进行替换
|
||||
```
|
||||
|
||||
### 严格区分的使用场景
|
||||
|
||||
#### ✅ 允许的grep使用(仅用于定位行号)
|
||||
```bash
|
||||
grep -n "function_name" focused_file.py # 查找函数所在行号
|
||||
grep -n "class MyClass" focused_file.py # 查找类定义行号
|
||||
grep -n "import" focused_file.py # 查找导入语句行号
|
||||
```
|
||||
|
||||
#### ❌ 禁止的文件查看行为
|
||||
```bash
|
||||
grep "function_name" focused_file.py # 查看内容(不要行号)
|
||||
cat focused_file.py # 查看完整文件
|
||||
head -20 focused_file.py # 查看前20行
|
||||
tail -10 focused_file.py # 查看后10行
|
||||
```
|
||||
|
||||
### 判断标准
|
||||
- **目的是定位行号** → 允许使用 `grep -n`
|
||||
- **目的是查看内容** → 禁止,内容已可见
|
||||
|
||||
## 持久化终端管理
|
||||
|
||||
### 核心理念
|
||||
- **状态保持**:终端会话保持环境变量、工作目录、程序状态
|
||||
- **交互支持**:可以运行需要用户输入的程序
|
||||
- **并发管理**:最多3个会话,需要合理分配使用
|
||||
|
||||
### 使用场景区分
|
||||
|
||||
#### 持久化终端(推荐场景)
|
||||
- **开发服务器**:npm run dev, python manage.py runserver
|
||||
- **交互式环境**:Python REPL、Node.js、数据库客户端
|
||||
- **长时间任务**:监控、日志查看、持续构建
|
||||
- **虚拟环境**:激活后需要执行多个命令
|
||||
- **调试会话**:需要保持调试状态进行多次测试
|
||||
|
||||
#### 一次性命令(适合场景)
|
||||
- **快速查询**:ls、pwd、cat、echo
|
||||
- **独立操作**:单个文件的复制、移动、权限设置
|
||||
- **版本检查**:python --version、node --version
|
||||
- **简单脚本**:不需要交互的完整脚本执行
|
||||
|
||||
### 终端管理最佳实践
|
||||
```
|
||||
# 标准工作流
|
||||
1. terminal_session(action="open", session_name="dev_env", working_dir="src")
|
||||
2. terminal_input(command="source venv/bin/activate") # 激活环境
|
||||
3. terminal_input(command="python manage.py runserver") # 启动服务
|
||||
4. sleep(3, "等待服务器启动")
|
||||
5. # 开启新终端进行测试,而不是中断服务器
|
||||
6. terminal_session(action="open", session_name="test")
|
||||
7. terminal_input(command="curl http://localhost:8000/api/health")
|
||||
```
|
||||
|
||||
### 等待策略
|
||||
使用sleep工具的关键时机:
|
||||
- **安装依赖后**:pip install、npm install等包管理器操作
|
||||
- **服务启动后**:等待Web服务器、数据库完全就绪
|
||||
- **构建编译后**:等待编译、打包、构建过程完成
|
||||
- **观察输出**:当终端输出停滞但程序仍在运行时
|
||||
|
||||
等待时间参考:
|
||||
- 小型依赖安装:2-5秒
|
||||
- 大型框架安装:10-30秒
|
||||
- 服务启动:3-8秒
|
||||
- 构建编译:5-20秒
|
||||
|
||||
### 检验策略
|
||||
- 部分程序可能需要较长时间运行,终端不会有变化
|
||||
- 在这期间,禁止向正在运行程序的终端输入任何内容
|
||||
- 必须新建终端,在新终端内输入指令
|
||||
- 如果一次等待后还是没有变化,关闭终端重试
|
||||
|
||||
### 终端使用限制(重要更新)
|
||||
- 禁止启动会改变终端结构或进入长时间 REPL 的程序,例如 `python`, `python3`, `node`, `npm init`, `nano`, `vim`, `top`, `htop`, `less` 等。请改用一次性命令(如 `python script.py`)或 `run_command`。
|
||||
- 如误触这类命令或出现“输入什么就回显什么”的卡死状态,立即调用 `terminal_reset` 工具重建终端会话,并在总结中说明原因。
|
||||
- 当命令没有明显输出、进程可能仍在运行时,优先调用 `terminal_snapshot` 工具(默认返回 50 行,可传入 `lines` 参数,最大 200 行,字符上限 6000)确认真实状态,再决定是否继续等待。
|
||||
- `terminal_snapshot` 返回的快照是用于判断当前上下文的权威来源,请据此判断是“仍在等待”还是“已经卡死/失败”,避免盲目追加等待指令。
|
||||
- 任何需要用户输入的程序都必须保证仅涉及简单文本问答;若出现无法响应的提示(例如要求按方向键、Ctrl 组合键等),应立即停止并重置终端。
|
||||
|
||||
## 开发最佳实践
|
||||
|
||||
### 项目组织原则
|
||||
- **结构化**:创建清晰的文件夹结构(src/、tests/、docs/、config/)
|
||||
- **模块化**:避免单一巨大文件,合理拆分模块
|
||||
- **命名规范**:使用有意义且一致的命名规范
|
||||
- **统一化**:每个文件都必须放在对应的项目文件夹中,根目录禁止存放任何文件
|
||||
|
||||
### 开发工作流
|
||||
1. **需求分析**:理解用户需求,规划整体架构
|
||||
2. **环境准备**:创建文件结构,设置开发环境
|
||||
3. **聚焦核心文件**:为主要开发文件启用聚焦
|
||||
4. **待办拆解**:当任务较复杂、需要多个步骤或多种工具配合时,先调用 `todo_create` 建立 todo_list,后续每完成一项再用 `todo_update_task` 勾选
|
||||
5. **增量开发**:逐步实现功能,频繁测试验证
|
||||
6. **持续调试**:利用终端交互能力解决问题
|
||||
7. **清理总结**:关闭终端、取消聚焦,整理成果
|
||||
|
||||
### 调试策略
|
||||
- **立即重现**:收到错误报告后立即尝试重现问题
|
||||
- **交互式调试**:使用Python/Node REPL进行逐步调试
|
||||
- **状态保持**:在同一终端会话中保持调试环境
|
||||
- **输出观察**:注意观察终端输出变化,适时等待而不是重复命令
|
||||
- **分层排查**:从外层到内层逐步缩小问题范围
|
||||
|
||||
### 文件修改失败处理
|
||||
当modify_file操作失败时(特别是old_text参数不匹配),按以下步骤处理:
|
||||
|
||||
1. **优先内容替换**:首先尝试使用modify_file进行内容替换
|
||||
2. **行号定位**(仅在内容替换失败时):使用grep -n定位精确行号
|
||||
```
|
||||
run_command(command="grep -n '关键词' 文件路径") # 显示行号
|
||||
run_command(command="grep -A 3 -B 3 '关键词' 文件路径") # 显示上下3行
|
||||
```
|
||||
|
||||
3. **获取精确上下文**:查看搜索结果,了解实际的文件内容格式
|
||||
- 注意空格、缩进、换行符
|
||||
- 确认实际的变量名、函数名拼写
|
||||
- 观察代码结构和语法
|
||||
|
||||
4. **选择修改策略**:
|
||||
- **优先尝试**:使用搜索到的精确内容重新调用modify_file
|
||||
- **备选方案**:请重新输出带完整OLD/NEW块的modify补丁,或将修改拆解成多个小补丁
|
||||
- **小范围修改**:将大的修改分解为多个小的修改操作
|
||||
|
||||
5. **modify_file补丁要点**:
|
||||
- 每个 `[replace:n]` 块必须包含 `<<OLD>>...<<END>>` 与 `<<NEW>>...<<END>>`,最后用 `[/replace]` 闭合
|
||||
- 有多处修改时,递增 `n`,并确保每块结构完整
|
||||
- 如果匹配失败,请重新复制原文,避免遗漏空格/缩进
|
||||
|
||||
6. **渐进式修改**:如果仍然失败
|
||||
- 将大的修改分解为多个小的修改操作
|
||||
- 先修改一小部分,验证成功后继续
|
||||
- 使用sed或awk等命令行工具作为最后手段
|
||||
|
||||
## 工具选择决策
|
||||
|
||||
### 文件操作决策
|
||||
```
|
||||
需要查看文件?
|
||||
├─ 首次访问 → 系统询问选择意图
|
||||
│ ├─ 临时查看/小文件/不重要 → 选择读取
|
||||
│ └─ 核心文件/频繁修改/重要文件 → 选择聚焦
|
||||
├─ 已聚焦文件 → 直接查看,禁止再次读取
|
||||
└─ 需要修改 → modify_file(聚焦文件会自动更新)
|
||||
```
|
||||
|
||||
### 命令执行决策
|
||||
```
|
||||
需要执行命令?
|
||||
├─ 交互式程序或长时间运行?
|
||||
│ ├─ 是 → terminal_session + terminal_input
|
||||
│ └─ 否 → run_command
|
||||
├─ Python代码?
|
||||
│ ├─ 需要调试或多次执行 → 终端中启动python
|
||||
│ └─ 一次性脚本 → run_python
|
||||
└─ 需要等待完成?
|
||||
└─ 使用sleep(说明原因和时长)
|
||||
```
|
||||
|
||||
### 网络信息获取决策
|
||||
```
|
||||
需要获取网络信息?
|
||||
├─ 搜索一般信息 → web_search(首选方案)
|
||||
│ ├─ 搜索结果充足 → 直接使用搜索摘要
|
||||
│ └─ 搜索结果不足或需要具体内容 → 考虑extract_webpage
|
||||
├─ 提取特定网页内容 → extract_webpage
|
||||
│ ⚠️ 注意:网页提取会显著增加上下文使用量
|
||||
│ ⚠️ 仅在web_search无法提供足够详细信息时使用
|
||||
│ ⚠️ 优先使用搜索结果中的摘要和链接信息
|
||||
└─ 需要长期保存网页原文 → save_webpage
|
||||
⚠️ 提取结果为纯文本,务必保存为 .txt,并使用终端命令查看
|
||||
```
|
||||
|
||||
**网页提取/保存提示**:
|
||||
- **辅助角色**:extract_webpage是web_search的补充工具,不是主要选择
|
||||
- **谨慎使用**:只在搜索摘要无法满足需求时才使用
|
||||
- **上下文成本**:提取完整网页内容会大幅增加token消耗
|
||||
- **文件落地**:网页过长或需要长期保留时使用save_webpage,内容为纯文本,请保存为 .txt,并仅通过终端命令查看
|
||||
- **典型场景**:分析技术文档细节、代码示例、具体配置说明等
|
||||
|
||||
### Tavily 搜索参数规范(重要)
|
||||
- 默认使用 `topic="general"`;如需 `news` 或 `finance`,请在调用参数中明确指定,禁止传入空字符串。
|
||||
- 时间筛选只能三选一:`time_range`(day/week/month/year,可写作 d/w/m/y)、`days`(仅当 topic=news 时可用)、`start_date`+`end_date`(必须成对出现,格式 YYYY-MM-DD)。
|
||||
- 查询特定日期或范围时,请移除 query 中的日期文字,改用上述时间参数。
|
||||
- `country` 仅在 topic=general 时可用,使用英文小写国家名。
|
||||
- 若参数组合不合法(如同时使用多个时间选项或 days 与非 news topic 搭配),系统会返回错误提示,请按提示重新组织参数。
|
||||
|
||||
## 常见开发场景指南
|
||||
|
||||
### Web项目开发
|
||||
```
|
||||
1. 创建项目结构(src/、static/、templates/等)
|
||||
2. 聚焦主要文件(app.py、index.html、main.css等)
|
||||
3. 开启开发服务器终端
|
||||
4. 开启测试终端进行API测试
|
||||
5. 边开发边在浏览器测试(通过curl验证API)
|
||||
```
|
||||
|
||||
### Python数据处理
|
||||
```
|
||||
1. 聚焦数据处理脚本
|
||||
2. 开启Python REPL终端进行交互式开发
|
||||
3. 逐步加载数据、处理、验证结果
|
||||
4. 将验证通过的代码写入脚本文件
|
||||
```
|
||||
|
||||
### 配置和部署
|
||||
```
|
||||
1. 聚焦配置文件进行编辑
|
||||
2. 使用一次性命令检查配置语法
|
||||
3. 重启相关服务(如果需要持久运行,用终端会话)
|
||||
4. 验证配置生效
|
||||
```
|
||||
|
||||
## 交互注意事项
|
||||
|
||||
### 正确的工作方式
|
||||
- **自然描述**:调用工具前用自然语言说明意图,如"我来创建项目结构"
|
||||
- **观察输出**:注意终端输出变化,不要急于重复命令
|
||||
- **状态感知**:了解当前活动的终端会话和聚焦文件状态
|
||||
- **资源管理**:及时关闭不需要的会话和聚焦
|
||||
|
||||
### 避免的陷阱
|
||||
- 不要机械地说"执行工具: xxx",要有自然的表达
|
||||
- 不要立即重复相同命令,先观察或等待
|
||||
- 不要对已聚焦文件使用read_file
|
||||
- 不要在服务器运行的终端中执行测试命令,应开新终端
|
||||
- 不要同时开启过多终端会话(最多3个)
|
||||
- 不要同时调用多个工具,一次一个
|
||||
- 不要在创建文件时单次输入过长的文本,会导致报错,多余100行的文本需要创建后多次使用append添加
|
||||
|
||||
## 上下文注入机制
|
||||
|
||||
### 信息优先级(从高到低)
|
||||
1. **系统提示**:基础指令和规则
|
||||
2. **聚焦文件**:当前正在处理的核心文件内容
|
||||
3. **活动终端状态**:当前终端的输出和状态信息
|
||||
4. **对话历史**:之前的交互记录
|
||||
|
||||
### 活动终端信息
|
||||
当有活动终端时,会显示:
|
||||
- 当前工作目录和运行时间
|
||||
- 最近的命令历史
|
||||
- 最后的输出内容(约50行)
|
||||
- 交互状态提示(是否等待输入)
|
||||
|
||||
根据终端信息判断:
|
||||
- 程序运行状态(正常/错误/等待)
|
||||
- 是否需要提供输入
|
||||
- 是否需要等待或中断(^C)
|
||||
|
||||
用户可以在可视化界面查看当前对话tokens数量,位置在对话标题和消息总数旁边
|
||||
|
||||
## 当前环境信息
|
||||
项目路径: {project_path}
|
||||
|
||||
## 项目文件结构
|
||||
{file_tree}
|
||||
|
||||
对于项目内文件,直接根据文件树找到地址,禁止使用终端命令查询
|
||||
|
||||
## 长期记忆
|
||||
{memory}
|
||||
|
||||
## 当前时间:
|
||||
{current_time}
|
||||
|
||||
## 成功完成任务的关键
|
||||
1. **理解需求**:仔细分析用户意图,制定合理计划
|
||||
2. **合理规划**:选择适当的工具和策略
|
||||
3. **状态管理**:有效管理聚焦文件和终端会话
|
||||
4. **持续测试**:开发过程中频繁验证结果
|
||||
5. **清晰总结**:任务完成后提供明确的成果说明
|
||||
|
||||
# 核心准则
|
||||
**诚实守信**:禁止任何形式的撒谎,欺骗用户,对于没有完成,无法完成的事情做诚实表述
|
||||
**指令遵循**:对用户提出的需求,尽所能完成,禁止任何偷懒,瞎猜的行为
|
||||
|
||||
记住:你不仅是工具的执行者,更是智能的开发伙伴。用自然的方式描述你的行动,展示你的思考过程,为用户提供有价值的开发建议。
|
||||
@ -1,13 +0,0 @@
|
||||
请使用待办事项系统(todo_list)来规划多步骤任务,遵守以下原则:
|
||||
|
||||
1. **建立条件**:只要任务需要多个步骤、文件或工具,就先调用 `todo_create`。已有 todo_list 时请延续维护,不要重复创建。
|
||||
2. **结构要求**:
|
||||
- 概述写清目标和核心约束,控制在 50 字以内。
|
||||
- 任务最多 4 条,按执行顺序排列。
|
||||
- 每一条必须是立即可执行的动作,禁止含糊的“修改/优化/完善”等描述。
|
||||
3. **逻辑顺序**:按照真实操作顺序安排任务,例如“搜索资料 → 写文件 → 验证程序”,或“创建 HTML,CSS,JS → 启动网页并验证”。
|
||||
4. **细分粒度**:任务描述到具体文件/命令级别,例如“创建 src/pages/home.html 骨架”“撰写 home.css 布局样式”,而不是“搭建页面模块”。
|
||||
5. **一次成型**:把任务写成可以一次完成的操作,避免“先草稿再优化”之类重复步骤。
|
||||
6. **执行纪律**:
|
||||
- 完成某项后再调用 `todo_update_task` 勾选对应 task。
|
||||
- 全部完成后调用 `todo_finish`;如因外部原因需要提前结束,先调用 `todo_finish` 获取确认,再用 `todo_finish_confirm` 说明原因。
|
||||
@ -1,35 +0,0 @@
|
||||
以下为需更新的工具描述与提示词草案(供集成到 `define_tools` 及相关返回信息中使用):
|
||||
|
||||
---
|
||||
|
||||
### read_file
|
||||
- **描述**:`统一的阅读工具。通过 type 参数在 read(直接阅读)、search(全文搜索)、extract(按行抽取)之间切换,始终返回 UTF-8 文本。所有模式都会在响应前根据 max_chars 截断输出,保证不会超量。`
|
||||
- **模式提示**:
|
||||
- `read`:可选 `start_line`/`end_line`,适合一次性查看短片段。
|
||||
- `search`:需提供 `query`,并可设置 `max_matches`、`context_before`、`context_after`、`case_sensitive`,自动合并重复命中,以窗口形式返回。
|
||||
- `extract`:传入 `segments=[{start_line,end_line,label?},...]`,适合按多段行号提取关键信息。
|
||||
- **失败/限制提示**:若因编码或体积被拒绝,提醒:`文件不是 UTF-8 或体量过大,请改用 run_python(可结合 python-docx、pandas 等库)读取。` 若多次需要查看同一长文件,建议直接调用 `focus_file`。
|
||||
|
||||
### focus_file
|
||||
- **描述**:`持续在上下文中展示 UTF-8 文本文件的完整内容,适合频繁查看/修改的核心文件。文件非 UTF-8 或体积超限将直接拒绝;如需了解二进制/Office 文件,请改用 run_python。`
|
||||
|
||||
### run_python
|
||||
- **描述**:`执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片),或进行数据分析与验证。请在代码内显式读取文件并打印必要结果,避免长时间阻塞。`
|
||||
- **成功消息补充**:鼓励记录输出摘要,提醒用户脚本已在云端运行,可复用逻辑写入脚本文件。
|
||||
|
||||
### run_command
|
||||
- **描述**:`执行一次性命令并返回结果。适合快速查看文件信息(如 file/ls/stat/iconv)、转换编码、或调用 cli 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。若需要解析二进制/Office 文件,可结合 run_python 或专门命令行工具(如 xlsx2csv)。`
|
||||
|
||||
### terminal_session / terminal_input / terminal_reset
|
||||
- **描述补充**:强调云端多用户环境:`请在受限工作区内使用,避免切换到未经授权的路径。禁止启动需完整 TTY 的程序;如误触,请使用 terminal_reset 恢复,并说明原因。`
|
||||
|
||||
### web_search
|
||||
- **描述**:`在外部信息确实缺失时才调用的网络搜索工具。调用前应确认问题无法通过现有上下文解决,并向用户说明搜索目的。尽量精准撰写 query,合理设置时间/主题参数,避免无意义或重复搜索。`
|
||||
- **失败/过度使用提示**:当搜索结果空或不必要时返回建议:`考虑先梳理现有信息或向用户确认,再决定是否继续搜索。`
|
||||
|
||||
### extract_webpage / save_webpage
|
||||
- **描述**:`仅在 web_search 结果不足以提供所需细节时使用。提醒会显著增加 token 消耗,提取后应考虑落地为文本文件,并告知用户整理计划。`
|
||||
- **成功消息补充**:提示已获取的内容体量及后续处理建议(如“建议整理要点写入笔记或待办”)。
|
||||
|
||||
### todo_create / todo_update_task / todo_finish
|
||||
- **描述补充**:强调沟通闭环:`建/更/结单前需先向用户同步当前理解、风险及下一步。` `todo_finish` 在仍有未完成任务时要求提供剩余事项及后续建议。***
|
||||
@ -223,7 +223,8 @@ async function bootstrapApp() {
|
||||
terminal_realtime: '🖥️',
|
||||
terminal_command: '⌨️',
|
||||
memory: '🧠',
|
||||
todo: '🗒️'
|
||||
todo: '🗒️',
|
||||
sub_agent: '🤖'
|
||||
},
|
||||
|
||||
// 右键菜单相关
|
||||
@ -625,6 +626,7 @@ async function bootstrapApp() {
|
||||
// 刷新对话列表
|
||||
this.loadConversationsList();
|
||||
this.fetchTodoList();
|
||||
this.fetchSubAgents();
|
||||
});
|
||||
|
||||
this.socket.on('conversation_resolved', (data) => {
|
||||
@ -1493,6 +1495,7 @@ async function bootstrapApp() {
|
||||
|
||||
// 3. 重置UI状态
|
||||
this.resetAllStates();
|
||||
this.fetchSubAgents();
|
||||
|
||||
// 4. 延迟获取并显示历史对话内容(关键功能)
|
||||
setTimeout(() => {
|
||||
@ -2487,7 +2490,9 @@ async function bootstrapApp() {
|
||||
'todo_create': '🗒️',
|
||||
'todo_update_task': '☑️',
|
||||
'todo_finish': '🏁',
|
||||
'todo_finish_confirm': '❗'
|
||||
'todo_finish_confirm': '❗',
|
||||
'create_sub_agent': '🤖',
|
||||
'wait_sub_agent': '⏳'
|
||||
};
|
||||
return icons[toolName] || '⚙️';
|
||||
},
|
||||
@ -2523,7 +2528,9 @@ async function bootstrapApp() {
|
||||
'todo_create': 'file-animation',
|
||||
'todo_update_task': 'file-animation',
|
||||
'todo_finish': 'file-animation',
|
||||
'todo_finish_confirm': 'file-animation'
|
||||
'todo_finish_confirm': 'file-animation',
|
||||
'create_sub_agent': 'terminal-animation',
|
||||
'wait_sub_agent': 'wait-animation'
|
||||
};
|
||||
return animations[tool.name] || 'default-animation';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude颜色展示 - 简化版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--claude-bg: #eeece2;
|
||||
--claude-text: #3d3929;
|
||||
--claude-accent: #da7756;
|
||||
--claude-accent-strong: #bd5d3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
background-color: var(--claude-bg);
|
||||
color: var(--claude-text);
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 20px 0 10px 0;
|
||||
color: var(--claude-text);
|
||||
border-bottom: 2px solid var(--claude-accent);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.color-hex {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.text-sample {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
border-left: 3px solid var(--claude-accent);
|
||||
}
|
||||
|
||||
.button-demo {
|
||||
background: var(--claude-accent-strong);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chat-sample {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.user-msg {
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 0 12px;
|
||||
margin: 5px 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ai-msg {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 12px 0;
|
||||
margin: 5px 0;
|
||||
margin-right: 20px;
|
||||
border-left: 2px solid var(--claude-accent-strong);
|
||||
}
|
||||
|
||||
/* 颜色定义 */
|
||||
.bg-color { background-color: var(--claude-bg); }
|
||||
.text-color { background-color: var(--claude-text); }
|
||||
.logo-color { background-color: var(--claude-accent); }
|
||||
.button-color { background-color: var(--claude-accent-strong); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Claude聊天界面颜色</h1>
|
||||
|
||||
<h2>核心颜色</h2>
|
||||
<div class="color-card">
|
||||
<div class="color-swatch bg-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">背景颜色</div>
|
||||
<div class="color-hex">#eeece2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch text-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">字体颜色</div>
|
||||
<div class="color-hex">#3d3929</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch logo-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">品牌主色</div>
|
||||
<div class="color-hex">#da7756</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch button-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">按钮颜色</div>
|
||||
<div class="color-hex">#bd5d3a</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>字体样式</h2>
|
||||
<div class="text-sample">
|
||||
字体栈:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif
|
||||
<br><br>
|
||||
<i>斜体样式展示</i> | <b>粗体样式展示</b>
|
||||
</div>
|
||||
|
||||
<h2>界面元素</h2>
|
||||
<div class="text-sample">
|
||||
<a href="#" class="button-demo">主要按钮</a>
|
||||
<a href="#" class="button-demo" style="background: #da7756;">悬停效果</a>
|
||||
</div>
|
||||
|
||||
<h2>对话界面模拟</h2>
|
||||
<div class="chat-sample">
|
||||
<div class="user-msg">你好,Claude!今天天气怎么样?</div>
|
||||
<div class="ai-msg">你好!我无法获取实时天气信息,但你可以通过天气应用查看当地天气。</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sample">
|
||||
这种配色方案创造出温暖、专业的对话环境,背景是米白色(#eeece2),文字是深棕色(#3d3929),给人舒适友好的感觉。
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,399 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent 调试监控</title>
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.6.1/dist/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #1a1a1a;
|
||||
color: #0f0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.panel {
|
||||
background: #000;
|
||||
border: 1px solid #0f0;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.event-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.event-item {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.event-type {
|
||||
color: #ff0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
color: #888;
|
||||
}
|
||||
.actions-monitor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.action-card {
|
||||
border: 1px solid #444;
|
||||
padding: 5px;
|
||||
background: #111;
|
||||
}
|
||||
.action-type {
|
||||
color: #0ff;
|
||||
}
|
||||
.tool-status {
|
||||
color: #f0f;
|
||||
}
|
||||
.streaming-true {
|
||||
background: #330;
|
||||
}
|
||||
.status-running {
|
||||
background: #003;
|
||||
}
|
||||
.status-completed {
|
||||
background: #030;
|
||||
}
|
||||
.raw-data {
|
||||
background: #222;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 11px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
button {
|
||||
background: #0f0;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="debug-app" class="container">
|
||||
<h1>🔧 AI Agent 调试监控</h1>
|
||||
|
||||
<!-- 连接状态 -->
|
||||
<div class="panel">
|
||||
<h3>连接状态</h3>
|
||||
<div>Socket连接: <span :style="{color: isConnected ? '#0f0' : '#f00'}">{{ isConnected ? '已连接' : '未连接' }}</span></div>
|
||||
<div>当前消息索引: {{ currentMessageIndex }}</div>
|
||||
<div>消息总数: {{ messages.length }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="panel">
|
||||
<h3>控制面板</h3>
|
||||
<button @click="clearLogs">清除日志</button>
|
||||
<button @click="testMessage">发送测试消息</button>
|
||||
<button @click="exportData">导出数据</button>
|
||||
<label>
|
||||
<input type="checkbox" v-model="pauseUpdates"> 暂停更新
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 事件日志 -->
|
||||
<div class="panel">
|
||||
<h3>WebSocket事件流 (最新 {{ events.length }} 条)</h3>
|
||||
<div class="event-log">
|
||||
<div v-for="(event, idx) in events" :key="idx" class="event-item">
|
||||
<span class="timestamp">{{ event.time }}</span>
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span v-if="event.data">: {{ JSON.stringify(event.data).slice(0, 200) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前消息的Actions监控 -->
|
||||
<div class="panel" v-if="currentMessageIndex >= 0 && messages[currentMessageIndex]">
|
||||
<h3>当前消息Actions状态 (消息 #{{ currentMessageIndex }})</h3>
|
||||
<div class="actions-monitor">
|
||||
<div v-for="(action, idx) in messages[currentMessageIndex].actions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
:class="{
|
||||
'streaming-true': action.streaming,
|
||||
'status-running': action.tool && action.tool.status === 'running',
|
||||
'status-completed': action.tool && action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="action-type">Action #{{ idx }}: {{ action.type }}</div>
|
||||
<div v-if="action.type === 'text'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
<div v-if="action.type === 'tool'">
|
||||
工具: {{ action.tool.name }}<br>
|
||||
<span class="tool-status">状态: {{ action.tool.status }}</span><br>
|
||||
ID: {{ action.tool.id }}<br>
|
||||
有结果: {{ !!action.tool.result }}
|
||||
</div>
|
||||
<div v-if="action.type === 'thinking'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据查看 -->
|
||||
<div class="panel">
|
||||
<h3>最新消息原始数据</h3>
|
||||
<div class="raw-data" v-if="messages.length > 0">{{ JSON.stringify(messages[messages.length - 1], null, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
const debugApp = createApp({
|
||||
data() {
|
||||
return {
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
events: [],
|
||||
messages: [],
|
||||
currentMessageIndex: -1,
|
||||
pauseUpdates: false,
|
||||
maxEvents: 100
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initSocket() {
|
||||
this.socket = io('/', {
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// 监听所有事件
|
||||
const events = [
|
||||
'connect', 'disconnect', 'system_ready',
|
||||
'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end',
|
||||
'text_start', 'text_chunk', 'text_end',
|
||||
'tool_preparing', 'tool_hint', 'tool_start', 'tool_status',
|
||||
'tool_execution_start', 'tool_execution_end', 'update_action',
|
||||
'task_complete', 'error'
|
||||
];
|
||||
|
||||
events.forEach(eventName => {
|
||||
this.socket.on(eventName, (data) => {
|
||||
this.logEvent(eventName, data);
|
||||
this.handleEvent(eventName, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
logEvent(type, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
const event = {
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour12: false, milliseconds: true }),
|
||||
type: type,
|
||||
data: data
|
||||
};
|
||||
|
||||
this.events.unshift(event);
|
||||
if (this.events.length > this.maxEvents) {
|
||||
this.events = this.events.slice(0, this.maxEvents);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(eventName, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
// 处理消息相关事件
|
||||
switch(eventName) {
|
||||
case 'ai_message_start':
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
actions: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
this.currentMessageIndex = this.messages.length - 1;
|
||||
break;
|
||||
|
||||
case 'thinking_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'thinking',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'thinking_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const thinkingAction = msg.actions.find(a => a.type === 'thinking' && a.streaming);
|
||||
if (thinkingAction) {
|
||||
thinkingAction.streaming = false;
|
||||
console.log('思考结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'text',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_chunk':
|
||||
if (this.currentMessageIndex >= 0 && data.content) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.content += data.content;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.streaming = false;
|
||||
console.log('文本结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_preparing':
|
||||
case 'tool_hint':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'preparing',
|
||||
arguments: {},
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
// 查找准备中的工具或创建新的
|
||||
let tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.id === data.preparing_id || a.tool.id === data.preparing_id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = 'running';
|
||||
tool.tool.arguments = data.arguments;
|
||||
console.log('工具开始运行,状态改为running');
|
||||
} else {
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'running',
|
||||
arguments: data.arguments,
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_action':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.tool.id === data.id || a.tool.id === data.preparing_id || a.id === data.id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = data.status || 'completed';
|
||||
tool.tool.result = data.result;
|
||||
console.log(`工具完成,状态改为${tool.tool.status}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_complete':
|
||||
this.currentMessageIndex = -1;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
clearLogs() {
|
||||
this.events = [];
|
||||
this.messages = [];
|
||||
this.currentMessageIndex = -1;
|
||||
},
|
||||
|
||||
testMessage() {
|
||||
this.socket.emit('send_message', { message: '创建一个test.txt文件' });
|
||||
},
|
||||
|
||||
exportData() {
|
||||
const data = {
|
||||
events: this.events,
|
||||
messages: this.messages,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `debug-${Date.now()}.json`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
debugApp.mount('#debug-app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,591 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent System</title>
|
||||
|
||||
<!-- Vue 3 CDN -->
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>
|
||||
|
||||
<!-- Socket.IO Client (local copy) -->
|
||||
<script src="/static/vendor/socket.io.min.js"></script>
|
||||
|
||||
<!-- Marked.js for Markdown -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
||||
|
||||
<!-- Prism.js for code highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
|
||||
<!-- KaTeX for LaTeX (无defer,同步加载) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="!isConnected && messages.length === 0" style="text-align: center; padding: 50px;">
|
||||
<h2>正在连接服务器...</h2>
|
||||
<p>如果长时间无响应,请刷新页面</p>
|
||||
</div>
|
||||
|
||||
<!-- Main UI (只在连接后显示) -->
|
||||
<template v-else>
|
||||
<!-- 顶部状态栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo">🤖 AI Agent</span>
|
||||
<span class="project-path">{{ projectPath }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="thinking-mode">{{ thinkingMode }}</span>
|
||||
<span class="connection-status" :class="{ connected: isConnected }">
|
||||
<span class="status-dot" :class="{ active: isConnected }"></span>
|
||||
{{ isConnected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 新增:对话历史侧边栏(最左侧) -->
|
||||
<aside class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="conversation-header">
|
||||
<button @click="createNewConversation" class="new-conversation-btn" v-if="!sidebarCollapsed">
|
||||
<span class="btn-icon">+</span>
|
||||
<span class="btn-text">新建对话</span>
|
||||
</button>
|
||||
<button @click="toggleSidebar" class="toggle-sidebar-btn">
|
||||
<span v-if="sidebarCollapsed">☰</span>
|
||||
<span v-else>←</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="!sidebarCollapsed">
|
||||
<div class="conversation-search">
|
||||
<input v-model="searchQuery"
|
||||
@input="searchConversations"
|
||||
placeholder="搜索对话..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div v-if="conversationsLoading" class="loading-conversations">
|
||||
正在加载...
|
||||
</div>
|
||||
<div v-else-if="conversations.length === 0" class="no-conversations">
|
||||
暂无对话记录
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: conv.id === currentConversationId }"
|
||||
@click="loadConversation(conv.id)">
|
||||
<div class="conversation-title">{{ conv.title }}</div>
|
||||
<div class="conversation-meta">
|
||||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="conversation-counts">
|
||||
{{ conv.total_messages }}条消息
|
||||
<span v-if="conv.total_tools > 0"> · {{ conv.total_tools }}工具</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="conversation-actions" @click.stop>
|
||||
<button @click="deleteConversation(conv.id)"
|
||||
class="conversation-action-btn delete-btn"
|
||||
title="删除对话">
|
||||
×
|
||||
</button>
|
||||
<button @click="duplicateConversation(conv.id)"
|
||||
class="conversation-action-btn copy-btn"
|
||||
title="复制对话">
|
||||
⧉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMoreConversations" class="load-more">
|
||||
<button @click="loadMoreConversations"
|
||||
:disabled="loadingMoreConversations"
|
||||
class="load-more-btn">
|
||||
{{ loadingMoreConversations ? '载入中...' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="conversation-collapsed-spacer"></div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧文件树 -->
|
||||
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<button class="sidebar-view-toggle"
|
||||
@click="toggleTodoPanel"
|
||||
:title="showTodoList ? '查看项目文件' : '查看待办列表'">
|
||||
<span v-if="showTodoList">{{ fileEmoji }}</span>
|
||||
<span v-else>{{ todoEmoji }}</span>
|
||||
</button>
|
||||
<h3>{{ showTodoList ? (todoEmoji + ' 待办列表') : (fileEmoji + ' 项目文件') }}</h3>
|
||||
</div>
|
||||
<template v-if="showTodoList">
|
||||
<div class="todo-panel">
|
||||
<div v-if="!todoList" class="todo-empty">
|
||||
暂无待办列表
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="todo-task"
|
||||
v-for="task in (todoList.tasks || [])"
|
||||
:key="task.index"
|
||||
:class="{ done: task.status === 'done' }">
|
||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||||
<span class="todo-task-status">{{ formatTaskStatus(task) }}</span>
|
||||
</div>
|
||||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-tree" @contextmenu.prevent>
|
||||
<file-node
|
||||
v-for="node in fileTree"
|
||||
:key="node.path"
|
||||
:node="node"
|
||||
:level="0"
|
||||
:expanded-folders="expandedFolders"
|
||||
@toggle-folder="toggleFolder"
|
||||
@context-menu="showContextMenu"
|
||||
></file-node>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
|
||||
|
||||
<!-- 中间聊天区域 -->
|
||||
<main class="chat-container">
|
||||
<!-- 当前对话信息栏 -->
|
||||
<div class="current-conversation-info" v-if="currentConversationTitle">
|
||||
<span class="conversation-title-display">{{ currentConversationTitle }}</span>
|
||||
<span class="conversation-stats">
|
||||
<span class="message-count">{{ messages.length }}条消息</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Token区域包装器 -->
|
||||
<div class="token-wrapper" v-if="currentConversationId">
|
||||
<!-- Token统计显示面板 -->
|
||||
<div class="token-display-panel" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<div class="token-panel-content">
|
||||
<div class="token-stats">
|
||||
<div class="token-item">
|
||||
<span class="token-label">当前上下文</span>
|
||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-separator"></div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输入</span>
|
||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输出</span>
|
||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的切换按钮 -->
|
||||
<button @click="toggleTokenPanel" class="token-toggle-btn" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<span v-if="!tokenPanelCollapsed">▲</span>
|
||||
<span v-else>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages-area" ref="messagesArea">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
||||
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.role === 'user'" class="user-message">
|
||||
<div class="message-header">👤 用户</div>
|
||||
<div class="message-text">{{ msg.content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- AI消息 -->
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<div class="message-header">🤖 AI Assistant</div>
|
||||
|
||||
<!-- 按顺序显示所有actions -->
|
||||
<div v-for="(action, actionIndex) in msg.actions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show': action.streaming || action.type === 'text'
|
||||
}">
|
||||
|
||||
<!-- 思考块 -->
|
||||
<div v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks.has(`${index}-thinking-${actionIndex}`) }">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-thinking-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">🧠</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner thinking-content">
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.streaming" class="progress-indicator"></div>
|
||||
</div>
|
||||
|
||||
<!-- 文本输出块 -->
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content"
|
||||
:class="{ 'streaming-text': action.streaming }">
|
||||
<!-- 流式:实时渲染markdown但不包装代码块 -->
|
||||
<div v-if="action.streaming" v-html="renderMarkdown(action.content, true)"></div>
|
||||
<!-- 完成:完整渲染包含代码块包装 -->
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 追加内容占位 -->
|
||||
<div v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }">
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
✏️ 已写入 {{ action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)
|
||||
</template>
|
||||
<template v-else>
|
||||
❌ 向 {{ action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning" v-if="action.append?.forced">
|
||||
⚠️ 未检测到结束标记,请根据提示继续补充。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改内容占位 -->
|
||||
<div v-else-if="action.type === 'modify_payload'" class="modify-placeholder">
|
||||
<div class="modify-placeholder-content">
|
||||
🛠️ 已对 {{ action.modify?.path || '目标文件' }} 执行补丁
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined">
|
||||
· 共 {{ action.modify.total }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length">
|
||||
· 已完成 {{ action.modify.completed.length }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length">
|
||||
· 未完成 {{ action.modify.failed.length }} 处
|
||||
</span>
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.forced">
|
||||
⚠️ 未检测到结束标记,系统已在流结束时执行补丁。
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.failed && action.modify.failed.length">
|
||||
⚠️ 未完成的序号:{{ action.modify.failed.map(f => f.index || f).join('、') || action.modify.failed.join('、') }},请根据提示重新输出。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具块(修复版) -->
|
||||
<div v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block"
|
||||
:class="{
|
||||
expanded: expandedBlocks.has(`${index}-tool-${actionIndex}`),
|
||||
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
|
||||
completed: action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-tool-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<!-- 修复:传递完整的tool对象 -->
|
||||
<span class="tool-icon"
|
||||
:class="getToolAnimationClass(action.tool)">
|
||||
{{ getToolIcon(action.tool) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">
|
||||
{{ getToolStatusText(action.tool) }}
|
||||
</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
<!-- 根据工具类型显示不同内容 -->
|
||||
<div v-if="action.tool.name === 'web_search' && action.tool.result">
|
||||
<div class="search-meta">
|
||||
<div><strong>搜索内容:</strong>{{ action.tool.result.query || action.tool.arguments.query }}</div>
|
||||
<div><strong>主题:</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>时间范围:</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
|
||||
<div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url"><a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else-if="action.tool.name === 'run_python' && action.tool.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments.code }}</code></pre>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 修复:只在running状态显示进度条 -->
|
||||
<!-- 只在流式消息期间且工具运行时显示进度条 -->
|
||||
<div v-if="streamingMessage && (action.tool.status === 'preparing' || action.tool.status === 'running')"
|
||||
class="progress-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统消息 -->
|
||||
<div v-else class="system-message">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
|
||||
<button @click="toggleScrollLock" class="scroll-lock-btn">
|
||||
<svg v-if="autoScrollEnabled && !userScrolling" viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
|
||||
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 5v12" />
|
||||
<path d="M7 13l5 5 5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown.enter.ctrl="sendMessage"
|
||||
placeholder="输入消息... (Ctrl+Enter 发送)"
|
||||
class="message-input"
|
||||
:disabled="!isConnected || streamingMessage"
|
||||
rows="3">
|
||||
</textarea>
|
||||
<div class="input-actions">
|
||||
<div class="upload-control">
|
||||
<input type="file"
|
||||
ref="fileUploadInput"
|
||||
class="file-input-hidden"
|
||||
@change="handleFileSelected">
|
||||
<button type="button"
|
||||
class="btn upload-btn"
|
||||
@click="triggerFileUpload"
|
||||
:disabled="!isConnected || uploading">
|
||||
{{ uploading ? '上传中...' : '上传文件' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tool-dropdown" ref="toolDropdown">
|
||||
<button type="button"
|
||||
class="btn tool-btn"
|
||||
@click="toggleToolMenu"
|
||||
:disabled="!isConnected || toolSettingsLoading">
|
||||
工具
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu tool-menu" v-if="toolMenuOpen">
|
||||
<div class="tool-menu-status" v-if="toolSettingsLoading">
|
||||
正在同步工具状态...
|
||||
</div>
|
||||
<div v-else-if="toolSettings.length === 0" class="tool-menu-empty">
|
||||
暂无可控工具
|
||||
</div>
|
||||
<div v-else class="tool-menu-list">
|
||||
<div v-for="category in toolSettings"
|
||||
:key="category.id"
|
||||
class="tool-category-item"
|
||||
:class="{ disabled: !category.enabled }">
|
||||
<span class="tool-category-label">
|
||||
<span class="tool-category-icon">{{ toolCategoryEmoji(category.id) }}</span>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="menu-btn tool-category-toggle"
|
||||
@click="updateToolCategory(category.id, !category.enabled)"
|
||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading">
|
||||
{{ category.enabled ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="handleSendOrStop"
|
||||
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)"
|
||||
:class="['btn', streamingMessage ? 'stop-btn' : 'send-btn']">
|
||||
{{ streamingMessage ? '停止' : '发送' }}
|
||||
</button>
|
||||
<div class="settings-dropdown" ref="settingsDropdown">
|
||||
<button type="button"
|
||||
class="btn settings-btn"
|
||||
@click="toggleSettings"
|
||||
:disabled="!isConnected">
|
||||
设置
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu" v-if="settingsOpen">
|
||||
<button type="button"
|
||||
class="menu-btn focus-entry"
|
||||
@click="toggleFocusPanel"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
{{ rightCollapsed ? '展开聚焦面板' : '折叠聚焦面板' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn compress-entry"
|
||||
@click="compressConversation"
|
||||
:disabled="compressing || streamingMessage || !isConnected">
|
||||
{{ compressing ? '压缩中...' : '压缩' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn clear-entry"
|
||||
@click="clearChat"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||
|
||||
<!-- 右侧聚焦文件 -->
|
||||
<aside class="sidebar right-sidebar"
|
||||
:class="{ collapsed: rightCollapsed }"
|
||||
:style="{ width: rightCollapsed ? '0px' : rightWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<h3>👁️ 聚焦文件 ({{ Object.keys(focusedFiles).length }}/3)</h3>
|
||||
</div>
|
||||
<div class="focused-files" v-if="!rightCollapsed">
|
||||
<div v-if="Object.keys(focusedFiles).length === 0" class="no-files">
|
||||
暂无聚焦文件
|
||||
</div>
|
||||
<div v-else class="file-tabs">
|
||||
<div v-for="(file, path) in focusedFiles" :key="path" class="file-tab">
|
||||
<div class="tab-header">
|
||||
<span class="file-name">{{ path.split('/').pop() }}</span>
|
||||
<span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code :class="getLanguageClass(path)">{{ file.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu"
|
||||
v-if="contextMenu.visible"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click.stop>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'file'"
|
||||
@click.stop="downloadFile(contextMenu.node.path)">
|
||||
下载文件
|
||||
</button>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'folder'"
|
||||
:disabled="!contextMenu.node.path"
|
||||
@click.stop="downloadFolder(contextMenu.node.path)">
|
||||
下载压缩包
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 全局复制代码块函数
|
||||
function copyCodeBlock(blockId) {
|
||||
const codeElement = document.querySelector(`[data-code-id="${blockId}"]`);
|
||||
if (!codeElement) return;
|
||||
|
||||
const button = document.querySelector(`[data-code="${blockId}"]`);
|
||||
|
||||
// 如果正在显示"已复制"状态,忽略点击
|
||||
if (button.classList.contains('copied')) return;
|
||||
|
||||
// 使用保存的原始代码内容
|
||||
const codeContent = codeElement.dataset.originalCode || codeElement.textContent;
|
||||
|
||||
// 首次点击时保存原始图标
|
||||
if (!button.dataset.originalIcon) {
|
||||
button.dataset.originalIcon = button.textContent;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeContent).then(() => {
|
||||
button.textContent = '✓';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
// 使用保存的原始图标恢复
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 即使失败也要恢复状态
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
});
|
||||
}
|
||||
// 使用事件委托处理复制按钮点击
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('copy-code-btn')) {
|
||||
const blockId = e.target.getAttribute('data-code');
|
||||
if (blockId) copyCodeBlock(blockId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- 加载应用脚本 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f8f2ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 32px 28px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 18px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>AI Agent 登录</h1>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<button id="login-btn">登录</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
还没有账号?<a href="/register">点击注册</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('login-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
if (!email || !password) {
|
||||
errorEl.textContent = '请输入邮箱和密码';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>注册 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f0eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.register-card {
|
||||
width: 380px;
|
||||
padding: 32px 30px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 16px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-card">
|
||||
<h1>创建账号</h1>
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" placeholder="仅限小写字母/数字/下划线" autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invite">邀请码</label>
|
||||
<input type="text" id="invite" placeholder="必填" autocomplete="off" />
|
||||
</div>
|
||||
<button id="register-btn">注册</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
已有账号?<a href="/login">返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('register-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
async function register() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const invite = document.getElementById('invite').value.trim();
|
||||
|
||||
if (!username || !email || !password || !invite) {
|
||||
errorEl.textContent = '请完整填写所有字段';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password, invite_code: invite })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '注册失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请稍后再试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', register);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
register();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,803 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||||
|
||||
<!-- xterm.js 终端模拟器 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #ef4444;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 会话标签栏 */
|
||||
.session-tabs {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 终端容器 */
|
||||
.terminal-wrapper {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #2d2d2d;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.control-btn.close { background: #ff5f56; }
|
||||
.control-btn.minimize { background: #ffbd2e; }
|
||||
.control-btn.maximize { background: #27c93f; }
|
||||
|
||||
#terminal {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
padding-bottom: 30px; /* 增加底部内边距 */
|
||||
overflow-y: auto; /* 确保可以滚动 */
|
||||
}
|
||||
|
||||
/* 侧边栏信息 */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 命令历史 */
|
||||
.command-history {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #4ade80;
|
||||
border-left: 2px solid #4ade80;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* 底部状态栏 */
|
||||
.status-bar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-left {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loader {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>
|
||||
🖥️ AI Terminal Monitor
|
||||
<span class="status-indicator" id="connectionStatus"></span>
|
||||
<span style="font-size: 14px; font-weight: normal; margin-left: 10px;" id="connectionText">连接中...</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 会话标签 -->
|
||||
<div class="session-tabs" id="sessionTabs">
|
||||
<div class="tab" onclick="createNewSession()">
|
||||
<span class="tab-icon">➕</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 终端容器 -->
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">
|
||||
<span id="terminalSession">无活动会话</span>
|
||||
<span id="terminalPath" style="color: #666;">-</span>
|
||||
</div>
|
||||
<div class="terminal-controls">
|
||||
<div class="control-btn close" title="关闭"></div>
|
||||
<div class="control-btn minimize" title="最小化"></div>
|
||||
<div class="control-btn maximize" title="最大化"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<!-- 会话信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📊 会话信息</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">会话名称</span>
|
||||
<span class="info-value" id="sessionName">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">工作目录</span>
|
||||
<span class="info-value" id="workingDir">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Shell类型</span>
|
||||
<span class="info-value" id="shellType">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">运行时间</span>
|
||||
<span class="info-value" id="uptime">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">缓冲区</span>
|
||||
<span class="info-value" id="bufferSize">0 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 命令历史 -->
|
||||
<div class="info-section">
|
||||
<h3>📝 最近命令</h3>
|
||||
<div class="command-history" id="commandHistory">
|
||||
<div style="color: #666; text-align: center;">暂无命令</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📈 统计</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">总命令数</span>
|
||||
<span class="info-value" id="commandCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">输出行数</span>
|
||||
<span class="info-value" id="outputLines">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">活动会话</span>
|
||||
<span class="info-value" id="activeCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-item">
|
||||
<span>⚡</span>
|
||||
<span id="latency">延迟: 0ms</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>📡</span>
|
||||
<span id="dataRate">0 KB/s</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>🕐</span>
|
||||
<span id="currentTime"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<!-- 引入依赖 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let term = null;
|
||||
let fitAddon = null;
|
||||
let socket = null;
|
||||
let currentSession = null;
|
||||
let sessions = {};
|
||||
let commandHistory = [];
|
||||
let stats = {
|
||||
commandCount: 0,
|
||||
outputLines: 0,
|
||||
dataReceived: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// 初始化终端
|
||||
function initTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#aeafad',
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5'
|
||||
},
|
||||
scrollback: 10000,
|
||||
cols: 120, // 设置固定列数
|
||||
rows: 30, // 设置固定行数
|
||||
convertEol: true, // 转换行尾
|
||||
wordSeparator: ' ()[]{}\'"', // 单词分隔符
|
||||
rendererType: 'canvas', // 使用canvas渲染器
|
||||
allowTransparency: false
|
||||
});
|
||||
|
||||
// 添加插件
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
|
||||
// 打开终端
|
||||
term.open(document.getElementById('terminal'));
|
||||
|
||||
// 延迟执行fit以确保正确计算尺寸
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
console.log(`终端尺寸: ${term.cols}x${term.rows}`);
|
||||
}, 100);
|
||||
|
||||
// 显示欢迎信息
|
||||
term.writeln('\x1b[1;36m╔════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||
term.writeln('');
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
socket = io();
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket连接成功');
|
||||
updateConnectionStatus(true);
|
||||
term.writeln('\x1b[32m✓ 已连接到服务器\x1b[0m');
|
||||
|
||||
// 订阅所有终端事件
|
||||
socket.emit('terminal_subscribe', { all: true });
|
||||
console.log('已发送终端订阅请求');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('WebSocket连接断开');
|
||||
updateConnectionStatus(false);
|
||||
term.writeln('\x1b[31m✗ 与服务器断开连接\x1b[0m');
|
||||
});
|
||||
|
||||
// 订阅成功确认
|
||||
socket.on('terminal_subscribed', (data) => {
|
||||
console.log('终端订阅成功:', data);
|
||||
term.writeln('\x1b[32m✓ 已订阅终端事件\x1b[0m');
|
||||
});
|
||||
|
||||
// 终端启动事件
|
||||
socket.on('terminal_started', (data) => {
|
||||
console.log('终端启动:', data);
|
||||
addSession(data.session, data);
|
||||
term.writeln(`\x1b[32m[终端启动]\x1b[0m ${data.session} - ${data.working_dir}`);
|
||||
});
|
||||
|
||||
// 终端列表更新
|
||||
socket.on('terminal_list_update', (data) => {
|
||||
console.log('终端列表更新:', data);
|
||||
if (data.terminals && data.terminals.length > 0) {
|
||||
// 清除所有会话并重新添加
|
||||
sessions = {};
|
||||
for (const terminal of data.terminals) {
|
||||
sessions[terminal.name] = {
|
||||
working_dir: terminal.working_dir,
|
||||
is_running: terminal.is_running,
|
||||
shell: 'bash'
|
||||
};
|
||||
}
|
||||
updateSessionTabs();
|
||||
if (data.active && !currentSession) {
|
||||
switchToSession(data.active);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输出事件
|
||||
socket.on('terminal_output', (data) => {
|
||||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 直接写入原始输出
|
||||
term.write(data.data);
|
||||
stats.outputLines++;
|
||||
stats.dataReceived += data.data.length;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输入事件(AI发送的命令)
|
||||
socket.on('terminal_input', (data) => {
|
||||
console.log('收到终端输入:', data.session, data.data);
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 用绿色显示输入的命令
|
||||
term.write(`\x1b[1;32m➜ ${data.data}\x1b[0m`);
|
||||
|
||||
// 添加到命令历史
|
||||
addCommandToHistory(data.data.trim());
|
||||
stats.commandCount++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端关闭事件
|
||||
socket.on('terminal_closed', (data) => {
|
||||
console.log('终端关闭:', data);
|
||||
removeSession(data.session);
|
||||
term.writeln(`\x1b[31m[终端关闭]\x1b[0m ${data.session}`);
|
||||
});
|
||||
|
||||
// 终端重置事件
|
||||
socket.on('terminal_reset', (data) => {
|
||||
console.log('终端重置:', data);
|
||||
if (data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: data.working_dir || 'unknown'
|
||||
});
|
||||
} else {
|
||||
sessions[data.session].working_dir = data.working_dir || sessions[data.session].working_dir;
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||||
resetCommandHistory();
|
||||
stats.commandCount = 0;
|
||||
stats.outputLines = 0;
|
||||
stats.dataReceived = 0;
|
||||
stats.startTime = Date.now();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// 终端切换事件
|
||||
socket.on('terminal_switched', (data) => {
|
||||
console.log('终端切换:', data);
|
||||
switchToSession(data.current);
|
||||
});
|
||||
|
||||
// 调试:监听所有事件
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
console.log('Socket事件:', args[0], args[1]);
|
||||
onevent.call(this, packet);
|
||||
};
|
||||
}
|
||||
|
||||
// 添加会话
|
||||
function addSession(name, info) {
|
||||
sessions[name] = info;
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是第一个会话,自动切换
|
||||
if (!currentSession) {
|
||||
switchToSession(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除会话
|
||||
function removeSession(name) {
|
||||
delete sessions[name];
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是当前会话,切换到其他会话
|
||||
if (currentSession === name) {
|
||||
const remaining = Object.keys(sessions);
|
||||
if (remaining.length > 0) {
|
||||
switchToSession(remaining[0]);
|
||||
} else {
|
||||
currentSession = null;
|
||||
updateSessionInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function switchToSession(name) {
|
||||
currentSession = name;
|
||||
updateSessionTabs();
|
||||
updateSessionInfo();
|
||||
|
||||
// 清空终端并显示切换信息
|
||||
term.clear();
|
||||
term.writeln(`\x1b[36m[切换到终端: ${name}]\x1b[0m`);
|
||||
term.writeln('');
|
||||
|
||||
// 请求该会话的历史输出
|
||||
socket.emit('get_terminal_output', { session: name });
|
||||
}
|
||||
|
||||
// 更新会话标签
|
||||
function updateSessionTabs() {
|
||||
const container = document.getElementById('sessionTabs');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (Object.keys(sessions).length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="tab">
|
||||
<span class="tab-icon">⏳</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, info] of Object.entries(sessions)) {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = `tab ${name === currentSession ? 'active' : ''}`;
|
||||
tab.innerHTML = `
|
||||
<span class="tab-icon">📟</span>
|
||||
<span>${name}</span>
|
||||
`;
|
||||
tab.onclick = () => switchToSession(name);
|
||||
container.appendChild(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新会话信息
|
||||
function updateSessionInfo() {
|
||||
if (!currentSession || !sessions[currentSession]) {
|
||||
document.getElementById('sessionName').textContent = '-';
|
||||
document.getElementById('workingDir').textContent = '-';
|
||||
document.getElementById('shellType').textContent = '-';
|
||||
document.getElementById('terminalSession').textContent = '无活动会话';
|
||||
document.getElementById('terminalPath').textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
const info = sessions[currentSession];
|
||||
document.getElementById('sessionName').textContent = currentSession;
|
||||
document.getElementById('workingDir').textContent = info.working_dir || '-';
|
||||
document.getElementById('shellType').textContent = info.shell || '-';
|
||||
document.getElementById('terminalSession').textContent = currentSession;
|
||||
document.getElementById('terminalPath').textContent = info.working_dir || '-';
|
||||
}
|
||||
|
||||
// 添加命令到历史
|
||||
function addCommandToHistory(command) {
|
||||
commandHistory.unshift(command);
|
||||
if (commandHistory.length > 20) {
|
||||
commandHistory.pop();
|
||||
}
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
function resetCommandHistory() {
|
||||
commandHistory = [];
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
// 更新命令历史显示
|
||||
function updateCommandHistory() {
|
||||
const container = document.getElementById('commandHistory');
|
||||
if (commandHistory.length === 0) {
|
||||
container.innerHTML = '<div style="color: #666; text-align: center;">暂无命令</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = commandHistory
|
||||
.map(cmd => `<div class="command-item">${cmd}</div>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
document.getElementById('commandCount').textContent = stats.commandCount;
|
||||
document.getElementById('outputLines').textContent = stats.outputLines;
|
||||
document.getElementById('activeCount').textContent = Object.keys(sessions).length;
|
||||
document.getElementById('bufferSize').textContent = (stats.dataReceived / 1024).toFixed(1) + ' KB';
|
||||
|
||||
// 更新运行时间
|
||||
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
document.getElementById('uptime').textContent =
|
||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('connectionStatus');
|
||||
const text = document.getElementById('connectionText');
|
||||
|
||||
if (connected) {
|
||||
indicator.classList.remove('disconnected');
|
||||
text.textContent = '已连接';
|
||||
text.style.color = '#4ade80';
|
||||
} else {
|
||||
indicator.classList.add('disconnected');
|
||||
text.textContent = '未连接';
|
||||
text.style.color = '#ef4444';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('currentTime').textContent =
|
||||
now.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTerminal();
|
||||
initWebSocket();
|
||||
|
||||
// 定时更新
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
setInterval(updateStats, 1000);
|
||||
updateCurrentTime();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude颜色展示 - 简化版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--claude-bg: #eeece2;
|
||||
--claude-text: #3d3929;
|
||||
--claude-accent: #da7756;
|
||||
--claude-accent-strong: #bd5d3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
background-color: var(--claude-bg);
|
||||
color: var(--claude-text);
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 20px 0 10px 0;
|
||||
color: var(--claude-text);
|
||||
border-bottom: 2px solid var(--claude-accent);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.color-hex {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.text-sample {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
border-left: 3px solid var(--claude-accent);
|
||||
}
|
||||
|
||||
.button-demo {
|
||||
background: var(--claude-accent-strong);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chat-sample {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.user-msg {
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 0 12px;
|
||||
margin: 5px 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ai-msg {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 12px 0;
|
||||
margin: 5px 0;
|
||||
margin-right: 20px;
|
||||
border-left: 2px solid var(--claude-accent-strong);
|
||||
}
|
||||
|
||||
/* 颜色定义 */
|
||||
.bg-color { background-color: var(--claude-bg); }
|
||||
.text-color { background-color: var(--claude-text); }
|
||||
.logo-color { background-color: var(--claude-accent); }
|
||||
.button-color { background-color: var(--claude-accent-strong); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Claude聊天界面颜色</h1>
|
||||
|
||||
<h2>核心颜色</h2>
|
||||
<div class="color-card">
|
||||
<div class="color-swatch bg-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">背景颜色</div>
|
||||
<div class="color-hex">#eeece2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch text-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">字体颜色</div>
|
||||
<div class="color-hex">#3d3929</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch logo-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">品牌主色</div>
|
||||
<div class="color-hex">#da7756</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch button-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">按钮颜色</div>
|
||||
<div class="color-hex">#bd5d3a</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>字体样式</h2>
|
||||
<div class="text-sample">
|
||||
字体栈:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif
|
||||
<br><br>
|
||||
<i>斜体样式展示</i> | <b>粗体样式展示</b>
|
||||
</div>
|
||||
|
||||
<h2>界面元素</h2>
|
||||
<div class="text-sample">
|
||||
<a href="#" class="button-demo">主要按钮</a>
|
||||
<a href="#" class="button-demo" style="background: #da7756;">悬停效果</a>
|
||||
</div>
|
||||
|
||||
<h2>对话界面模拟</h2>
|
||||
<div class="chat-sample">
|
||||
<div class="user-msg">你好,Claude!今天天气怎么样?</div>
|
||||
<div class="ai-msg">你好!我无法获取实时天气信息,但你可以通过天气应用查看当地天气。</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sample">
|
||||
这种配色方案创造出温暖、专业的对话环境,背景是米白色(#eeece2),文字是深棕色(#3d3929),给人舒适友好的感觉。
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,399 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent 调试监控</title>
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.6.1/dist/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #1a1a1a;
|
||||
color: #0f0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.panel {
|
||||
background: #000;
|
||||
border: 1px solid #0f0;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.event-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.event-item {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.event-type {
|
||||
color: #ff0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
color: #888;
|
||||
}
|
||||
.actions-monitor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.action-card {
|
||||
border: 1px solid #444;
|
||||
padding: 5px;
|
||||
background: #111;
|
||||
}
|
||||
.action-type {
|
||||
color: #0ff;
|
||||
}
|
||||
.tool-status {
|
||||
color: #f0f;
|
||||
}
|
||||
.streaming-true {
|
||||
background: #330;
|
||||
}
|
||||
.status-running {
|
||||
background: #003;
|
||||
}
|
||||
.status-completed {
|
||||
background: #030;
|
||||
}
|
||||
.raw-data {
|
||||
background: #222;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 11px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
button {
|
||||
background: #0f0;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="debug-app" class="container">
|
||||
<h1>🔧 AI Agent 调试监控</h1>
|
||||
|
||||
<!-- 连接状态 -->
|
||||
<div class="panel">
|
||||
<h3>连接状态</h3>
|
||||
<div>Socket连接: <span :style="{color: isConnected ? '#0f0' : '#f00'}">{{ isConnected ? '已连接' : '未连接' }}</span></div>
|
||||
<div>当前消息索引: {{ currentMessageIndex }}</div>
|
||||
<div>消息总数: {{ messages.length }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="panel">
|
||||
<h3>控制面板</h3>
|
||||
<button @click="clearLogs">清除日志</button>
|
||||
<button @click="testMessage">发送测试消息</button>
|
||||
<button @click="exportData">导出数据</button>
|
||||
<label>
|
||||
<input type="checkbox" v-model="pauseUpdates"> 暂停更新
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 事件日志 -->
|
||||
<div class="panel">
|
||||
<h3>WebSocket事件流 (最新 {{ events.length }} 条)</h3>
|
||||
<div class="event-log">
|
||||
<div v-for="(event, idx) in events" :key="idx" class="event-item">
|
||||
<span class="timestamp">{{ event.time }}</span>
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span v-if="event.data">: {{ JSON.stringify(event.data).slice(0, 200) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前消息的Actions监控 -->
|
||||
<div class="panel" v-if="currentMessageIndex >= 0 && messages[currentMessageIndex]">
|
||||
<h3>当前消息Actions状态 (消息 #{{ currentMessageIndex }})</h3>
|
||||
<div class="actions-monitor">
|
||||
<div v-for="(action, idx) in messages[currentMessageIndex].actions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
:class="{
|
||||
'streaming-true': action.streaming,
|
||||
'status-running': action.tool && action.tool.status === 'running',
|
||||
'status-completed': action.tool && action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="action-type">Action #{{ idx }}: {{ action.type }}</div>
|
||||
<div v-if="action.type === 'text'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
<div v-if="action.type === 'tool'">
|
||||
工具: {{ action.tool.name }}<br>
|
||||
<span class="tool-status">状态: {{ action.tool.status }}</span><br>
|
||||
ID: {{ action.tool.id }}<br>
|
||||
有结果: {{ !!action.tool.result }}
|
||||
</div>
|
||||
<div v-if="action.type === 'thinking'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据查看 -->
|
||||
<div class="panel">
|
||||
<h3>最新消息原始数据</h3>
|
||||
<div class="raw-data" v-if="messages.length > 0">{{ JSON.stringify(messages[messages.length - 1], null, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
const debugApp = createApp({
|
||||
data() {
|
||||
return {
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
events: [],
|
||||
messages: [],
|
||||
currentMessageIndex: -1,
|
||||
pauseUpdates: false,
|
||||
maxEvents: 100
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initSocket() {
|
||||
this.socket = io('/', {
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// 监听所有事件
|
||||
const events = [
|
||||
'connect', 'disconnect', 'system_ready',
|
||||
'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end',
|
||||
'text_start', 'text_chunk', 'text_end',
|
||||
'tool_preparing', 'tool_hint', 'tool_start', 'tool_status',
|
||||
'tool_execution_start', 'tool_execution_end', 'update_action',
|
||||
'task_complete', 'error'
|
||||
];
|
||||
|
||||
events.forEach(eventName => {
|
||||
this.socket.on(eventName, (data) => {
|
||||
this.logEvent(eventName, data);
|
||||
this.handleEvent(eventName, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
logEvent(type, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
const event = {
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour12: false, milliseconds: true }),
|
||||
type: type,
|
||||
data: data
|
||||
};
|
||||
|
||||
this.events.unshift(event);
|
||||
if (this.events.length > this.maxEvents) {
|
||||
this.events = this.events.slice(0, this.maxEvents);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(eventName, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
// 处理消息相关事件
|
||||
switch(eventName) {
|
||||
case 'ai_message_start':
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
actions: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
this.currentMessageIndex = this.messages.length - 1;
|
||||
break;
|
||||
|
||||
case 'thinking_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'thinking',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'thinking_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const thinkingAction = msg.actions.find(a => a.type === 'thinking' && a.streaming);
|
||||
if (thinkingAction) {
|
||||
thinkingAction.streaming = false;
|
||||
console.log('思考结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'text',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_chunk':
|
||||
if (this.currentMessageIndex >= 0 && data.content) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.content += data.content;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.streaming = false;
|
||||
console.log('文本结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_preparing':
|
||||
case 'tool_hint':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'preparing',
|
||||
arguments: {},
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
// 查找准备中的工具或创建新的
|
||||
let tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.id === data.preparing_id || a.tool.id === data.preparing_id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = 'running';
|
||||
tool.tool.arguments = data.arguments;
|
||||
console.log('工具开始运行,状态改为running');
|
||||
} else {
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'running',
|
||||
arguments: data.arguments,
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_action':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.tool.id === data.id || a.tool.id === data.preparing_id || a.id === data.id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = data.status || 'completed';
|
||||
tool.tool.result = data.result;
|
||||
console.log(`工具完成,状态改为${tool.tool.status}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_complete':
|
||||
this.currentMessageIndex = -1;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
clearLogs() {
|
||||
this.events = [];
|
||||
this.messages = [];
|
||||
this.currentMessageIndex = -1;
|
||||
},
|
||||
|
||||
testMessage() {
|
||||
this.socket.emit('send_message', { message: '创建一个test.txt文件' });
|
||||
},
|
||||
|
||||
exportData() {
|
||||
const data = {
|
||||
events: this.events,
|
||||
messages: this.messages,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `debug-${Date.now()}.json`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
debugApp.mount('#debug-app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,591 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent System</title>
|
||||
|
||||
<!-- Vue 3 CDN -->
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>
|
||||
|
||||
<!-- Socket.IO Client(CDN优先,JS 内有多源兜底加载) -->
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js" defer></script>
|
||||
|
||||
<!-- Marked.js for Markdown -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
||||
|
||||
<!-- Prism.js for code highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
|
||||
<!-- KaTeX for LaTeX (无defer,同步加载) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="!isConnected && messages.length === 0" style="text-align: center; padding: 50px;">
|
||||
<h2>正在连接服务器...</h2>
|
||||
<p>如果长时间无响应,请刷新页面</p>
|
||||
</div>
|
||||
|
||||
<!-- Main UI (只在连接后显示) -->
|
||||
<template v-else>
|
||||
<!-- 顶部状态栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo">🤖 AI Agent</span>
|
||||
<span class="project-path">{{ projectPath }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="thinking-mode">{{ thinkingMode }}</span>
|
||||
<span class="connection-status" :class="{ connected: isConnected }">
|
||||
<span class="status-dot" :class="{ active: isConnected }"></span>
|
||||
{{ isConnected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 新增:对话历史侧边栏(最左侧) -->
|
||||
<aside class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="conversation-header">
|
||||
<button @click="createNewConversation" class="new-conversation-btn" v-if="!sidebarCollapsed">
|
||||
<span class="btn-icon">+</span>
|
||||
<span class="btn-text">新建对话</span>
|
||||
</button>
|
||||
<button @click="toggleSidebar" class="toggle-sidebar-btn">
|
||||
<span v-if="sidebarCollapsed">☰</span>
|
||||
<span v-else>←</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="!sidebarCollapsed">
|
||||
<div class="conversation-search">
|
||||
<input v-model="searchQuery"
|
||||
@input="searchConversations"
|
||||
placeholder="搜索对话..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div v-if="conversationsLoading" class="loading-conversations">
|
||||
正在加载...
|
||||
</div>
|
||||
<div v-else-if="conversations.length === 0" class="no-conversations">
|
||||
暂无对话记录
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: conv.id === currentConversationId }"
|
||||
@click="loadConversation(conv.id)">
|
||||
<div class="conversation-title">{{ conv.title }}</div>
|
||||
<div class="conversation-meta">
|
||||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="conversation-counts">
|
||||
{{ conv.total_messages }}条消息
|
||||
<span v-if="conv.total_tools > 0"> · {{ conv.total_tools }}工具</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="conversation-actions" @click.stop>
|
||||
<button @click="deleteConversation(conv.id)"
|
||||
class="conversation-action-btn delete-btn"
|
||||
title="删除对话">
|
||||
×
|
||||
</button>
|
||||
<button @click="duplicateConversation(conv.id)"
|
||||
class="conversation-action-btn copy-btn"
|
||||
title="复制对话">
|
||||
⧉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMoreConversations" class="load-more">
|
||||
<button @click="loadMoreConversations"
|
||||
:disabled="loadingMoreConversations"
|
||||
class="load-more-btn">
|
||||
{{ loadingMoreConversations ? '载入中...' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="conversation-collapsed-spacer"></div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧文件树 -->
|
||||
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<button class="sidebar-view-toggle"
|
||||
@click="toggleTodoPanel"
|
||||
:title="showTodoList ? '查看项目文件' : '查看待办列表'">
|
||||
<span v-if="showTodoList">{{ fileEmoji }}</span>
|
||||
<span v-else>{{ todoEmoji }}</span>
|
||||
</button>
|
||||
<h3>{{ showTodoList ? (todoEmoji + ' 待办列表') : (fileEmoji + ' 项目文件') }}</h3>
|
||||
</div>
|
||||
<template v-if="showTodoList">
|
||||
<div class="todo-panel">
|
||||
<div v-if="!todoList" class="todo-empty">
|
||||
暂无待办列表
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="todo-task"
|
||||
v-for="task in (todoList.tasks || [])"
|
||||
:key="task.index"
|
||||
:class="{ done: task.status === 'done' }">
|
||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||||
<span class="todo-task-status">{{ formatTaskStatus(task) }}</span>
|
||||
</div>
|
||||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-tree" @contextmenu.prevent>
|
||||
<file-node
|
||||
v-for="node in fileTree"
|
||||
:key="node.path"
|
||||
:node="node"
|
||||
:level="0"
|
||||
:expanded-folders="expandedFolders"
|
||||
@toggle-folder="toggleFolder"
|
||||
@context-menu="showContextMenu"
|
||||
></file-node>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
|
||||
|
||||
<!-- 中间聊天区域 -->
|
||||
<main class="chat-container">
|
||||
<!-- 当前对话信息栏 -->
|
||||
<div class="current-conversation-info" v-if="currentConversationTitle">
|
||||
<span class="conversation-title-display">{{ currentConversationTitle }}</span>
|
||||
<span class="conversation-stats">
|
||||
<span class="message-count">{{ messages.length }}条消息</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Token区域包装器 -->
|
||||
<div class="token-wrapper" v-if="currentConversationId">
|
||||
<!-- Token统计显示面板 -->
|
||||
<div class="token-display-panel" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<div class="token-panel-content">
|
||||
<div class="token-stats">
|
||||
<div class="token-item">
|
||||
<span class="token-label">当前上下文</span>
|
||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-separator"></div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输入</span>
|
||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输出</span>
|
||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的切换按钮 -->
|
||||
<button @click="toggleTokenPanel" class="token-toggle-btn" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<span v-if="!tokenPanelCollapsed">▲</span>
|
||||
<span v-else>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages-area" ref="messagesArea">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
||||
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.role === 'user'" class="user-message">
|
||||
<div class="message-header">👤 用户</div>
|
||||
<div class="message-text">{{ msg.content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- AI消息 -->
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<div class="message-header">🤖 AI Assistant</div>
|
||||
|
||||
<!-- 按顺序显示所有actions -->
|
||||
<div v-for="(action, actionIndex) in msg.actions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show': action.streaming || action.type === 'text'
|
||||
}">
|
||||
|
||||
<!-- 思考块 -->
|
||||
<div v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks.has(`${index}-thinking-${actionIndex}`) }">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-thinking-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">🧠</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner thinking-content">
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.streaming" class="progress-indicator"></div>
|
||||
</div>
|
||||
|
||||
<!-- 文本输出块 -->
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content"
|
||||
:class="{ 'streaming-text': action.streaming }">
|
||||
<!-- 流式:实时渲染markdown但不包装代码块 -->
|
||||
<div v-if="action.streaming" v-html="renderMarkdown(action.content, true)"></div>
|
||||
<!-- 完成:完整渲染包含代码块包装 -->
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 追加内容占位 -->
|
||||
<div v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }">
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
✏️ 已写入 {{ action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)
|
||||
</template>
|
||||
<template v-else>
|
||||
❌ 向 {{ action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning" v-if="action.append?.forced">
|
||||
⚠️ 未检测到结束标记,请根据提示继续补充。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改内容占位 -->
|
||||
<div v-else-if="action.type === 'modify_payload'" class="modify-placeholder">
|
||||
<div class="modify-placeholder-content">
|
||||
🛠️ 已对 {{ action.modify?.path || '目标文件' }} 执行补丁
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined">
|
||||
· 共 {{ action.modify.total }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length">
|
||||
· 已完成 {{ action.modify.completed.length }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length">
|
||||
· 未完成 {{ action.modify.failed.length }} 处
|
||||
</span>
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.forced">
|
||||
⚠️ 未检测到结束标记,系统已在流结束时执行补丁。
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.failed && action.modify.failed.length">
|
||||
⚠️ 未完成的序号:{{ action.modify.failed.map(f => f.index || f).join('、') || action.modify.failed.join('、') }},请根据提示重新输出。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具块(修复版) -->
|
||||
<div v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block"
|
||||
:class="{
|
||||
expanded: expandedBlocks.has(`${index}-tool-${actionIndex}`),
|
||||
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
|
||||
completed: action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-tool-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<!-- 修复:传递完整的tool对象 -->
|
||||
<span class="tool-icon"
|
||||
:class="getToolAnimationClass(action.tool)">
|
||||
{{ getToolIcon(action.tool) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">
|
||||
{{ getToolStatusText(action.tool) }}
|
||||
</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
<!-- 根据工具类型显示不同内容 -->
|
||||
<div v-if="action.tool.name === 'web_search' && action.tool.result">
|
||||
<div class="search-meta">
|
||||
<div><strong>搜索内容:</strong>{{ action.tool.result.query || action.tool.arguments.query }}</div>
|
||||
<div><strong>主题:</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>时间范围:</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
|
||||
<div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url"><a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else-if="action.tool.name === 'run_python' && action.tool.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments.code }}</code></pre>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 修复:只在running状态显示进度条 -->
|
||||
<!-- 只在流式消息期间且工具运行时显示进度条 -->
|
||||
<div v-if="streamingMessage && (action.tool.status === 'preparing' || action.tool.status === 'running')"
|
||||
class="progress-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统消息 -->
|
||||
<div v-else class="system-message">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
|
||||
<button @click="toggleScrollLock" class="scroll-lock-btn">
|
||||
<svg v-if="autoScrollEnabled && !userScrolling" viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
|
||||
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 5v12" />
|
||||
<path d="M7 13l5 5 5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown.enter.ctrl="sendMessage"
|
||||
placeholder="输入消息... (Ctrl+Enter 发送)"
|
||||
class="message-input"
|
||||
:disabled="!isConnected || streamingMessage"
|
||||
rows="3">
|
||||
</textarea>
|
||||
<div class="input-actions">
|
||||
<div class="upload-control">
|
||||
<input type="file"
|
||||
ref="fileUploadInput"
|
||||
class="file-input-hidden"
|
||||
@change="handleFileSelected">
|
||||
<button type="button"
|
||||
class="btn upload-btn"
|
||||
@click="triggerFileUpload"
|
||||
:disabled="!isConnected || uploading">
|
||||
{{ uploading ? '上传中...' : '上传文件' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tool-dropdown" ref="toolDropdown">
|
||||
<button type="button"
|
||||
class="btn tool-btn"
|
||||
@click="toggleToolMenu"
|
||||
:disabled="!isConnected || toolSettingsLoading">
|
||||
工具
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu tool-menu" v-if="toolMenuOpen">
|
||||
<div class="tool-menu-status" v-if="toolSettingsLoading">
|
||||
正在同步工具状态...
|
||||
</div>
|
||||
<div v-else-if="toolSettings.length === 0" class="tool-menu-empty">
|
||||
暂无可控工具
|
||||
</div>
|
||||
<div v-else class="tool-menu-list">
|
||||
<div v-for="category in toolSettings"
|
||||
:key="category.id"
|
||||
class="tool-category-item"
|
||||
:class="{ disabled: !category.enabled }">
|
||||
<span class="tool-category-label">
|
||||
<span class="tool-category-icon">{{ toolCategoryEmoji(category.id) }}</span>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="menu-btn tool-category-toggle"
|
||||
@click="updateToolCategory(category.id, !category.enabled)"
|
||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading">
|
||||
{{ category.enabled ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="handleSendOrStop"
|
||||
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)"
|
||||
:class="['btn', streamingMessage ? 'stop-btn' : 'send-btn']">
|
||||
{{ streamingMessage ? '停止' : '发送' }}
|
||||
</button>
|
||||
<div class="settings-dropdown" ref="settingsDropdown">
|
||||
<button type="button"
|
||||
class="btn settings-btn"
|
||||
@click="toggleSettings"
|
||||
:disabled="!isConnected">
|
||||
设置
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu" v-if="settingsOpen">
|
||||
<button type="button"
|
||||
class="menu-btn focus-entry"
|
||||
@click="toggleFocusPanel"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
{{ rightCollapsed ? '展开聚焦面板' : '折叠聚焦面板' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn compress-entry"
|
||||
@click="compressConversation"
|
||||
:disabled="compressing || streamingMessage || !isConnected">
|
||||
{{ compressing ? '压缩中...' : '压缩' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn clear-entry"
|
||||
@click="clearChat"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||
|
||||
<!-- 右侧聚焦文件 -->
|
||||
<aside class="sidebar right-sidebar"
|
||||
:class="{ collapsed: rightCollapsed }"
|
||||
:style="{ width: rightCollapsed ? '0px' : rightWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<h3>👁️ 聚焦文件 ({{ Object.keys(focusedFiles).length }}/3)</h3>
|
||||
</div>
|
||||
<div class="focused-files" v-if="!rightCollapsed">
|
||||
<div v-if="Object.keys(focusedFiles).length === 0" class="no-files">
|
||||
暂无聚焦文件
|
||||
</div>
|
||||
<div v-else class="file-tabs">
|
||||
<div v-for="(file, path) in focusedFiles" :key="path" class="file-tab">
|
||||
<div class="tab-header">
|
||||
<span class="file-name">{{ path.split('/').pop() }}</span>
|
||||
<span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code :class="getLanguageClass(path)">{{ file.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu"
|
||||
v-if="contextMenu.visible"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click.stop>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'file'"
|
||||
@click.stop="downloadFile(contextMenu.node.path)">
|
||||
下载文件
|
||||
</button>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'folder'"
|
||||
:disabled="!contextMenu.node.path"
|
||||
@click.stop="downloadFolder(contextMenu.node.path)">
|
||||
下载压缩包
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 全局复制代码块函数
|
||||
function copyCodeBlock(blockId) {
|
||||
const codeElement = document.querySelector(`[data-code-id="${blockId}"]`);
|
||||
if (!codeElement) return;
|
||||
|
||||
const button = document.querySelector(`[data-code="${blockId}"]`);
|
||||
|
||||
// 如果正在显示"已复制"状态,忽略点击
|
||||
if (button.classList.contains('copied')) return;
|
||||
|
||||
// 使用保存的原始代码内容
|
||||
const codeContent = codeElement.dataset.originalCode || codeElement.textContent;
|
||||
|
||||
// 首次点击时保存原始图标
|
||||
if (!button.dataset.originalIcon) {
|
||||
button.dataset.originalIcon = button.textContent;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeContent).then(() => {
|
||||
button.textContent = '✓';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
// 使用保存的原始图标恢复
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 即使失败也要恢复状态
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
});
|
||||
}
|
||||
// 使用事件委托处理复制按钮点击
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('copy-code-btn')) {
|
||||
const blockId = e.target.getAttribute('data-code');
|
||||
if (blockId) copyCodeBlock(blockId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- 加载应用脚本 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f8f2ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 32px 28px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 18px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>AI Agent 登录</h1>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<button id="login-btn">登录</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
还没有账号?<a href="/register">点击注册</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('login-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
if (!email || !password) {
|
||||
errorEl.textContent = '请输入邮箱和密码';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>注册 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f0eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.register-card {
|
||||
width: 380px;
|
||||
padding: 32px 30px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 16px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-card">
|
||||
<h1>创建账号</h1>
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" placeholder="仅限小写字母/数字/下划线" autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invite">邀请码</label>
|
||||
<input type="text" id="invite" placeholder="必填" autocomplete="off" />
|
||||
</div>
|
||||
<button id="register-btn">注册</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
已有账号?<a href="/login">返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('register-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
async function register() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const invite = document.getElementById('invite').value.trim();
|
||||
|
||||
if (!username || !email || !password || !invite) {
|
||||
errorEl.textContent = '请完整填写所有字段';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password, invite_code: invite })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '注册失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请稍后再试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', register);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
register();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,803 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||||
|
||||
<!-- xterm.js 终端模拟器 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #ef4444;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 会话标签栏 */
|
||||
.session-tabs {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 终端容器 */
|
||||
.terminal-wrapper {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #2d2d2d;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.control-btn.close { background: #ff5f56; }
|
||||
.control-btn.minimize { background: #ffbd2e; }
|
||||
.control-btn.maximize { background: #27c93f; }
|
||||
|
||||
#terminal {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
padding-bottom: 30px; /* 增加底部内边距 */
|
||||
overflow-y: auto; /* 确保可以滚动 */
|
||||
}
|
||||
|
||||
/* 侧边栏信息 */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 命令历史 */
|
||||
.command-history {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #4ade80;
|
||||
border-left: 2px solid #4ade80;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* 底部状态栏 */
|
||||
.status-bar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-left {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loader {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>
|
||||
🖥️ AI Terminal Monitor
|
||||
<span class="status-indicator" id="connectionStatus"></span>
|
||||
<span style="font-size: 14px; font-weight: normal; margin-left: 10px;" id="connectionText">连接中...</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 会话标签 -->
|
||||
<div class="session-tabs" id="sessionTabs">
|
||||
<div class="tab" onclick="createNewSession()">
|
||||
<span class="tab-icon">➕</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 终端容器 -->
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">
|
||||
<span id="terminalSession">无活动会话</span>
|
||||
<span id="terminalPath" style="color: #666;">-</span>
|
||||
</div>
|
||||
<div class="terminal-controls">
|
||||
<div class="control-btn close" title="关闭"></div>
|
||||
<div class="control-btn minimize" title="最小化"></div>
|
||||
<div class="control-btn maximize" title="最大化"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<!-- 会话信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📊 会话信息</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">会话名称</span>
|
||||
<span class="info-value" id="sessionName">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">工作目录</span>
|
||||
<span class="info-value" id="workingDir">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Shell类型</span>
|
||||
<span class="info-value" id="shellType">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">运行时间</span>
|
||||
<span class="info-value" id="uptime">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">缓冲区</span>
|
||||
<span class="info-value" id="bufferSize">0 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 命令历史 -->
|
||||
<div class="info-section">
|
||||
<h3>📝 最近命令</h3>
|
||||
<div class="command-history" id="commandHistory">
|
||||
<div style="color: #666; text-align: center;">暂无命令</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📈 统计</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">总命令数</span>
|
||||
<span class="info-value" id="commandCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">输出行数</span>
|
||||
<span class="info-value" id="outputLines">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">活动会话</span>
|
||||
<span class="info-value" id="activeCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-item">
|
||||
<span>⚡</span>
|
||||
<span id="latency">延迟: 0ms</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>📡</span>
|
||||
<span id="dataRate">0 KB/s</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>🕐</span>
|
||||
<span id="currentTime"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<!-- 引入依赖 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let term = null;
|
||||
let fitAddon = null;
|
||||
let socket = null;
|
||||
let currentSession = null;
|
||||
let sessions = {};
|
||||
let commandHistory = [];
|
||||
let stats = {
|
||||
commandCount: 0,
|
||||
outputLines: 0,
|
||||
dataReceived: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// 初始化终端
|
||||
function initTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#aeafad',
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5'
|
||||
},
|
||||
scrollback: 10000,
|
||||
cols: 120, // 设置固定列数
|
||||
rows: 30, // 设置固定行数
|
||||
convertEol: true, // 转换行尾
|
||||
wordSeparator: ' ()[]{}\'"', // 单词分隔符
|
||||
rendererType: 'canvas', // 使用canvas渲染器
|
||||
allowTransparency: false
|
||||
});
|
||||
|
||||
// 添加插件
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
|
||||
// 打开终端
|
||||
term.open(document.getElementById('terminal'));
|
||||
|
||||
// 延迟执行fit以确保正确计算尺寸
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
console.log(`终端尺寸: ${term.cols}x${term.rows}`);
|
||||
}, 100);
|
||||
|
||||
// 显示欢迎信息
|
||||
term.writeln('\x1b[1;36m╔════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||
term.writeln('');
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
socket = io();
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket连接成功');
|
||||
updateConnectionStatus(true);
|
||||
term.writeln('\x1b[32m✓ 已连接到服务器\x1b[0m');
|
||||
|
||||
// 订阅所有终端事件
|
||||
socket.emit('terminal_subscribe', { all: true });
|
||||
console.log('已发送终端订阅请求');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('WebSocket连接断开');
|
||||
updateConnectionStatus(false);
|
||||
term.writeln('\x1b[31m✗ 与服务器断开连接\x1b[0m');
|
||||
});
|
||||
|
||||
// 订阅成功确认
|
||||
socket.on('terminal_subscribed', (data) => {
|
||||
console.log('终端订阅成功:', data);
|
||||
term.writeln('\x1b[32m✓ 已订阅终端事件\x1b[0m');
|
||||
});
|
||||
|
||||
// 终端启动事件
|
||||
socket.on('terminal_started', (data) => {
|
||||
console.log('终端启动:', data);
|
||||
addSession(data.session, data);
|
||||
term.writeln(`\x1b[32m[终端启动]\x1b[0m ${data.session} - ${data.working_dir}`);
|
||||
});
|
||||
|
||||
// 终端列表更新
|
||||
socket.on('terminal_list_update', (data) => {
|
||||
console.log('终端列表更新:', data);
|
||||
if (data.terminals && data.terminals.length > 0) {
|
||||
// 清除所有会话并重新添加
|
||||
sessions = {};
|
||||
for (const terminal of data.terminals) {
|
||||
sessions[terminal.name] = {
|
||||
working_dir: terminal.working_dir,
|
||||
is_running: terminal.is_running,
|
||||
shell: 'bash'
|
||||
};
|
||||
}
|
||||
updateSessionTabs();
|
||||
if (data.active && !currentSession) {
|
||||
switchToSession(data.active);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输出事件
|
||||
socket.on('terminal_output', (data) => {
|
||||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 直接写入原始输出
|
||||
term.write(data.data);
|
||||
stats.outputLines++;
|
||||
stats.dataReceived += data.data.length;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输入事件(AI发送的命令)
|
||||
socket.on('terminal_input', (data) => {
|
||||
console.log('收到终端输入:', data.session, data.data);
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 用绿色显示输入的命令
|
||||
term.write(`\x1b[1;32m➜ ${data.data}\x1b[0m`);
|
||||
|
||||
// 添加到命令历史
|
||||
addCommandToHistory(data.data.trim());
|
||||
stats.commandCount++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端关闭事件
|
||||
socket.on('terminal_closed', (data) => {
|
||||
console.log('终端关闭:', data);
|
||||
removeSession(data.session);
|
||||
term.writeln(`\x1b[31m[终端关闭]\x1b[0m ${data.session}`);
|
||||
});
|
||||
|
||||
// 终端重置事件
|
||||
socket.on('terminal_reset', (data) => {
|
||||
console.log('终端重置:', data);
|
||||
if (data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: data.working_dir || 'unknown'
|
||||
});
|
||||
} else {
|
||||
sessions[data.session].working_dir = data.working_dir || sessions[data.session].working_dir;
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||||
resetCommandHistory();
|
||||
stats.commandCount = 0;
|
||||
stats.outputLines = 0;
|
||||
stats.dataReceived = 0;
|
||||
stats.startTime = Date.now();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// 终端切换事件
|
||||
socket.on('terminal_switched', (data) => {
|
||||
console.log('终端切换:', data);
|
||||
switchToSession(data.current);
|
||||
});
|
||||
|
||||
// 调试:监听所有事件
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
console.log('Socket事件:', args[0], args[1]);
|
||||
onevent.call(this, packet);
|
||||
};
|
||||
}
|
||||
|
||||
// 添加会话
|
||||
function addSession(name, info) {
|
||||
sessions[name] = info;
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是第一个会话,自动切换
|
||||
if (!currentSession) {
|
||||
switchToSession(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除会话
|
||||
function removeSession(name) {
|
||||
delete sessions[name];
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是当前会话,切换到其他会话
|
||||
if (currentSession === name) {
|
||||
const remaining = Object.keys(sessions);
|
||||
if (remaining.length > 0) {
|
||||
switchToSession(remaining[0]);
|
||||
} else {
|
||||
currentSession = null;
|
||||
updateSessionInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function switchToSession(name) {
|
||||
currentSession = name;
|
||||
updateSessionTabs();
|
||||
updateSessionInfo();
|
||||
|
||||
// 清空终端并显示切换信息
|
||||
term.clear();
|
||||
term.writeln(`\x1b[36m[切换到终端: ${name}]\x1b[0m`);
|
||||
term.writeln('');
|
||||
|
||||
// 请求该会话的历史输出
|
||||
socket.emit('get_terminal_output', { session: name });
|
||||
}
|
||||
|
||||
// 更新会话标签
|
||||
function updateSessionTabs() {
|
||||
const container = document.getElementById('sessionTabs');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (Object.keys(sessions).length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="tab">
|
||||
<span class="tab-icon">⏳</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, info] of Object.entries(sessions)) {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = `tab ${name === currentSession ? 'active' : ''}`;
|
||||
tab.innerHTML = `
|
||||
<span class="tab-icon">📟</span>
|
||||
<span>${name}</span>
|
||||
`;
|
||||
tab.onclick = () => switchToSession(name);
|
||||
container.appendChild(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新会话信息
|
||||
function updateSessionInfo() {
|
||||
if (!currentSession || !sessions[currentSession]) {
|
||||
document.getElementById('sessionName').textContent = '-';
|
||||
document.getElementById('workingDir').textContent = '-';
|
||||
document.getElementById('shellType').textContent = '-';
|
||||
document.getElementById('terminalSession').textContent = '无活动会话';
|
||||
document.getElementById('terminalPath').textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
const info = sessions[currentSession];
|
||||
document.getElementById('sessionName').textContent = currentSession;
|
||||
document.getElementById('workingDir').textContent = info.working_dir || '-';
|
||||
document.getElementById('shellType').textContent = info.shell || '-';
|
||||
document.getElementById('terminalSession').textContent = currentSession;
|
||||
document.getElementById('terminalPath').textContent = info.working_dir || '-';
|
||||
}
|
||||
|
||||
// 添加命令到历史
|
||||
function addCommandToHistory(command) {
|
||||
commandHistory.unshift(command);
|
||||
if (commandHistory.length > 20) {
|
||||
commandHistory.pop();
|
||||
}
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
function resetCommandHistory() {
|
||||
commandHistory = [];
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
// 更新命令历史显示
|
||||
function updateCommandHistory() {
|
||||
const container = document.getElementById('commandHistory');
|
||||
if (commandHistory.length === 0) {
|
||||
container.innerHTML = '<div style="color: #666; text-align: center;">暂无命令</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = commandHistory
|
||||
.map(cmd => `<div class="command-item">${cmd}</div>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
document.getElementById('commandCount').textContent = stats.commandCount;
|
||||
document.getElementById('outputLines').textContent = stats.outputLines;
|
||||
document.getElementById('activeCount').textContent = Object.keys(sessions).length;
|
||||
document.getElementById('bufferSize').textContent = (stats.dataReceived / 1024).toFixed(1) + ' KB';
|
||||
|
||||
// 更新运行时间
|
||||
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
document.getElementById('uptime').textContent =
|
||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('connectionStatus');
|
||||
const text = document.getElementById('connectionText');
|
||||
|
||||
if (connected) {
|
||||
indicator.classList.remove('disconnected');
|
||||
text.textContent = '已连接';
|
||||
text.style.color = '#4ade80';
|
||||
} else {
|
||||
indicator.classList.add('disconnected');
|
||||
text.textContent = '未连接';
|
||||
text.style.color = '#ef4444';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('currentTime').textContent =
|
||||
now.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTerminal();
|
||||
initWebSocket();
|
||||
|
||||
// 定时更新
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
setInterval(updateStats, 1000);
|
||||
updateCurrentTime();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -56,8 +56,4 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
||||
label="待办事项",
|
||||
tools=["todo_create", "todo_update_task", "todo_finish", "todo_finish_confirm"],
|
||||
),
|
||||
"sub_agent": ToolCategory(
|
||||
label="子智能体",
|
||||
tools=["create_sub_agent", "wait_sub_agent"],
|
||||
),
|
||||
}
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
你是子智能体 {agent_id}(任务编号 {task_id})。你与主智能体完全隔离,必须遵守以下规则:
|
||||
你是子智能体 {agent_id}(任务编号 {task_id}),与主智能体完全隔离,唯一的输入来源就是本任务描述与 `references/` 中的只读资料。你当前的工作区结构如下:
|
||||
|
||||
1. 只能在工作目录 `{workspace}` 内读写文件;该目录已准备好可写的 `deliverables/` 与只读的 `references/`。
|
||||
2. 所有交付成果必须放入 `{deliverables}`,并维护一份 `result.md`,用中文或中英双语说明完成情况、交付列表、后续建议。
|
||||
3. 可在 `references/` 内查阅主智能体提供的快照,但不得修改这些文件。
|
||||
4. 不得调用任何记忆或待办事项相关工具。
|
||||
5. 只有在所有交付准备完毕且 `result.md` 填写完成后,才能调用 `finish_sub_agent` 工具结束任务。
|
||||
- 工作区:`{workspace}` —— 只能在此路径内创建/修改文件。
|
||||
- 参考目录:`{references}` —— 主智能体提供的上下文快照,严格只读,用于查阅需求、接口或示例实现。
|
||||
- 交付目录:`{deliverables}` —— 必须放置交付成果与 `result.md`;主系统会把整个交付目录复制到 `{target_project_dir}` 下对应的 `*_deliverables` 文件夹。
|
||||
|
||||
请严格遵守以下原则:
|
||||
1. **明确职责**:根据“任务摘要/详细任务”在工作区内独立完成实现,无权回问主智能体或等待额外说明;若信息不足,应在 `result.md` 中说明局限。
|
||||
2. **参考使用**:需要引用 `references/` 中的文件时,复制或转述其必要片段,不可修改原文件;引用时注明来源,避免与交付混淆。
|
||||
3. **交付规范**:`deliverables/` 至少包含:
|
||||
- `result.md`:总结完成度、关键实现、测试/验收情况、未解决问题与后续建议(中文或中英双语)。
|
||||
- 任务成果文件:遵循约定的目录/命名,可附 README 或使用说明,确保主智能体复制后即可查阅。
|
||||
4. **禁止事项**:不得调用记忆/待办类工具,不得越出工作区,也不要尝试联系主智能体;所有说明请写入正常回复或 `result.md`。
|
||||
5. **流程要求**:先复盘任务、列出执行计划,再分步骤完成并自检。需要运行脚本或命令时务必记录要点,方便主智能体验收。
|
||||
6. **完成条件**:确认所有交付文件就绪且 `result.md` 信息完整后,才能调用 `finish_sub_agent`,并在 `reason` 中概括完成情况与下一步建议。
|
||||
|
||||
任务摘要:{summary}
|
||||
详细任务:{task}
|
||||
交付目标目录:{target_project_dir}
|
||||
|
||||
请分阶段规划、执行、验证。必要时使用终端运行命令并记录关键日志。遇到阻塞或需要澄清时,先在消息中说明,再等待主智能体指令。
|
||||
请在隔离环境中独立完成任务,遇到阻塞时在消息与 `result.md` 中说明原因及建议,而不是等待主智能体回应。
|
||||
|
||||
@ -1,168 +1,35 @@
|
||||
# 待办事项系统使用指南(简化版)
|
||||
# 子智能体待办事项速记
|
||||
|
||||
待办事项就像你的任务清单,帮你把复杂的工作拆成小步骤。
|
||||
你无法向主智能体提问,只能依靠待办清单来规划和追踪工作。清单越精炼、越可执行,你就越能掌控节奏。
|
||||
|
||||
## 什么时候用
|
||||
## 何时创建
|
||||
- 任务包含 2 步以上且每步需要独立确认。
|
||||
- 需要同时操作多个文件/工具,容易遗漏。
|
||||
- 需要自查进度或复盘交付内容。
|
||||
|
||||
以下情况建议创建待办清单:
|
||||
- ✅ 任务需要2步以上
|
||||
- ✅ 要操作多个文件
|
||||
- ✅ 需要用多个工具配合
|
||||
- ✅ 不确定具体怎么做,需要先规划
|
||||
## 如何编写
|
||||
1. **概述**:一句话写清楚你正在完成的目标(≤50 字)。
|
||||
2. **任务项**:2~4 条即可,按执行顺序罗列。每条必须描述“对哪个对象做什么动作”,例如 “读取 sales.xlsx,统计月度汇总”。
|
||||
3. **粒度**:避免含糊词(“处理”、“完善”等);能在十分钟内完成的最小可执行步骤即可。
|
||||
|
||||
**例子**:
|
||||
- "帮我整理这些照片" → 需要:分类、重命名、压缩、打包
|
||||
- "写一份周报" → 需要:收集数据、整理内容、格式排版、生成PDF
|
||||
- "分析这个表格" → 需要:读取数据、清洗数据、统计分析、制图
|
||||
## 使用流程
|
||||
1. **先规划**:在创建清单前,用自然语言写下你准备执行的流程,让自己确认无遗漏。
|
||||
2. **todo_create**:把概述与任务数组一次性写对,创建后尽量不要反复删除重建。
|
||||
3. **todo_update_task**:每完成一项立刻勾选;若步骤发生变化,先写明原因再修改对应任务。
|
||||
4. **todo_finish**:所有任务完成后调用。若仍有未完项但必须停止,先调用 `todo_finish`,再用 `todo_finish_confirm` 说明原因与后续建议。
|
||||
|
||||
## 怎么创建清单
|
||||
|
||||
### 1. 概述(一句话说明目标)
|
||||
- **要求**:不超过50字
|
||||
- **包含**:做什么事、主要约束条件
|
||||
- **例子**:
|
||||
- ✅ "整理家庭照片,按年份分类并压缩,不超过2GB"
|
||||
- ❌ "处理照片"(太模糊)
|
||||
|
||||
### 2. 任务列表(最多4条)
|
||||
- **数量**:建议2-4条,最多不超过4条
|
||||
- **顺序**:按照实际操作顺序排列
|
||||
- **要求**:每条任务要说清楚具体做什么
|
||||
|
||||
**✅ 好的任务描述**:
|
||||
- "读取sales.xlsx文件,统计各月销售额"
|
||||
- "创建summary.txt文件,写入统计结果"
|
||||
- "用Python生成柱状图,保存为chart.png"
|
||||
- "整理所有文件到report文件夹"
|
||||
|
||||
**❌ 不好的任务描述**:
|
||||
- "处理数据"(不知道处理什么)
|
||||
- "优化文件"(不知道怎么优化)
|
||||
- "完善内容"(太模糊)
|
||||
|
||||
## 执行流程
|
||||
|
||||
### 第1步:先沟通
|
||||
创建清单前,要先:
|
||||
1. 复述理解的任务
|
||||
2. 说明计划怎么做
|
||||
3. 列出主要步骤
|
||||
4. 等用户确认
|
||||
|
||||
**例子**:
|
||||
## 编写示例
|
||||
```
|
||||
用户:"帮我整理这周的工作日志"
|
||||
你应该说:
|
||||
"我理解您想整理工作日志。我计划这样做:
|
||||
1. 读取所有日志文件
|
||||
2. 按时间排序合并
|
||||
3. 提取关键事项
|
||||
4. 生成一份汇总文档
|
||||
您看这样可以吗?"
|
||||
```
|
||||
|
||||
### 第2步:创建清单
|
||||
用户确认后,调用 `todo_create` 创建清单
|
||||
|
||||
### 第3步:逐项执行
|
||||
- 完成一项任务后,立即调用 `todo_update_task` 勾选
|
||||
- 如果发现计划需要调整,先告诉用户,再修改
|
||||
|
||||
### 第4步:结束清单
|
||||
- 全部完成:直接调用 `todo_finish`
|
||||
- 中途需要停止:说明原因,询问是否结束
|
||||
|
||||
## 常见场景示例
|
||||
|
||||
### 场景1:文档整理
|
||||
```
|
||||
概述:合并三个Word文档为一个PDF,统一格式
|
||||
任务1:读取doc1.docx、doc2.docx、doc3.docx
|
||||
任务2:统一字体和标题格式
|
||||
任务3:合并内容到report.docx
|
||||
任务4:转换为PDF并保存
|
||||
```
|
||||
|
||||
### 场景2:数据分析
|
||||
```
|
||||
概述:分析销售表格,生成月度报告图表
|
||||
任务1:读取sales.xlsx,提取本月数据
|
||||
任务2:计算总销售额和环比增长
|
||||
任务3:用Python生成折线图和柱状图
|
||||
任务4:整理结果到report文件夹
|
||||
```
|
||||
|
||||
### 场景3:批量处理
|
||||
```
|
||||
概述:重命名photos文件夹的照片,按日期排序
|
||||
任务1:扫描photos文件夹所有jpg文件
|
||||
任务2:读取照片拍摄日期
|
||||
任务3:按"YYYYMMDD_序号.jpg"格式重命名
|
||||
任务4:移动到organized文件夹
|
||||
```
|
||||
|
||||
### 场景4:信息收集
|
||||
```
|
||||
概述:搜集人工智能相关资料并整理成文档
|
||||
任务1:搜索AI最新发展和应用案例
|
||||
任务2:提取3-5篇重要文章内容
|
||||
任务3:整理成结构化文档
|
||||
任务4:保存为ai_report.md
|
||||
概述:整理 physics_test 题解,生成 deliverables/result.md
|
||||
任务1:read_file physics_problems.txt,列出 5 道题
|
||||
任务2:在 workspace/solutions.md 中逐题写解答
|
||||
任务3:整理 result.md,概括完成情况与风险
|
||||
任务4:检查 deliverables/ 是否包含 result.md 与 solutions.md
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
- 只写你能够自主完成的步骤;不要写“等待主智能体确认”之类无法执行的任务。
|
||||
- 如果任务被新的发现打断,先在普通回复里说明,再用待办系统更新下一步。
|
||||
- 清单结束前必须保证 deliverables/ 与 result.md 已同步更新,否则不要 finish。
|
||||
|
||||
### ✅ 应该做的
|
||||
- 任务之间有清晰的先后顺序
|
||||
- 每个任务可以独立完成
|
||||
- 任务描述具体明确
|
||||
- 完成一项立即勾选
|
||||
|
||||
### ❌ 不应该做的
|
||||
- 不要把"先草稿后修改"分成两个任务(一次做完)
|
||||
- 不要创建重复的清单(已有清单就延续使用)
|
||||
- 不要跳过步骤(按顺序执行)
|
||||
- 不要忘记勾选已完成的任务
|
||||
|
||||
## 如果任务未完成就要结束
|
||||
|
||||
有时候会遇到:
|
||||
- 缺少必要信息,无法继续
|
||||
- 发现技术限制,做不了
|
||||
- 用户改变想法,不做了
|
||||
|
||||
**正确做法**:
|
||||
1. 调用 `todo_finish` 尝试结束
|
||||
2. 系统会提示有未完成任务
|
||||
3. 调用 `todo_finish_confirm` 并说明原因
|
||||
4. 告诉用户哪些完成了,哪些没做
|
||||
|
||||
**例子**:
|
||||
```
|
||||
"由于xxx文件找不到,任务2无法执行。
|
||||
已完成:任务1(读取文件)
|
||||
未完成:任务2-4
|
||||
是否结束当前清单?"
|
||||
```
|
||||
|
||||
## 快速参考
|
||||
|
||||
| 工具 | 用途 | 什么时候用 |
|
||||
|-----|------|---------|
|
||||
| todo_create | 创建清单 | 开始多步骤任务时 |
|
||||
| todo_update_task | 勾选任务 | 每完成一项任务后 |
|
||||
| todo_finish | 结束清单 | 全部任务完成时 |
|
||||
| todo_finish_confirm | 确认提前结束 | 有未完成任务但需要停止时 |
|
||||
|
||||
## 总结
|
||||
|
||||
待办事项系统的核心是:
|
||||
1. **确认需求**:对于复杂项目先和用户探讨
|
||||
2. **先想后做**:不要拿到任务就开始执行
|
||||
3. **明确指令**:在用户明确给出“好的,请开始”的指令时,才能开始创建待办事项
|
||||
4. **拆解清晰**:把大任务分成小步骤
|
||||
5. **及时反馈**:完成一步说一步
|
||||
6. **灵活调整**:发现问题及时沟通
|
||||
|
||||
记住:清单是给你自己看的,要给自己明确可执行的规划,同时要让用户知道你在做什么、完成到哪一步了。在用户明确给出“好的,请开始”的指令时,才能开始创建待办事项哦!
|
||||
|
||||
遵循以上规则能让子任务自洽、可追踪,也方便最终在 `result.md` 中回溯整个执行过程。***
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
以下为需更新的工具描述与提示词草案(供集成到 `define_tools` 及相关返回信息中使用):
|
||||
|
||||
---
|
||||
|
||||
### read_file
|
||||
- **描述**:`统一的阅读工具。通过 type 参数在 read(直接阅读)、search(全文搜索)、extract(按行抽取)之间切换,始终返回 UTF-8 文本。所有模式都会在响应前根据 max_chars 截断输出,保证不会超量。`
|
||||
- **模式提示**:
|
||||
- `read`:可选 `start_line`/`end_line`,适合一次性查看短片段。
|
||||
- `search`:需提供 `query`,并可设置 `max_matches`、`context_before`、`context_after`、`case_sensitive`,自动合并重复命中,以窗口形式返回。
|
||||
- `extract`:传入 `segments=[{start_line,end_line,label?},...]`,适合按多段行号提取关键信息。
|
||||
- **失败/限制提示**:若因编码或体积被拒绝,提醒:`文件不是 UTF-8 或体量过大,请改用 run_python(可结合 python-docx、pandas 等库)读取。` 若多次需要查看同一长文件,建议直接调用 `focus_file`。
|
||||
|
||||
### focus_file
|
||||
- **描述**:`持续在上下文中展示 UTF-8 文本文件的完整内容,适合频繁查看/修改的核心文件。文件非 UTF-8 或体积超限将直接拒绝;如需了解二进制/Office 文件,请改用 run_python。`
|
||||
|
||||
### run_python
|
||||
- **描述**:`执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片),或进行数据分析与验证。请在代码内显式读取文件并打印必要结果,避免长时间阻塞。`
|
||||
- **成功消息补充**:鼓励记录输出摘要,提醒用户脚本已在云端运行,可复用逻辑写入脚本文件。
|
||||
|
||||
### run_command
|
||||
- **描述**:`执行一次性命令并返回结果。适合快速查看文件信息(如 file/ls/stat/iconv)、转换编码、或调用 cli 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。若需要解析二进制/Office 文件,可结合 run_python 或专门命令行工具(如 xlsx2csv)。`
|
||||
|
||||
### terminal_session / terminal_input / terminal_reset
|
||||
- **描述补充**:强调云端多用户环境:`请在受限工作区内使用,避免切换到未经授权的路径。禁止启动需完整 TTY 的程序;如误触,请使用 terminal_reset 恢复,并说明原因。`
|
||||
|
||||
### web_search
|
||||
- **描述**:`在外部信息确实缺失时才调用的网络搜索工具。调用前应确认问题无法通过现有上下文解决,并向用户说明搜索目的。尽量精准撰写 query,合理设置时间/主题参数,避免无意义或重复搜索。`
|
||||
- **失败/过度使用提示**:当搜索结果空或不必要时返回建议:`考虑先梳理现有信息或向用户确认,再决定是否继续搜索。`
|
||||
|
||||
### extract_webpage / save_webpage
|
||||
- **描述**:`仅在 web_search 结果不足以提供所需细节时使用。提醒会显著增加 token 消耗,提取后应考虑落地为文本文件,并告知用户整理计划。`
|
||||
- **成功消息补充**:提示已获取的内容体量及后续处理建议(如“建议整理要点写入笔记或待办”)。
|
||||
|
||||
### todo_create / todo_update_task / todo_finish
|
||||
- **描述补充**:强调沟通闭环:`建/更/结单前需先向用户同步当前理解、风险及下一步。` `todo_finish` 在仍有未完成任务时要求提供剩余事项及后续建议。***
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude颜色展示 - 简化版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--claude-bg: #eeece2;
|
||||
--claude-text: #3d3929;
|
||||
--claude-accent: #da7756;
|
||||
--claude-accent-strong: #bd5d3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
background-color: var(--claude-bg);
|
||||
color: var(--claude-text);
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 20px 0 10px 0;
|
||||
color: var(--claude-text);
|
||||
border-bottom: 2px solid var(--claude-accent);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.color-hex {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.text-sample {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
border-left: 3px solid var(--claude-accent);
|
||||
}
|
||||
|
||||
.button-demo {
|
||||
background: var(--claude-accent-strong);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chat-sample {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.user-msg {
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 0 12px;
|
||||
margin: 5px 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ai-msg {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 12px 0;
|
||||
margin: 5px 0;
|
||||
margin-right: 20px;
|
||||
border-left: 2px solid var(--claude-accent-strong);
|
||||
}
|
||||
|
||||
/* 颜色定义 */
|
||||
.bg-color { background-color: var(--claude-bg); }
|
||||
.text-color { background-color: var(--claude-text); }
|
||||
.logo-color { background-color: var(--claude-accent); }
|
||||
.button-color { background-color: var(--claude-accent-strong); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Claude聊天界面颜色</h1>
|
||||
|
||||
<h2>核心颜色</h2>
|
||||
<div class="color-card">
|
||||
<div class="color-swatch bg-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">背景颜色</div>
|
||||
<div class="color-hex">#eeece2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch text-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">字体颜色</div>
|
||||
<div class="color-hex">#3d3929</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch logo-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">品牌主色</div>
|
||||
<div class="color-hex">#da7756</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch button-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">按钮颜色</div>
|
||||
<div class="color-hex">#bd5d3a</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>字体样式</h2>
|
||||
<div class="text-sample">
|
||||
字体栈:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif
|
||||
<br><br>
|
||||
<i>斜体样式展示</i> | <b>粗体样式展示</b>
|
||||
</div>
|
||||
|
||||
<h2>界面元素</h2>
|
||||
<div class="text-sample">
|
||||
<a href="#" class="button-demo">主要按钮</a>
|
||||
<a href="#" class="button-demo" style="background: #da7756;">悬停效果</a>
|
||||
</div>
|
||||
|
||||
<h2>对话界面模拟</h2>
|
||||
<div class="chat-sample">
|
||||
<div class="user-msg">你好,Claude!今天天气怎么样?</div>
|
||||
<div class="ai-msg">你好!我无法获取实时天气信息,但你可以通过天气应用查看当地天气。</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sample">
|
||||
这种配色方案创造出温暖、专业的对话环境,背景是米白色(#eeece2),文字是深棕色(#3d3929),给人舒适友好的感觉。
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,399 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent 调试监控</title>
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.6.1/dist/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #1a1a1a;
|
||||
color: #0f0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.panel {
|
||||
background: #000;
|
||||
border: 1px solid #0f0;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.event-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.event-item {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.event-type {
|
||||
color: #ff0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
color: #888;
|
||||
}
|
||||
.actions-monitor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.action-card {
|
||||
border: 1px solid #444;
|
||||
padding: 5px;
|
||||
background: #111;
|
||||
}
|
||||
.action-type {
|
||||
color: #0ff;
|
||||
}
|
||||
.tool-status {
|
||||
color: #f0f;
|
||||
}
|
||||
.streaming-true {
|
||||
background: #330;
|
||||
}
|
||||
.status-running {
|
||||
background: #003;
|
||||
}
|
||||
.status-completed {
|
||||
background: #030;
|
||||
}
|
||||
.raw-data {
|
||||
background: #222;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 11px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
button {
|
||||
background: #0f0;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="debug-app" class="container">
|
||||
<h1>🔧 AI Agent 调试监控</h1>
|
||||
|
||||
<!-- 连接状态 -->
|
||||
<div class="panel">
|
||||
<h3>连接状态</h3>
|
||||
<div>Socket连接: <span :style="{color: isConnected ? '#0f0' : '#f00'}">{{ isConnected ? '已连接' : '未连接' }}</span></div>
|
||||
<div>当前消息索引: {{ currentMessageIndex }}</div>
|
||||
<div>消息总数: {{ messages.length }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="panel">
|
||||
<h3>控制面板</h3>
|
||||
<button @click="clearLogs">清除日志</button>
|
||||
<button @click="testMessage">发送测试消息</button>
|
||||
<button @click="exportData">导出数据</button>
|
||||
<label>
|
||||
<input type="checkbox" v-model="pauseUpdates"> 暂停更新
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 事件日志 -->
|
||||
<div class="panel">
|
||||
<h3>WebSocket事件流 (最新 {{ events.length }} 条)</h3>
|
||||
<div class="event-log">
|
||||
<div v-for="(event, idx) in events" :key="idx" class="event-item">
|
||||
<span class="timestamp">{{ event.time }}</span>
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span v-if="event.data">: {{ JSON.stringify(event.data).slice(0, 200) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前消息的Actions监控 -->
|
||||
<div class="panel" v-if="currentMessageIndex >= 0 && messages[currentMessageIndex]">
|
||||
<h3>当前消息Actions状态 (消息 #{{ currentMessageIndex }})</h3>
|
||||
<div class="actions-monitor">
|
||||
<div v-for="(action, idx) in messages[currentMessageIndex].actions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
:class="{
|
||||
'streaming-true': action.streaming,
|
||||
'status-running': action.tool && action.tool.status === 'running',
|
||||
'status-completed': action.tool && action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="action-type">Action #{{ idx }}: {{ action.type }}</div>
|
||||
<div v-if="action.type === 'text'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
<div v-if="action.type === 'tool'">
|
||||
工具: {{ action.tool.name }}<br>
|
||||
<span class="tool-status">状态: {{ action.tool.status }}</span><br>
|
||||
ID: {{ action.tool.id }}<br>
|
||||
有结果: {{ !!action.tool.result }}
|
||||
</div>
|
||||
<div v-if="action.type === 'thinking'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据查看 -->
|
||||
<div class="panel">
|
||||
<h3>最新消息原始数据</h3>
|
||||
<div class="raw-data" v-if="messages.length > 0">{{ JSON.stringify(messages[messages.length - 1], null, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
const debugApp = createApp({
|
||||
data() {
|
||||
return {
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
events: [],
|
||||
messages: [],
|
||||
currentMessageIndex: -1,
|
||||
pauseUpdates: false,
|
||||
maxEvents: 100
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initSocket() {
|
||||
this.socket = io('/', {
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// 监听所有事件
|
||||
const events = [
|
||||
'connect', 'disconnect', 'system_ready',
|
||||
'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end',
|
||||
'text_start', 'text_chunk', 'text_end',
|
||||
'tool_preparing', 'tool_hint', 'tool_start', 'tool_status',
|
||||
'tool_execution_start', 'tool_execution_end', 'update_action',
|
||||
'task_complete', 'error'
|
||||
];
|
||||
|
||||
events.forEach(eventName => {
|
||||
this.socket.on(eventName, (data) => {
|
||||
this.logEvent(eventName, data);
|
||||
this.handleEvent(eventName, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
logEvent(type, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
const event = {
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour12: false, milliseconds: true }),
|
||||
type: type,
|
||||
data: data
|
||||
};
|
||||
|
||||
this.events.unshift(event);
|
||||
if (this.events.length > this.maxEvents) {
|
||||
this.events = this.events.slice(0, this.maxEvents);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(eventName, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
// 处理消息相关事件
|
||||
switch(eventName) {
|
||||
case 'ai_message_start':
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
actions: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
this.currentMessageIndex = this.messages.length - 1;
|
||||
break;
|
||||
|
||||
case 'thinking_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'thinking',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'thinking_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const thinkingAction = msg.actions.find(a => a.type === 'thinking' && a.streaming);
|
||||
if (thinkingAction) {
|
||||
thinkingAction.streaming = false;
|
||||
console.log('思考结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'text',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_chunk':
|
||||
if (this.currentMessageIndex >= 0 && data.content) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.content += data.content;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.streaming = false;
|
||||
console.log('文本结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_preparing':
|
||||
case 'tool_hint':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'preparing',
|
||||
arguments: {},
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
// 查找准备中的工具或创建新的
|
||||
let tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.id === data.preparing_id || a.tool.id === data.preparing_id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = 'running';
|
||||
tool.tool.arguments = data.arguments;
|
||||
console.log('工具开始运行,状态改为running');
|
||||
} else {
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'running',
|
||||
arguments: data.arguments,
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_action':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.tool.id === data.id || a.tool.id === data.preparing_id || a.id === data.id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = data.status || 'completed';
|
||||
tool.tool.result = data.result;
|
||||
console.log(`工具完成,状态改为${tool.tool.status}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_complete':
|
||||
this.currentMessageIndex = -1;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
clearLogs() {
|
||||
this.events = [];
|
||||
this.messages = [];
|
||||
this.currentMessageIndex = -1;
|
||||
},
|
||||
|
||||
testMessage() {
|
||||
this.socket.emit('send_message', { message: '创建一个test.txt文件' });
|
||||
},
|
||||
|
||||
exportData() {
|
||||
const data = {
|
||||
events: this.events,
|
||||
messages: this.messages,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `debug-${Date.now()}.json`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
debugApp.mount('#debug-app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,591 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent System</title>
|
||||
|
||||
<!-- Vue 3 CDN -->
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>
|
||||
|
||||
<!-- Socket.IO Client (local copy) -->
|
||||
<script src="/static/vendor/socket.io.min.js"></script>
|
||||
|
||||
<!-- Marked.js for Markdown -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
||||
|
||||
<!-- Prism.js for code highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
|
||||
<!-- KaTeX for LaTeX (无defer,同步加载) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="!isConnected && messages.length === 0" style="text-align: center; padding: 50px;">
|
||||
<h2>正在连接服务器...</h2>
|
||||
<p>如果长时间无响应,请刷新页面</p>
|
||||
</div>
|
||||
|
||||
<!-- Main UI (只在连接后显示) -->
|
||||
<template v-else>
|
||||
<!-- 顶部状态栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo">🤖 AI Agent</span>
|
||||
<span class="project-path">{{ projectPath }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="thinking-mode">{{ thinkingMode }}</span>
|
||||
<span class="connection-status" :class="{ connected: isConnected }">
|
||||
<span class="status-dot" :class="{ active: isConnected }"></span>
|
||||
{{ isConnected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 新增:对话历史侧边栏(最左侧) -->
|
||||
<aside class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="conversation-header">
|
||||
<button @click="createNewConversation" class="new-conversation-btn" v-if="!sidebarCollapsed">
|
||||
<span class="btn-icon">+</span>
|
||||
<span class="btn-text">新建对话</span>
|
||||
</button>
|
||||
<button @click="toggleSidebar" class="toggle-sidebar-btn">
|
||||
<span v-if="sidebarCollapsed">☰</span>
|
||||
<span v-else>←</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="!sidebarCollapsed">
|
||||
<div class="conversation-search">
|
||||
<input v-model="searchQuery"
|
||||
@input="searchConversations"
|
||||
placeholder="搜索对话..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div v-if="conversationsLoading" class="loading-conversations">
|
||||
正在加载...
|
||||
</div>
|
||||
<div v-else-if="conversations.length === 0" class="no-conversations">
|
||||
暂无对话记录
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: conv.id === currentConversationId }"
|
||||
@click="loadConversation(conv.id)">
|
||||
<div class="conversation-title">{{ conv.title }}</div>
|
||||
<div class="conversation-meta">
|
||||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="conversation-counts">
|
||||
{{ conv.total_messages }}条消息
|
||||
<span v-if="conv.total_tools > 0"> · {{ conv.total_tools }}工具</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="conversation-actions" @click.stop>
|
||||
<button @click="deleteConversation(conv.id)"
|
||||
class="conversation-action-btn delete-btn"
|
||||
title="删除对话">
|
||||
×
|
||||
</button>
|
||||
<button @click="duplicateConversation(conv.id)"
|
||||
class="conversation-action-btn copy-btn"
|
||||
title="复制对话">
|
||||
⧉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMoreConversations" class="load-more">
|
||||
<button @click="loadMoreConversations"
|
||||
:disabled="loadingMoreConversations"
|
||||
class="load-more-btn">
|
||||
{{ loadingMoreConversations ? '载入中...' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="conversation-collapsed-spacer"></div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧文件树 -->
|
||||
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<button class="sidebar-view-toggle"
|
||||
@click="toggleTodoPanel"
|
||||
:title="showTodoList ? '查看项目文件' : '查看待办列表'">
|
||||
<span v-if="showTodoList">{{ fileEmoji }}</span>
|
||||
<span v-else>{{ todoEmoji }}</span>
|
||||
</button>
|
||||
<h3>{{ showTodoList ? (todoEmoji + ' 待办列表') : (fileEmoji + ' 项目文件') }}</h3>
|
||||
</div>
|
||||
<template v-if="showTodoList">
|
||||
<div class="todo-panel">
|
||||
<div v-if="!todoList" class="todo-empty">
|
||||
暂无待办列表
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="todo-task"
|
||||
v-for="task in (todoList.tasks || [])"
|
||||
:key="task.index"
|
||||
:class="{ done: task.status === 'done' }">
|
||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||||
<span class="todo-task-status">{{ formatTaskStatus(task) }}</span>
|
||||
</div>
|
||||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-tree" @contextmenu.prevent>
|
||||
<file-node
|
||||
v-for="node in fileTree"
|
||||
:key="node.path"
|
||||
:node="node"
|
||||
:level="0"
|
||||
:expanded-folders="expandedFolders"
|
||||
@toggle-folder="toggleFolder"
|
||||
@context-menu="showContextMenu"
|
||||
></file-node>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
|
||||
|
||||
<!-- 中间聊天区域 -->
|
||||
<main class="chat-container">
|
||||
<!-- 当前对话信息栏 -->
|
||||
<div class="current-conversation-info" v-if="currentConversationTitle">
|
||||
<span class="conversation-title-display">{{ currentConversationTitle }}</span>
|
||||
<span class="conversation-stats">
|
||||
<span class="message-count">{{ messages.length }}条消息</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Token区域包装器 -->
|
||||
<div class="token-wrapper" v-if="currentConversationId">
|
||||
<!-- Token统计显示面板 -->
|
||||
<div class="token-display-panel" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<div class="token-panel-content">
|
||||
<div class="token-stats">
|
||||
<div class="token-item">
|
||||
<span class="token-label">当前上下文</span>
|
||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-separator"></div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输入</span>
|
||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输出</span>
|
||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的切换按钮 -->
|
||||
<button @click="toggleTokenPanel" class="token-toggle-btn" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<span v-if="!tokenPanelCollapsed">▲</span>
|
||||
<span v-else>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages-area" ref="messagesArea">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
||||
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.role === 'user'" class="user-message">
|
||||
<div class="message-header">👤 用户</div>
|
||||
<div class="message-text">{{ msg.content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- AI消息 -->
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<div class="message-header">🤖 AI Assistant</div>
|
||||
|
||||
<!-- 按顺序显示所有actions -->
|
||||
<div v-for="(action, actionIndex) in msg.actions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show': action.streaming || action.type === 'text'
|
||||
}">
|
||||
|
||||
<!-- 思考块 -->
|
||||
<div v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks.has(`${index}-thinking-${actionIndex}`) }">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-thinking-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">🧠</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner thinking-content">
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.streaming" class="progress-indicator"></div>
|
||||
</div>
|
||||
|
||||
<!-- 文本输出块 -->
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content"
|
||||
:class="{ 'streaming-text': action.streaming }">
|
||||
<!-- 流式:实时渲染markdown但不包装代码块 -->
|
||||
<div v-if="action.streaming" v-html="renderMarkdown(action.content, true)"></div>
|
||||
<!-- 完成:完整渲染包含代码块包装 -->
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 追加内容占位 -->
|
||||
<div v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }">
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
✏️ 已写入 {{ action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)
|
||||
</template>
|
||||
<template v-else>
|
||||
❌ 向 {{ action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning" v-if="action.append?.forced">
|
||||
⚠️ 未检测到结束标记,请根据提示继续补充。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改内容占位 -->
|
||||
<div v-else-if="action.type === 'modify_payload'" class="modify-placeholder">
|
||||
<div class="modify-placeholder-content">
|
||||
🛠️ 已对 {{ action.modify?.path || '目标文件' }} 执行补丁
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined">
|
||||
· 共 {{ action.modify.total }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length">
|
||||
· 已完成 {{ action.modify.completed.length }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length">
|
||||
· 未完成 {{ action.modify.failed.length }} 处
|
||||
</span>
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.forced">
|
||||
⚠️ 未检测到结束标记,系统已在流结束时执行补丁。
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.failed && action.modify.failed.length">
|
||||
⚠️ 未完成的序号:{{ action.modify.failed.map(f => f.index || f).join('、') || action.modify.failed.join('、') }},请根据提示重新输出。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具块(修复版) -->
|
||||
<div v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block"
|
||||
:class="{
|
||||
expanded: expandedBlocks.has(`${index}-tool-${actionIndex}`),
|
||||
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
|
||||
completed: action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-tool-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<!-- 修复:传递完整的tool对象 -->
|
||||
<span class="tool-icon"
|
||||
:class="getToolAnimationClass(action.tool)">
|
||||
{{ getToolIcon(action.tool) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">
|
||||
{{ getToolStatusText(action.tool) }}
|
||||
</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
<!-- 根据工具类型显示不同内容 -->
|
||||
<div v-if="action.tool.name === 'web_search' && action.tool.result">
|
||||
<div class="search-meta">
|
||||
<div><strong>搜索内容:</strong>{{ action.tool.result.query || action.tool.arguments.query }}</div>
|
||||
<div><strong>主题:</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>时间范围:</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
|
||||
<div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url"><a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else-if="action.tool.name === 'run_python' && action.tool.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments.code }}</code></pre>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 修复:只在running状态显示进度条 -->
|
||||
<!-- 只在流式消息期间且工具运行时显示进度条 -->
|
||||
<div v-if="streamingMessage && (action.tool.status === 'preparing' || action.tool.status === 'running')"
|
||||
class="progress-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统消息 -->
|
||||
<div v-else class="system-message">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
|
||||
<button @click="toggleScrollLock" class="scroll-lock-btn">
|
||||
<svg v-if="autoScrollEnabled && !userScrolling" viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
|
||||
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 5v12" />
|
||||
<path d="M7 13l5 5 5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown.enter.ctrl="sendMessage"
|
||||
placeholder="输入消息... (Ctrl+Enter 发送)"
|
||||
class="message-input"
|
||||
:disabled="!isConnected || streamingMessage"
|
||||
rows="3">
|
||||
</textarea>
|
||||
<div class="input-actions">
|
||||
<div class="upload-control">
|
||||
<input type="file"
|
||||
ref="fileUploadInput"
|
||||
class="file-input-hidden"
|
||||
@change="handleFileSelected">
|
||||
<button type="button"
|
||||
class="btn upload-btn"
|
||||
@click="triggerFileUpload"
|
||||
:disabled="!isConnected || uploading">
|
||||
{{ uploading ? '上传中...' : '上传文件' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tool-dropdown" ref="toolDropdown">
|
||||
<button type="button"
|
||||
class="btn tool-btn"
|
||||
@click="toggleToolMenu"
|
||||
:disabled="!isConnected || toolSettingsLoading">
|
||||
工具
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu tool-menu" v-if="toolMenuOpen">
|
||||
<div class="tool-menu-status" v-if="toolSettingsLoading">
|
||||
正在同步工具状态...
|
||||
</div>
|
||||
<div v-else-if="toolSettings.length === 0" class="tool-menu-empty">
|
||||
暂无可控工具
|
||||
</div>
|
||||
<div v-else class="tool-menu-list">
|
||||
<div v-for="category in toolSettings"
|
||||
:key="category.id"
|
||||
class="tool-category-item"
|
||||
:class="{ disabled: !category.enabled }">
|
||||
<span class="tool-category-label">
|
||||
<span class="tool-category-icon">{{ toolCategoryEmoji(category.id) }}</span>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="menu-btn tool-category-toggle"
|
||||
@click="updateToolCategory(category.id, !category.enabled)"
|
||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading">
|
||||
{{ category.enabled ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="handleSendOrStop"
|
||||
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)"
|
||||
:class="['btn', streamingMessage ? 'stop-btn' : 'send-btn']">
|
||||
{{ streamingMessage ? '停止' : '发送' }}
|
||||
</button>
|
||||
<div class="settings-dropdown" ref="settingsDropdown">
|
||||
<button type="button"
|
||||
class="btn settings-btn"
|
||||
@click="toggleSettings"
|
||||
:disabled="!isConnected">
|
||||
设置
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu" v-if="settingsOpen">
|
||||
<button type="button"
|
||||
class="menu-btn focus-entry"
|
||||
@click="toggleFocusPanel"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
{{ rightCollapsed ? '展开聚焦面板' : '折叠聚焦面板' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn compress-entry"
|
||||
@click="compressConversation"
|
||||
:disabled="compressing || streamingMessage || !isConnected">
|
||||
{{ compressing ? '压缩中...' : '压缩' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn clear-entry"
|
||||
@click="clearChat"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||
|
||||
<!-- 右侧聚焦文件 -->
|
||||
<aside class="sidebar right-sidebar"
|
||||
:class="{ collapsed: rightCollapsed }"
|
||||
:style="{ width: rightCollapsed ? '0px' : rightWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<h3>👁️ 聚焦文件 ({{ Object.keys(focusedFiles).length }}/3)</h3>
|
||||
</div>
|
||||
<div class="focused-files" v-if="!rightCollapsed">
|
||||
<div v-if="Object.keys(focusedFiles).length === 0" class="no-files">
|
||||
暂无聚焦文件
|
||||
</div>
|
||||
<div v-else class="file-tabs">
|
||||
<div v-for="(file, path) in focusedFiles" :key="path" class="file-tab">
|
||||
<div class="tab-header">
|
||||
<span class="file-name">{{ path.split('/').pop() }}</span>
|
||||
<span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code :class="getLanguageClass(path)">{{ file.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu"
|
||||
v-if="contextMenu.visible"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click.stop>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'file'"
|
||||
@click.stop="downloadFile(contextMenu.node.path)">
|
||||
下载文件
|
||||
</button>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'folder'"
|
||||
:disabled="!contextMenu.node.path"
|
||||
@click.stop="downloadFolder(contextMenu.node.path)">
|
||||
下载压缩包
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 全局复制代码块函数
|
||||
function copyCodeBlock(blockId) {
|
||||
const codeElement = document.querySelector(`[data-code-id="${blockId}"]`);
|
||||
if (!codeElement) return;
|
||||
|
||||
const button = document.querySelector(`[data-code="${blockId}"]`);
|
||||
|
||||
// 如果正在显示"已复制"状态,忽略点击
|
||||
if (button.classList.contains('copied')) return;
|
||||
|
||||
// 使用保存的原始代码内容
|
||||
const codeContent = codeElement.dataset.originalCode || codeElement.textContent;
|
||||
|
||||
// 首次点击时保存原始图标
|
||||
if (!button.dataset.originalIcon) {
|
||||
button.dataset.originalIcon = button.textContent;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeContent).then(() => {
|
||||
button.textContent = '✓';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
// 使用保存的原始图标恢复
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 即使失败也要恢复状态
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
});
|
||||
}
|
||||
// 使用事件委托处理复制按钮点击
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('copy-code-btn')) {
|
||||
const blockId = e.target.getAttribute('data-code');
|
||||
if (blockId) copyCodeBlock(blockId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- 加载应用脚本 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f8f2ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 32px 28px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 18px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>AI Agent 登录</h1>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<button id="login-btn">登录</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
还没有账号?<a href="/register">点击注册</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('login-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
if (!email || !password) {
|
||||
errorEl.textContent = '请输入邮箱和密码';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>注册 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f0eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.register-card {
|
||||
width: 380px;
|
||||
padding: 32px 30px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 16px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-card">
|
||||
<h1>创建账号</h1>
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" placeholder="仅限小写字母/数字/下划线" autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invite">邀请码</label>
|
||||
<input type="text" id="invite" placeholder="必填" autocomplete="off" />
|
||||
</div>
|
||||
<button id="register-btn">注册</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
已有账号?<a href="/login">返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('register-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
async function register() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const invite = document.getElementById('invite').value.trim();
|
||||
|
||||
if (!username || !email || !password || !invite) {
|
||||
errorEl.textContent = '请完整填写所有字段';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password, invite_code: invite })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '注册失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请稍后再试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', register);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
register();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,803 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||||
|
||||
<!-- xterm.js 终端模拟器 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #ef4444;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 会话标签栏 */
|
||||
.session-tabs {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 终端容器 */
|
||||
.terminal-wrapper {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #2d2d2d;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.control-btn.close { background: #ff5f56; }
|
||||
.control-btn.minimize { background: #ffbd2e; }
|
||||
.control-btn.maximize { background: #27c93f; }
|
||||
|
||||
#terminal {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
padding-bottom: 30px; /* 增加底部内边距 */
|
||||
overflow-y: auto; /* 确保可以滚动 */
|
||||
}
|
||||
|
||||
/* 侧边栏信息 */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 命令历史 */
|
||||
.command-history {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #4ade80;
|
||||
border-left: 2px solid #4ade80;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* 底部状态栏 */
|
||||
.status-bar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-left {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loader {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>
|
||||
🖥️ AI Terminal Monitor
|
||||
<span class="status-indicator" id="connectionStatus"></span>
|
||||
<span style="font-size: 14px; font-weight: normal; margin-left: 10px;" id="connectionText">连接中...</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 会话标签 -->
|
||||
<div class="session-tabs" id="sessionTabs">
|
||||
<div class="tab" onclick="createNewSession()">
|
||||
<span class="tab-icon">➕</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 终端容器 -->
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">
|
||||
<span id="terminalSession">无活动会话</span>
|
||||
<span id="terminalPath" style="color: #666;">-</span>
|
||||
</div>
|
||||
<div class="terminal-controls">
|
||||
<div class="control-btn close" title="关闭"></div>
|
||||
<div class="control-btn minimize" title="最小化"></div>
|
||||
<div class="control-btn maximize" title="最大化"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<!-- 会话信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📊 会话信息</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">会话名称</span>
|
||||
<span class="info-value" id="sessionName">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">工作目录</span>
|
||||
<span class="info-value" id="workingDir">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Shell类型</span>
|
||||
<span class="info-value" id="shellType">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">运行时间</span>
|
||||
<span class="info-value" id="uptime">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">缓冲区</span>
|
||||
<span class="info-value" id="bufferSize">0 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 命令历史 -->
|
||||
<div class="info-section">
|
||||
<h3>📝 最近命令</h3>
|
||||
<div class="command-history" id="commandHistory">
|
||||
<div style="color: #666; text-align: center;">暂无命令</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📈 统计</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">总命令数</span>
|
||||
<span class="info-value" id="commandCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">输出行数</span>
|
||||
<span class="info-value" id="outputLines">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">活动会话</span>
|
||||
<span class="info-value" id="activeCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-item">
|
||||
<span>⚡</span>
|
||||
<span id="latency">延迟: 0ms</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>📡</span>
|
||||
<span id="dataRate">0 KB/s</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>🕐</span>
|
||||
<span id="currentTime"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<!-- 引入依赖 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let term = null;
|
||||
let fitAddon = null;
|
||||
let socket = null;
|
||||
let currentSession = null;
|
||||
let sessions = {};
|
||||
let commandHistory = [];
|
||||
let stats = {
|
||||
commandCount: 0,
|
||||
outputLines: 0,
|
||||
dataReceived: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// 初始化终端
|
||||
function initTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#aeafad',
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5'
|
||||
},
|
||||
scrollback: 10000,
|
||||
cols: 120, // 设置固定列数
|
||||
rows: 30, // 设置固定行数
|
||||
convertEol: true, // 转换行尾
|
||||
wordSeparator: ' ()[]{}\'"', // 单词分隔符
|
||||
rendererType: 'canvas', // 使用canvas渲染器
|
||||
allowTransparency: false
|
||||
});
|
||||
|
||||
// 添加插件
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
|
||||
// 打开终端
|
||||
term.open(document.getElementById('terminal'));
|
||||
|
||||
// 延迟执行fit以确保正确计算尺寸
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
console.log(`终端尺寸: ${term.cols}x${term.rows}`);
|
||||
}, 100);
|
||||
|
||||
// 显示欢迎信息
|
||||
term.writeln('\x1b[1;36m╔════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||
term.writeln('');
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
socket = io();
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket连接成功');
|
||||
updateConnectionStatus(true);
|
||||
term.writeln('\x1b[32m✓ 已连接到服务器\x1b[0m');
|
||||
|
||||
// 订阅所有终端事件
|
||||
socket.emit('terminal_subscribe', { all: true });
|
||||
console.log('已发送终端订阅请求');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('WebSocket连接断开');
|
||||
updateConnectionStatus(false);
|
||||
term.writeln('\x1b[31m✗ 与服务器断开连接\x1b[0m');
|
||||
});
|
||||
|
||||
// 订阅成功确认
|
||||
socket.on('terminal_subscribed', (data) => {
|
||||
console.log('终端订阅成功:', data);
|
||||
term.writeln('\x1b[32m✓ 已订阅终端事件\x1b[0m');
|
||||
});
|
||||
|
||||
// 终端启动事件
|
||||
socket.on('terminal_started', (data) => {
|
||||
console.log('终端启动:', data);
|
||||
addSession(data.session, data);
|
||||
term.writeln(`\x1b[32m[终端启动]\x1b[0m ${data.session} - ${data.working_dir}`);
|
||||
});
|
||||
|
||||
// 终端列表更新
|
||||
socket.on('terminal_list_update', (data) => {
|
||||
console.log('终端列表更新:', data);
|
||||
if (data.terminals && data.terminals.length > 0) {
|
||||
// 清除所有会话并重新添加
|
||||
sessions = {};
|
||||
for (const terminal of data.terminals) {
|
||||
sessions[terminal.name] = {
|
||||
working_dir: terminal.working_dir,
|
||||
is_running: terminal.is_running,
|
||||
shell: 'bash'
|
||||
};
|
||||
}
|
||||
updateSessionTabs();
|
||||
if (data.active && !currentSession) {
|
||||
switchToSession(data.active);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输出事件
|
||||
socket.on('terminal_output', (data) => {
|
||||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 直接写入原始输出
|
||||
term.write(data.data);
|
||||
stats.outputLines++;
|
||||
stats.dataReceived += data.data.length;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输入事件(AI发送的命令)
|
||||
socket.on('terminal_input', (data) => {
|
||||
console.log('收到终端输入:', data.session, data.data);
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 用绿色显示输入的命令
|
||||
term.write(`\x1b[1;32m➜ ${data.data}\x1b[0m`);
|
||||
|
||||
// 添加到命令历史
|
||||
addCommandToHistory(data.data.trim());
|
||||
stats.commandCount++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端关闭事件
|
||||
socket.on('terminal_closed', (data) => {
|
||||
console.log('终端关闭:', data);
|
||||
removeSession(data.session);
|
||||
term.writeln(`\x1b[31m[终端关闭]\x1b[0m ${data.session}`);
|
||||
});
|
||||
|
||||
// 终端重置事件
|
||||
socket.on('terminal_reset', (data) => {
|
||||
console.log('终端重置:', data);
|
||||
if (data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: data.working_dir || 'unknown'
|
||||
});
|
||||
} else {
|
||||
sessions[data.session].working_dir = data.working_dir || sessions[data.session].working_dir;
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||||
resetCommandHistory();
|
||||
stats.commandCount = 0;
|
||||
stats.outputLines = 0;
|
||||
stats.dataReceived = 0;
|
||||
stats.startTime = Date.now();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// 终端切换事件
|
||||
socket.on('terminal_switched', (data) => {
|
||||
console.log('终端切换:', data);
|
||||
switchToSession(data.current);
|
||||
});
|
||||
|
||||
// 调试:监听所有事件
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
console.log('Socket事件:', args[0], args[1]);
|
||||
onevent.call(this, packet);
|
||||
};
|
||||
}
|
||||
|
||||
// 添加会话
|
||||
function addSession(name, info) {
|
||||
sessions[name] = info;
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是第一个会话,自动切换
|
||||
if (!currentSession) {
|
||||
switchToSession(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除会话
|
||||
function removeSession(name) {
|
||||
delete sessions[name];
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是当前会话,切换到其他会话
|
||||
if (currentSession === name) {
|
||||
const remaining = Object.keys(sessions);
|
||||
if (remaining.length > 0) {
|
||||
switchToSession(remaining[0]);
|
||||
} else {
|
||||
currentSession = null;
|
||||
updateSessionInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function switchToSession(name) {
|
||||
currentSession = name;
|
||||
updateSessionTabs();
|
||||
updateSessionInfo();
|
||||
|
||||
// 清空终端并显示切换信息
|
||||
term.clear();
|
||||
term.writeln(`\x1b[36m[切换到终端: ${name}]\x1b[0m`);
|
||||
term.writeln('');
|
||||
|
||||
// 请求该会话的历史输出
|
||||
socket.emit('get_terminal_output', { session: name });
|
||||
}
|
||||
|
||||
// 更新会话标签
|
||||
function updateSessionTabs() {
|
||||
const container = document.getElementById('sessionTabs');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (Object.keys(sessions).length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="tab">
|
||||
<span class="tab-icon">⏳</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, info] of Object.entries(sessions)) {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = `tab ${name === currentSession ? 'active' : ''}`;
|
||||
tab.innerHTML = `
|
||||
<span class="tab-icon">📟</span>
|
||||
<span>${name}</span>
|
||||
`;
|
||||
tab.onclick = () => switchToSession(name);
|
||||
container.appendChild(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新会话信息
|
||||
function updateSessionInfo() {
|
||||
if (!currentSession || !sessions[currentSession]) {
|
||||
document.getElementById('sessionName').textContent = '-';
|
||||
document.getElementById('workingDir').textContent = '-';
|
||||
document.getElementById('shellType').textContent = '-';
|
||||
document.getElementById('terminalSession').textContent = '无活动会话';
|
||||
document.getElementById('terminalPath').textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
const info = sessions[currentSession];
|
||||
document.getElementById('sessionName').textContent = currentSession;
|
||||
document.getElementById('workingDir').textContent = info.working_dir || '-';
|
||||
document.getElementById('shellType').textContent = info.shell || '-';
|
||||
document.getElementById('terminalSession').textContent = currentSession;
|
||||
document.getElementById('terminalPath').textContent = info.working_dir || '-';
|
||||
}
|
||||
|
||||
// 添加命令到历史
|
||||
function addCommandToHistory(command) {
|
||||
commandHistory.unshift(command);
|
||||
if (commandHistory.length > 20) {
|
||||
commandHistory.pop();
|
||||
}
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
function resetCommandHistory() {
|
||||
commandHistory = [];
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
// 更新命令历史显示
|
||||
function updateCommandHistory() {
|
||||
const container = document.getElementById('commandHistory');
|
||||
if (commandHistory.length === 0) {
|
||||
container.innerHTML = '<div style="color: #666; text-align: center;">暂无命令</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = commandHistory
|
||||
.map(cmd => `<div class="command-item">${cmd}</div>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
document.getElementById('commandCount').textContent = stats.commandCount;
|
||||
document.getElementById('outputLines').textContent = stats.outputLines;
|
||||
document.getElementById('activeCount').textContent = Object.keys(sessions).length;
|
||||
document.getElementById('bufferSize').textContent = (stats.dataReceived / 1024).toFixed(1) + ' KB';
|
||||
|
||||
// 更新运行时间
|
||||
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
document.getElementById('uptime').textContent =
|
||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('connectionStatus');
|
||||
const text = document.getElementById('connectionText');
|
||||
|
||||
if (connected) {
|
||||
indicator.classList.remove('disconnected');
|
||||
text.textContent = '已连接';
|
||||
text.style.color = '#4ade80';
|
||||
} else {
|
||||
indicator.classList.add('disconnected');
|
||||
text.textContent = '未连接';
|
||||
text.style.color = '#ef4444';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('currentTime').textContent =
|
||||
now.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTerminal();
|
||||
initWebSocket();
|
||||
|
||||
// 定时更新
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
setInterval(updateStats, 1000);
|
||||
updateCurrentTime();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,194 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Claude颜色展示 - 简化版</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--claude-bg: #eeece2;
|
||||
--claude-text: #3d3929;
|
||||
--claude-accent: #da7756;
|
||||
--claude-accent-strong: #bd5d3a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
background-color: var(--claude-bg);
|
||||
color: var(--claude-text);
|
||||
line-height: 1.5;
|
||||
overflow-y: auto;
|
||||
min-height: 100vh;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
margin: 15px 0;
|
||||
text-align: center;
|
||||
color: var(--claude-text);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
margin: 20px 0 10px 0;
|
||||
color: var(--claude-text);
|
||||
border-bottom: 2px solid var(--claude-accent);
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 10px 0;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.color-name {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.color-hex {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.text-sample {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
border-left: 3px solid var(--claude-accent);
|
||||
}
|
||||
|
||||
.button-demo {
|
||||
background: var(--claude-accent-strong);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chat-sample {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.user-msg {
|
||||
background: rgba(255,255,255,0.7);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 0 12px;
|
||||
margin: 5px 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.ai-msg {
|
||||
background: rgba(218, 119, 86, 0.12);
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px 12px 12px 0;
|
||||
margin: 5px 0;
|
||||
margin-right: 20px;
|
||||
border-left: 2px solid var(--claude-accent-strong);
|
||||
}
|
||||
|
||||
/* 颜色定义 */
|
||||
.bg-color { background-color: var(--claude-bg); }
|
||||
.text-color { background-color: var(--claude-text); }
|
||||
.logo-color { background-color: var(--claude-accent); }
|
||||
.button-color { background-color: var(--claude-accent-strong); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Claude聊天界面颜色</h1>
|
||||
|
||||
<h2>核心颜色</h2>
|
||||
<div class="color-card">
|
||||
<div class="color-swatch bg-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">背景颜色</div>
|
||||
<div class="color-hex">#eeece2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch text-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">字体颜色</div>
|
||||
<div class="color-hex">#3d3929</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch logo-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">品牌主色</div>
|
||||
<div class="color-hex">#da7756</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-card">
|
||||
<div class="color-swatch button-color"></div>
|
||||
<div class="color-info">
|
||||
<div class="color-name">按钮颜色</div>
|
||||
<div class="color-hex">#bd5d3a</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>字体样式</h2>
|
||||
<div class="text-sample">
|
||||
字体栈:ui-serif, Georgia, Cambria, "Times New Roman", Times, serif
|
||||
<br><br>
|
||||
<i>斜体样式展示</i> | <b>粗体样式展示</b>
|
||||
</div>
|
||||
|
||||
<h2>界面元素</h2>
|
||||
<div class="text-sample">
|
||||
<a href="#" class="button-demo">主要按钮</a>
|
||||
<a href="#" class="button-demo" style="background: #da7756;">悬停效果</a>
|
||||
</div>
|
||||
|
||||
<h2>对话界面模拟</h2>
|
||||
<div class="chat-sample">
|
||||
<div class="user-msg">你好,Claude!今天天气怎么样?</div>
|
||||
<div class="ai-msg">你好!我无法获取实时天气信息,但你可以通过天气应用查看当地天气。</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sample">
|
||||
这种配色方案创造出温暖、专业的对话环境,背景是米白色(#eeece2),文字是深棕色(#3d3929),给人舒适友好的感觉。
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,399 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent 调试监控</title>
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.6.1/dist/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
background: #1a1a1a;
|
||||
color: #0f0;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.panel {
|
||||
background: #000;
|
||||
border: 1px solid #0f0;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.event-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
.event-item {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
.event-type {
|
||||
color: #ff0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.timestamp {
|
||||
color: #888;
|
||||
}
|
||||
.actions-monitor {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.action-card {
|
||||
border: 1px solid #444;
|
||||
padding: 5px;
|
||||
background: #111;
|
||||
}
|
||||
.action-type {
|
||||
color: #0ff;
|
||||
}
|
||||
.tool-status {
|
||||
color: #f0f;
|
||||
}
|
||||
.streaming-true {
|
||||
background: #330;
|
||||
}
|
||||
.status-running {
|
||||
background: #003;
|
||||
}
|
||||
.status-completed {
|
||||
background: #030;
|
||||
}
|
||||
.raw-data {
|
||||
background: #222;
|
||||
padding: 5px;
|
||||
margin: 5px 0;
|
||||
white-space: pre-wrap;
|
||||
font-size: 11px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
button {
|
||||
background: #0f0;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="debug-app" class="container">
|
||||
<h1>🔧 AI Agent 调试监控</h1>
|
||||
|
||||
<!-- 连接状态 -->
|
||||
<div class="panel">
|
||||
<h3>连接状态</h3>
|
||||
<div>Socket连接: <span :style="{color: isConnected ? '#0f0' : '#f00'}">{{ isConnected ? '已连接' : '未连接' }}</span></div>
|
||||
<div>当前消息索引: {{ currentMessageIndex }}</div>
|
||||
<div>消息总数: {{ messages.length }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="panel">
|
||||
<h3>控制面板</h3>
|
||||
<button @click="clearLogs">清除日志</button>
|
||||
<button @click="testMessage">发送测试消息</button>
|
||||
<button @click="exportData">导出数据</button>
|
||||
<label>
|
||||
<input type="checkbox" v-model="pauseUpdates"> 暂停更新
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 事件日志 -->
|
||||
<div class="panel">
|
||||
<h3>WebSocket事件流 (最新 {{ events.length }} 条)</h3>
|
||||
<div class="event-log">
|
||||
<div v-for="(event, idx) in events" :key="idx" class="event-item">
|
||||
<span class="timestamp">{{ event.time }}</span>
|
||||
<span class="event-type">{{ event.type }}</span>
|
||||
<span v-if="event.data">: {{ JSON.stringify(event.data).slice(0, 200) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前消息的Actions监控 -->
|
||||
<div class="panel" v-if="currentMessageIndex >= 0 && messages[currentMessageIndex]">
|
||||
<h3>当前消息Actions状态 (消息 #{{ currentMessageIndex }})</h3>
|
||||
<div class="actions-monitor">
|
||||
<div v-for="(action, idx) in messages[currentMessageIndex].actions"
|
||||
:key="action.id"
|
||||
class="action-card"
|
||||
:class="{
|
||||
'streaming-true': action.streaming,
|
||||
'status-running': action.tool && action.tool.status === 'running',
|
||||
'status-completed': action.tool && action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="action-type">Action #{{ idx }}: {{ action.type }}</div>
|
||||
<div v-if="action.type === 'text'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
<div v-if="action.type === 'tool'">
|
||||
工具: {{ action.tool.name }}<br>
|
||||
<span class="tool-status">状态: {{ action.tool.status }}</span><br>
|
||||
ID: {{ action.tool.id }}<br>
|
||||
有结果: {{ !!action.tool.result }}
|
||||
</div>
|
||||
<div v-if="action.type === 'thinking'">
|
||||
Streaming: <b>{{ action.streaming }}</b><br>
|
||||
Content长度: {{ action.content ? action.content.length : 0 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 原始数据查看 -->
|
||||
<div class="panel">
|
||||
<h3>最新消息原始数据</h3>
|
||||
<div class="raw-data" v-if="messages.length > 0">{{ JSON.stringify(messages[messages.length - 1], null, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
|
||||
const debugApp = createApp({
|
||||
data() {
|
||||
return {
|
||||
isConnected: false,
|
||||
socket: null,
|
||||
events: [],
|
||||
messages: [],
|
||||
currentMessageIndex: -1,
|
||||
pauseUpdates: false,
|
||||
maxEvents: 100
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initSocket();
|
||||
},
|
||||
|
||||
methods: {
|
||||
initSocket() {
|
||||
this.socket = io('/', {
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// 监听所有事件
|
||||
const events = [
|
||||
'connect', 'disconnect', 'system_ready',
|
||||
'ai_message_start', 'thinking_start', 'thinking_chunk', 'thinking_end',
|
||||
'text_start', 'text_chunk', 'text_end',
|
||||
'tool_preparing', 'tool_hint', 'tool_start', 'tool_status',
|
||||
'tool_execution_start', 'tool_execution_end', 'update_action',
|
||||
'task_complete', 'error'
|
||||
];
|
||||
|
||||
events.forEach(eventName => {
|
||||
this.socket.on(eventName, (data) => {
|
||||
this.logEvent(eventName, data);
|
||||
this.handleEvent(eventName, data);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
this.isConnected = false;
|
||||
});
|
||||
},
|
||||
|
||||
logEvent(type, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
const event = {
|
||||
time: new Date().toLocaleTimeString('zh-CN', { hour12: false, milliseconds: true }),
|
||||
type: type,
|
||||
data: data
|
||||
};
|
||||
|
||||
this.events.unshift(event);
|
||||
if (this.events.length > this.maxEvents) {
|
||||
this.events = this.events.slice(0, this.maxEvents);
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent(eventName, data) {
|
||||
if (this.pauseUpdates) return;
|
||||
|
||||
// 处理消息相关事件
|
||||
switch(eventName) {
|
||||
case 'ai_message_start':
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
actions: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
this.currentMessageIndex = this.messages.length - 1;
|
||||
break;
|
||||
|
||||
case 'thinking_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'thinking',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'thinking_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const thinkingAction = msg.actions.find(a => a.type === 'thinking' && a.streaming);
|
||||
if (thinkingAction) {
|
||||
thinkingAction.streaming = false;
|
||||
console.log('思考结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: Date.now(),
|
||||
type: 'text',
|
||||
content: '',
|
||||
streaming: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_chunk':
|
||||
if (this.currentMessageIndex >= 0 && data.content) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.content += data.content;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text_end':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const textAction = [...msg.actions].reverse().find(a => a.type === 'text' && a.streaming);
|
||||
if (textAction) {
|
||||
textAction.streaming = false;
|
||||
console.log('文本结束,设置streaming=false');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_preparing':
|
||||
case 'tool_hint':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'preparing',
|
||||
arguments: {},
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tool_start':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
// 查找准备中的工具或创建新的
|
||||
let tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.id === data.preparing_id || a.tool.id === data.preparing_id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = 'running';
|
||||
tool.tool.arguments = data.arguments;
|
||||
console.log('工具开始运行,状态改为running');
|
||||
} else {
|
||||
msg.actions.push({
|
||||
id: data.id,
|
||||
type: 'tool',
|
||||
tool: {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
status: 'running',
|
||||
arguments: data.arguments,
|
||||
result: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_action':
|
||||
if (this.currentMessageIndex >= 0) {
|
||||
const msg = this.messages[this.currentMessageIndex];
|
||||
const tool = msg.actions.find(a =>
|
||||
a.type === 'tool' &&
|
||||
(a.tool.id === data.id || a.tool.id === data.preparing_id || a.id === data.id)
|
||||
);
|
||||
|
||||
if (tool) {
|
||||
tool.tool.status = data.status || 'completed';
|
||||
tool.tool.result = data.result;
|
||||
console.log(`工具完成,状态改为${tool.tool.status}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_complete':
|
||||
this.currentMessageIndex = -1;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
clearLogs() {
|
||||
this.events = [];
|
||||
this.messages = [];
|
||||
this.currentMessageIndex = -1;
|
||||
},
|
||||
|
||||
testMessage() {
|
||||
this.socket.emit('send_message', { message: '创建一个test.txt文件' });
|
||||
},
|
||||
|
||||
exportData() {
|
||||
const data = {
|
||||
events: this.events,
|
||||
messages: this.messages,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `debug-${Date.now()}.json`;
|
||||
a.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
debugApp.mount('#debug-app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,591 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent System</title>
|
||||
|
||||
<!-- Vue 3 CDN -->
|
||||
<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>
|
||||
|
||||
<!-- Socket.IO Client(CDN优先,JS 内有多源兜底加载) -->
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js" defer></script>
|
||||
|
||||
<!-- Marked.js for Markdown -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script>
|
||||
|
||||
<!-- Prism.js for code highlighting -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
|
||||
|
||||
<!-- KaTeX for LaTeX (无defer,同步加载) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="!isConnected && messages.length === 0" style="text-align: center; padding: 50px;">
|
||||
<h2>正在连接服务器...</h2>
|
||||
<p>如果长时间无响应,请刷新页面</p>
|
||||
</div>
|
||||
|
||||
<!-- Main UI (只在连接后显示) -->
|
||||
<template v-else>
|
||||
<!-- 顶部状态栏 -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<span class="logo">🤖 AI Agent</span>
|
||||
<span class="project-path">{{ projectPath }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="thinking-mode">{{ thinkingMode }}</span>
|
||||
<span class="connection-status" :class="{ connected: isConnected }">
|
||||
<span class="status-dot" :class="{ active: isConnected }"></span>
|
||||
{{ isConnected ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="main-container">
|
||||
<!-- 新增:对话历史侧边栏(最左侧) -->
|
||||
<aside class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="conversation-header">
|
||||
<button @click="createNewConversation" class="new-conversation-btn" v-if="!sidebarCollapsed">
|
||||
<span class="btn-icon">+</span>
|
||||
<span class="btn-text">新建对话</span>
|
||||
</button>
|
||||
<button @click="toggleSidebar" class="toggle-sidebar-btn">
|
||||
<span v-if="sidebarCollapsed">☰</span>
|
||||
<span v-else>←</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="!sidebarCollapsed">
|
||||
<div class="conversation-search">
|
||||
<input v-model="searchQuery"
|
||||
@input="searchConversations"
|
||||
placeholder="搜索对话..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="conversation-list">
|
||||
<div v-if="conversationsLoading" class="loading-conversations">
|
||||
正在加载...
|
||||
</div>
|
||||
<div v-else-if="conversations.length === 0" class="no-conversations">
|
||||
暂无对话记录
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="conv in conversations"
|
||||
:key="conv.id"
|
||||
class="conversation-item"
|
||||
:class="{ active: conv.id === currentConversationId }"
|
||||
@click="loadConversation(conv.id)">
|
||||
<div class="conversation-title">{{ conv.title }}</div>
|
||||
<div class="conversation-meta">
|
||||
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
|
||||
<span class="conversation-counts">
|
||||
{{ conv.total_messages }}条消息
|
||||
<span v-if="conv.total_tools > 0"> · {{ conv.total_tools }}工具</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="conversation-actions" @click.stop>
|
||||
<button @click="deleteConversation(conv.id)"
|
||||
class="conversation-action-btn delete-btn"
|
||||
title="删除对话">
|
||||
×
|
||||
</button>
|
||||
<button @click="duplicateConversation(conv.id)"
|
||||
class="conversation-action-btn copy-btn"
|
||||
title="复制对话">
|
||||
⧉
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasMoreConversations" class="load-more">
|
||||
<button @click="loadMoreConversations"
|
||||
:disabled="loadingMoreConversations"
|
||||
class="load-more-btn">
|
||||
{{ loadingMoreConversations ? '载入中...' : '加载更多' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="conversation-collapsed-spacer"></div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧文件树 -->
|
||||
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<button class="sidebar-view-toggle"
|
||||
@click="toggleTodoPanel"
|
||||
:title="showTodoList ? '查看项目文件' : '查看待办列表'">
|
||||
<span v-if="showTodoList">{{ fileEmoji }}</span>
|
||||
<span v-else>{{ todoEmoji }}</span>
|
||||
</button>
|
||||
<h3>{{ showTodoList ? (todoEmoji + ' 待办列表') : (fileEmoji + ' 项目文件') }}</h3>
|
||||
</div>
|
||||
<template v-if="showTodoList">
|
||||
<div class="todo-panel">
|
||||
<div v-if="!todoList" class="todo-empty">
|
||||
暂无待办列表
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="todo-task"
|
||||
v-for="task in (todoList.tasks || [])"
|
||||
:key="task.index"
|
||||
:class="{ done: task.status === 'done' }">
|
||||
<span class="todo-task-title">task{{ task.index }}:{{ task.title }}</span>
|
||||
<span class="todo-task-status">{{ formatTaskStatus(task) }}</span>
|
||||
</div>
|
||||
<div class="todo-instruction">{{ todoList.instruction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-tree" @contextmenu.prevent>
|
||||
<file-node
|
||||
v-for="node in fileTree"
|
||||
:key="node.path"
|
||||
:node="node"
|
||||
:level="0"
|
||||
:expanded-folders="expandedFolders"
|
||||
@toggle-folder="toggleFolder"
|
||||
@context-menu="showContextMenu"
|
||||
></file-node>
|
||||
</div>
|
||||
</template>
|
||||
</aside>
|
||||
|
||||
<!-- 左侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
|
||||
|
||||
<!-- 中间聊天区域 -->
|
||||
<main class="chat-container">
|
||||
<!-- 当前对话信息栏 -->
|
||||
<div class="current-conversation-info" v-if="currentConversationTitle">
|
||||
<span class="conversation-title-display">{{ currentConversationTitle }}</span>
|
||||
<span class="conversation-stats">
|
||||
<span class="message-count">{{ messages.length }}条消息</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Token区域包装器 -->
|
||||
<div class="token-wrapper" v-if="currentConversationId">
|
||||
<!-- Token统计显示面板 -->
|
||||
<div class="token-display-panel" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<div class="token-panel-content">
|
||||
<div class="token-stats">
|
||||
<div class="token-item">
|
||||
<span class="token-label">当前上下文</span>
|
||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-separator"></div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输入</span>
|
||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="token-item">
|
||||
<span class="token-label">累计输出</span>
|
||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的切换按钮 -->
|
||||
<button @click="toggleTokenPanel" class="token-toggle-btn" :class="{ collapsed: tokenPanelCollapsed }">
|
||||
<span v-if="!tokenPanelCollapsed">▲</span>
|
||||
<span v-else>▼</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages-area" ref="messagesArea">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
||||
|
||||
<!-- 用户消息 -->
|
||||
<div v-if="msg.role === 'user'" class="user-message">
|
||||
<div class="message-header">👤 用户</div>
|
||||
<div class="message-text">{{ msg.content }}</div>
|
||||
</div>
|
||||
|
||||
<!-- AI消息 -->
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<div class="message-header">🤖 AI Assistant</div>
|
||||
|
||||
<!-- 按顺序显示所有actions -->
|
||||
<div v-for="(action, actionIndex) in msg.actions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="{
|
||||
'streaming-content': action.streaming,
|
||||
'completed-tool': action.type === 'tool' && !action.streaming,
|
||||
'immediate-show': action.streaming || action.type === 'text'
|
||||
}">
|
||||
|
||||
<!-- 思考块 -->
|
||||
<div v-if="action.type === 'thinking'"
|
||||
class="collapsible-block thinking-block"
|
||||
:class="{ expanded: expandedBlocks.has(`${index}-thinking-${actionIndex}`) }">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-thinking-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<span class="thinking-icon" :class="{ 'thinking-animation': action.streaming }">🧠</span>
|
||||
</div>
|
||||
<span class="status-text">{{ action.streaming ? '正在思考...' : '思考过程' }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner thinking-content">
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="action.streaming" class="progress-indicator"></div>
|
||||
</div>
|
||||
|
||||
<!-- 文本输出块 -->
|
||||
<div v-else-if="action.type === 'text'" class="text-output">
|
||||
<div class="text-content"
|
||||
:class="{ 'streaming-text': action.streaming }">
|
||||
<!-- 流式:实时渲染markdown但不包装代码块 -->
|
||||
<div v-if="action.streaming" v-html="renderMarkdown(action.content, true)"></div>
|
||||
<!-- 完成:完整渲染包含代码块包装 -->
|
||||
<div v-else v-html="renderMarkdown(action.content, false)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 追加内容占位 -->
|
||||
<div v-else-if="action.type === 'append_payload'"
|
||||
class="append-placeholder"
|
||||
:class="{ 'append-error': action.append?.success === false }">
|
||||
<div class="append-placeholder-content">
|
||||
<template v-if="action.append?.success !== false">
|
||||
✏️ 已写入 {{ action.append?.path || '目标文件' }} 的追加内容(内容已保存至文件)
|
||||
</template>
|
||||
<template v-else>
|
||||
❌ 向 {{ action.append?.path || '目标文件' }} 写入失败,内容已截获供后续修复。
|
||||
</template>
|
||||
<div class="append-meta" v-if="action.append">
|
||||
<span v-if="action.append.lines !== null && action.append.lines !== undefined">
|
||||
· 行数 {{ action.append.lines }}
|
||||
</span>
|
||||
<span v-if="action.append.bytes !== null && action.append.bytes !== undefined">
|
||||
· 字节 {{ action.append.bytes }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="append-warning" v-if="action.append?.forced">
|
||||
⚠️ 未检测到结束标记,请根据提示继续补充。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改内容占位 -->
|
||||
<div v-else-if="action.type === 'modify_payload'" class="modify-placeholder">
|
||||
<div class="modify-placeholder-content">
|
||||
🛠️ 已对 {{ action.modify?.path || '目标文件' }} 执行补丁
|
||||
<div class="modify-meta" v-if="action.modify">
|
||||
<span v-if="action.modify.total !== null && action.modify.total !== undefined">
|
||||
· 共 {{ action.modify.total }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.completed && action.modify.completed.length">
|
||||
· 已完成 {{ action.modify.completed.length }} 处
|
||||
</span>
|
||||
<span v-if="action.modify.failed && action.modify.failed.length">
|
||||
· 未完成 {{ action.modify.failed.length }} 处
|
||||
</span>
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.forced">
|
||||
⚠️ 未检测到结束标记,系统已在流结束时执行补丁。
|
||||
</div>
|
||||
<div class="modify-warning" v-if="action.modify?.failed && action.modify.failed.length">
|
||||
⚠️ 未完成的序号:{{ action.modify.failed.map(f => f.index || f).join('、') || action.modify.failed.join('、') }},请根据提示重新输出。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具块(修复版) -->
|
||||
<div v-else-if="action.type === 'tool'"
|
||||
class="collapsible-block tool-block"
|
||||
:class="{
|
||||
expanded: expandedBlocks.has(`${index}-tool-${actionIndex}`),
|
||||
processing: action.tool.status === 'preparing' || action.tool.status === 'running',
|
||||
completed: action.tool.status === 'completed'
|
||||
}">
|
||||
<div class="collapsible-header" @click="toggleBlock(`${index}-tool-${actionIndex}`)">
|
||||
<div class="arrow"></div>
|
||||
<div class="status-icon">
|
||||
<!-- 修复:传递完整的tool对象 -->
|
||||
<span class="tool-icon"
|
||||
:class="getToolAnimationClass(action.tool)">
|
||||
{{ getToolIcon(action.tool) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="status-text">
|
||||
{{ getToolStatusText(action.tool) }}
|
||||
</span>
|
||||
<span class="tool-desc">{{ getToolDescription(action.tool) }}</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
<!-- 根据工具类型显示不同内容 -->
|
||||
<div v-if="action.tool.name === 'web_search' && action.tool.result">
|
||||
<div class="search-meta">
|
||||
<div><strong>搜索内容:</strong>{{ action.tool.result.query || action.tool.arguments.query }}</div>
|
||||
<div><strong>主题:</strong>{{ formatSearchTopic(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>时间范围:</strong>{{ formatSearchTime(action.tool.result.filters || {}) }}</div>
|
||||
<div><strong>结果数量:</strong>{{ action.tool.result.total_results }}</div>
|
||||
</div>
|
||||
<div v-if="action.tool.result.results && action.tool.result.results.length" class="search-result-list">
|
||||
<div v-for="item in action.tool.result.results" :key="item.url || item.index" class="search-result-item">
|
||||
<div class="search-result-title">{{ item.title || '无标题' }}</div>
|
||||
<div class="search-result-url"><a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a><span v-else>无可用链接</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="search-empty">未返回详细的搜索结果。</div>
|
||||
</div>
|
||||
<div v-else-if="action.tool.name === 'run_python' && action.tool.result">
|
||||
<div class="code-block">
|
||||
<div class="code-label">代码:</div>
|
||||
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments.code }}</code></pre>
|
||||
</div>
|
||||
<div v-if="action.tool.result.output" class="output-block">
|
||||
<div class="output-label">输出:</div>
|
||||
<pre>{{ action.tool.result.output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<pre>{{ JSON.stringify(action.tool.result || action.tool.arguments, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 修复:只在running状态显示进度条 -->
|
||||
<!-- 只在流式消息期间且工具运行时显示进度条 -->
|
||||
<div v-if="streamingMessage && (action.tool.status === 'preparing' || action.tool.status === 'running')"
|
||||
class="progress-indicator"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统消息 -->
|
||||
<div v-else class="system-message">
|
||||
{{ msg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
|
||||
<button @click="toggleScrollLock" class="scroll-lock-btn">
|
||||
<svg v-if="autoScrollEnabled && !userScrolling" viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
|
||||
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 5v12" />
|
||||
<path d="M7 13l5 5 5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown.enter.ctrl="sendMessage"
|
||||
placeholder="输入消息... (Ctrl+Enter 发送)"
|
||||
class="message-input"
|
||||
:disabled="!isConnected || streamingMessage"
|
||||
rows="3">
|
||||
</textarea>
|
||||
<div class="input-actions">
|
||||
<div class="upload-control">
|
||||
<input type="file"
|
||||
ref="fileUploadInput"
|
||||
class="file-input-hidden"
|
||||
@change="handleFileSelected">
|
||||
<button type="button"
|
||||
class="btn upload-btn"
|
||||
@click="triggerFileUpload"
|
||||
:disabled="!isConnected || uploading">
|
||||
{{ uploading ? '上传中...' : '上传文件' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="tool-dropdown" ref="toolDropdown">
|
||||
<button type="button"
|
||||
class="btn tool-btn"
|
||||
@click="toggleToolMenu"
|
||||
:disabled="!isConnected || toolSettingsLoading">
|
||||
工具
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu tool-menu" v-if="toolMenuOpen">
|
||||
<div class="tool-menu-status" v-if="toolSettingsLoading">
|
||||
正在同步工具状态...
|
||||
</div>
|
||||
<div v-else-if="toolSettings.length === 0" class="tool-menu-empty">
|
||||
暂无可控工具
|
||||
</div>
|
||||
<div v-else class="tool-menu-list">
|
||||
<div v-for="category in toolSettings"
|
||||
:key="category.id"
|
||||
class="tool-category-item"
|
||||
:class="{ disabled: !category.enabled }">
|
||||
<span class="tool-category-label">
|
||||
<span class="tool-category-icon">{{ toolCategoryEmoji(category.id) }}</span>
|
||||
{{ category.label }}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="menu-btn tool-category-toggle"
|
||||
@click="updateToolCategory(category.id, !category.enabled)"
|
||||
:disabled="streamingMessage || !isConnected || toolSettingsLoading">
|
||||
{{ category.enabled ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<button @click="handleSendOrStop"
|
||||
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)"
|
||||
:class="['btn', streamingMessage ? 'stop-btn' : 'send-btn']">
|
||||
{{ streamingMessage ? '停止' : '发送' }}
|
||||
</button>
|
||||
<div class="settings-dropdown" ref="settingsDropdown">
|
||||
<button type="button"
|
||||
class="btn settings-btn"
|
||||
@click="toggleSettings"
|
||||
:disabled="!isConnected">
|
||||
设置
|
||||
</button>
|
||||
<transition name="settings-menu">
|
||||
<div class="settings-menu" v-if="settingsOpen">
|
||||
<button type="button"
|
||||
class="menu-btn focus-entry"
|
||||
@click="toggleFocusPanel"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
{{ rightCollapsed ? '展开聚焦面板' : '折叠聚焦面板' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn compress-entry"
|
||||
@click="compressConversation"
|
||||
:disabled="compressing || streamingMessage || !isConnected">
|
||||
{{ compressing ? '压缩中...' : '压缩' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="menu-btn clear-entry"
|
||||
@click="clearChat"
|
||||
:disabled="streamingMessage || !isConnected">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧拖拽手柄 -->
|
||||
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||
|
||||
<!-- 右侧聚焦文件 -->
|
||||
<aside class="sidebar right-sidebar"
|
||||
:class="{ collapsed: rightCollapsed }"
|
||||
:style="{ width: rightCollapsed ? '0px' : rightWidth + 'px' }">
|
||||
<div class="sidebar-header">
|
||||
<h3>👁️ 聚焦文件 ({{ Object.keys(focusedFiles).length }}/3)</h3>
|
||||
</div>
|
||||
<div class="focused-files" v-if="!rightCollapsed">
|
||||
<div v-if="Object.keys(focusedFiles).length === 0" class="no-files">
|
||||
暂无聚焦文件
|
||||
</div>
|
||||
<div v-else class="file-tabs">
|
||||
<div v-for="(file, path) in focusedFiles" :key="path" class="file-tab">
|
||||
<div class="tab-header">
|
||||
<span class="file-name">{{ path.split('/').pop() }}</span>
|
||||
<span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
|
||||
</div>
|
||||
<div class="file-content">
|
||||
<pre><code :class="getLanguageClass(path)">{{ file.content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
<div class="context-menu"
|
||||
v-if="contextMenu.visible"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
@click.stop>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'file'"
|
||||
@click.stop="downloadFile(contextMenu.node.path)">
|
||||
下载文件
|
||||
</button>
|
||||
<button v-if="contextMenu.node && contextMenu.node.type === 'folder'"
|
||||
:disabled="!contextMenu.node.path"
|
||||
@click.stop="downloadFolder(contextMenu.node.path)">
|
||||
下载压缩包
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// 全局复制代码块函数
|
||||
function copyCodeBlock(blockId) {
|
||||
const codeElement = document.querySelector(`[data-code-id="${blockId}"]`);
|
||||
if (!codeElement) return;
|
||||
|
||||
const button = document.querySelector(`[data-code="${blockId}"]`);
|
||||
|
||||
// 如果正在显示"已复制"状态,忽略点击
|
||||
if (button.classList.contains('copied')) return;
|
||||
|
||||
// 使用保存的原始代码内容
|
||||
const codeContent = codeElement.dataset.originalCode || codeElement.textContent;
|
||||
|
||||
// 首次点击时保存原始图标
|
||||
if (!button.dataset.originalIcon) {
|
||||
button.dataset.originalIcon = button.textContent;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(codeContent).then(() => {
|
||||
button.textContent = '✓';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
// 使用保存的原始图标恢复
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
// 即使失败也要恢复状态
|
||||
button.textContent = button.dataset.originalIcon || '📋';
|
||||
button.classList.remove('copied');
|
||||
});
|
||||
}
|
||||
// 使用事件委托处理复制按钮点击
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('copy-code-btn')) {
|
||||
const blockId = e.target.getAttribute('data-code');
|
||||
if (blockId) copyCodeBlock(blockId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- 加载应用脚本 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,136 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f8f2ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.login-card {
|
||||
width: 360px;
|
||||
padding: 32px 28px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 1.5rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 18px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>AI Agent 登录</h1>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
</div>
|
||||
<button id="login-btn">登录</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
还没有账号?<a href="/register">点击注册</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('login-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
if (!email || !password) {
|
||||
errorEl.textContent = '请输入邮箱和密码';
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>注册 - AI Agent</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f0eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.register-card {
|
||||
width: 380px;
|
||||
padding: 32px 30px;
|
||||
background: #fffefc;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 16px 45px rgba(118, 103, 84, 0.18);
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
color: #4b392c;
|
||||
}
|
||||
.form-group {
|
||||
text-align: left;
|
||||
margin-top: 16px;
|
||||
}
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(118, 103, 84, 0.3);
|
||||
font-size: 0.95rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
button {
|
||||
margin-top: 22px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #d8894c;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
margin-top: 14px;
|
||||
color: #c0392b;
|
||||
font-size: 0.85rem;
|
||||
min-height: 1em;
|
||||
}
|
||||
.link {
|
||||
margin-top: 18px;
|
||||
font-size: 0.85rem;
|
||||
color: #6c5a4a;
|
||||
}
|
||||
.link a {
|
||||
color: #d8894c;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="register-card">
|
||||
<h1>创建账号</h1>
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" placeholder="仅限小写字母/数字/下划线" autocomplete="username" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" autocomplete="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="invite">邀请码</label>
|
||||
<input type="text" id="invite" placeholder="必填" autocomplete="off" />
|
||||
</div>
|
||||
<button id="register-btn">注册</button>
|
||||
<div class="error" id="error"></div>
|
||||
<div class="link">
|
||||
已有账号?<a href="/login">返回登录</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const btn = document.getElementById('register-btn');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
async function register() {
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const invite = document.getElementById('invite').value.trim();
|
||||
|
||||
if (!username || !email || !password || !invite) {
|
||||
errorEl.textContent = '请完整填写所有字段';
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
errorEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch('/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email, password, invite_code: invite })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
errorEl.textContent = data.error || '注册失败';
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请稍后再试';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', register);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
register();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,803 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||||
|
||||
<!-- xterm.js 终端模拟器 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部区域 */
|
||||
.header {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 15px 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||||
}
|
||||
|
||||
.status-indicator.disconnected {
|
||||
background: #ef4444;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* 会话标签栏 */
|
||||
.session-tabs {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 终端容器 */
|
||||
.terminal-wrapper {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
background: #2d2d2d;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.control-btn.close { background: #ff5f56; }
|
||||
.control-btn.minimize { background: #ffbd2e; }
|
||||
.control-btn.maximize { background: #27c93f; }
|
||||
|
||||
#terminal {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
padding-bottom: 30px; /* 增加底部内边距 */
|
||||
overflow-y: auto; /* 确保可以滚动 */
|
||||
}
|
||||
|
||||
/* 侧边栏信息 */
|
||||
.sidebar {
|
||||
width: 300px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-section h3 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 命令历史 */
|
||||
.command-history {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #4ade80;
|
||||
border-left: 2px solid #4ade80;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* 底部状态栏 */
|
||||
.status-bar {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-left {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loader {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>
|
||||
🖥️ AI Terminal Monitor
|
||||
<span class="status-indicator" id="connectionStatus"></span>
|
||||
<span style="font-size: 14px; font-weight: normal; margin-left: 10px;" id="connectionText">连接中...</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 会话标签 -->
|
||||
<div class="session-tabs" id="sessionTabs">
|
||||
<div class="tab" onclick="createNewSession()">
|
||||
<span class="tab-icon">➕</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 终端容器 -->
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-title">
|
||||
<span id="terminalSession">无活动会话</span>
|
||||
<span id="terminalPath" style="color: #666;">-</span>
|
||||
</div>
|
||||
<div class="terminal-controls">
|
||||
<div class="control-btn close" title="关闭"></div>
|
||||
<div class="control-btn minimize" title="最小化"></div>
|
||||
<div class="control-btn maximize" title="最大化"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="terminal"></div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<!-- 会话信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📊 会话信息</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">会话名称</span>
|
||||
<span class="info-value" id="sessionName">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">工作目录</span>
|
||||
<span class="info-value" id="workingDir">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Shell类型</span>
|
||||
<span class="info-value" id="shellType">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">运行时间</span>
|
||||
<span class="info-value" id="uptime">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">缓冲区</span>
|
||||
<span class="info-value" id="bufferSize">0 KB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 命令历史 -->
|
||||
<div class="info-section">
|
||||
<h3>📝 最近命令</h3>
|
||||
<div class="command-history" id="commandHistory">
|
||||
<div style="color: #666; text-align: center;">暂无命令</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="info-section">
|
||||
<h3>📈 统计</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">总命令数</span>
|
||||
<span class="info-value" id="commandCount">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">输出行数</span>
|
||||
<span class="info-value" id="outputLines">0</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">活动会话</span>
|
||||
<span class="info-value" id="activeCount">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-left">
|
||||
<div class="status-item">
|
||||
<span>⚡</span>
|
||||
<span id="latency">延迟: 0ms</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>📡</span>
|
||||
<span id="dataRate">0 KB/s</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>🕐</span>
|
||||
<span id="currentTime"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示框 -->
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<!-- 引入依赖 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||||
|
||||
<script>
|
||||
// 全局变量
|
||||
let term = null;
|
||||
let fitAddon = null;
|
||||
let socket = null;
|
||||
let currentSession = null;
|
||||
let sessions = {};
|
||||
let commandHistory = [];
|
||||
let stats = {
|
||||
commandCount: 0,
|
||||
outputLines: 0,
|
||||
dataReceived: 0,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// 初始化终端
|
||||
function initTerminal() {
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#aeafad',
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5'
|
||||
},
|
||||
scrollback: 10000,
|
||||
cols: 120, // 设置固定列数
|
||||
rows: 30, // 设置固定行数
|
||||
convertEol: true, // 转换行尾
|
||||
wordSeparator: ' ()[]{}\'"', // 单词分隔符
|
||||
rendererType: 'canvas', // 使用canvas渲染器
|
||||
allowTransparency: false
|
||||
});
|
||||
|
||||
// 添加插件
|
||||
fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||||
term.loadAddon(webLinksAddon);
|
||||
|
||||
// 打开终端
|
||||
term.open(document.getElementById('terminal'));
|
||||
|
||||
// 延迟执行fit以确保正确计算尺寸
|
||||
setTimeout(() => {
|
||||
fitAddon.fit();
|
||||
console.log(`终端尺寸: ${term.cols}x${term.rows}`);
|
||||
}, 100);
|
||||
|
||||
// 显示欢迎信息
|
||||
term.writeln('\x1b[1;36m╔════════════════════════════════════════╗\x1b[0m');
|
||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||
term.writeln('');
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
function initWebSocket() {
|
||||
socket = io();
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket连接成功');
|
||||
updateConnectionStatus(true);
|
||||
term.writeln('\x1b[32m✓ 已连接到服务器\x1b[0m');
|
||||
|
||||
// 订阅所有终端事件
|
||||
socket.emit('terminal_subscribe', { all: true });
|
||||
console.log('已发送终端订阅请求');
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
socket.on('disconnect', () => {
|
||||
console.log('WebSocket连接断开');
|
||||
updateConnectionStatus(false);
|
||||
term.writeln('\x1b[31m✗ 与服务器断开连接\x1b[0m');
|
||||
});
|
||||
|
||||
// 订阅成功确认
|
||||
socket.on('terminal_subscribed', (data) => {
|
||||
console.log('终端订阅成功:', data);
|
||||
term.writeln('\x1b[32m✓ 已订阅终端事件\x1b[0m');
|
||||
});
|
||||
|
||||
// 终端启动事件
|
||||
socket.on('terminal_started', (data) => {
|
||||
console.log('终端启动:', data);
|
||||
addSession(data.session, data);
|
||||
term.writeln(`\x1b[32m[终端启动]\x1b[0m ${data.session} - ${data.working_dir}`);
|
||||
});
|
||||
|
||||
// 终端列表更新
|
||||
socket.on('terminal_list_update', (data) => {
|
||||
console.log('终端列表更新:', data);
|
||||
if (data.terminals && data.terminals.length > 0) {
|
||||
// 清除所有会话并重新添加
|
||||
sessions = {};
|
||||
for (const terminal of data.terminals) {
|
||||
sessions[terminal.name] = {
|
||||
working_dir: terminal.working_dir,
|
||||
is_running: terminal.is_running,
|
||||
shell: 'bash'
|
||||
};
|
||||
}
|
||||
updateSessionTabs();
|
||||
if (data.active && !currentSession) {
|
||||
switchToSession(data.active);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输出事件
|
||||
socket.on('terminal_output', (data) => {
|
||||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 直接写入原始输出
|
||||
term.write(data.data);
|
||||
stats.outputLines++;
|
||||
stats.dataReceived += data.data.length;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端输入事件(AI发送的命令)
|
||||
socket.on('terminal_input', (data) => {
|
||||
console.log('收到终端输入:', data.session, data.data);
|
||||
if (data.session === currentSession || !currentSession) {
|
||||
// 如果没有当前会话,自动切换到这个会话
|
||||
if (!currentSession && data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: 'unknown'
|
||||
});
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
// 用绿色显示输入的命令
|
||||
term.write(`\x1b[1;32m➜ ${data.data}\x1b[0m`);
|
||||
|
||||
// 添加到命令历史
|
||||
addCommandToHistory(data.data.trim());
|
||||
stats.commandCount++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
// 终端关闭事件
|
||||
socket.on('terminal_closed', (data) => {
|
||||
console.log('终端关闭:', data);
|
||||
removeSession(data.session);
|
||||
term.writeln(`\x1b[31m[终端关闭]\x1b[0m ${data.session}`);
|
||||
});
|
||||
|
||||
// 终端重置事件
|
||||
socket.on('terminal_reset', (data) => {
|
||||
console.log('终端重置:', data);
|
||||
if (data.session) {
|
||||
if (!sessions[data.session]) {
|
||||
addSession(data.session, {
|
||||
session: data.session,
|
||||
working_dir: data.working_dir || 'unknown'
|
||||
});
|
||||
} else {
|
||||
sessions[data.session].working_dir = data.working_dir || sessions[data.session].working_dir;
|
||||
}
|
||||
switchToSession(data.session);
|
||||
}
|
||||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||||
resetCommandHistory();
|
||||
stats.commandCount = 0;
|
||||
stats.outputLines = 0;
|
||||
stats.dataReceived = 0;
|
||||
stats.startTime = Date.now();
|
||||
updateStats();
|
||||
});
|
||||
|
||||
// 终端切换事件
|
||||
socket.on('terminal_switched', (data) => {
|
||||
console.log('终端切换:', data);
|
||||
switchToSession(data.current);
|
||||
});
|
||||
|
||||
// 调试:监听所有事件
|
||||
const onevent = socket.onevent;
|
||||
socket.onevent = function (packet) {
|
||||
const args = packet.data || [];
|
||||
console.log('Socket事件:', args[0], args[1]);
|
||||
onevent.call(this, packet);
|
||||
};
|
||||
}
|
||||
|
||||
// 添加会话
|
||||
function addSession(name, info) {
|
||||
sessions[name] = info;
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是第一个会话,自动切换
|
||||
if (!currentSession) {
|
||||
switchToSession(name);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除会话
|
||||
function removeSession(name) {
|
||||
delete sessions[name];
|
||||
updateSessionTabs();
|
||||
|
||||
// 如果是当前会话,切换到其他会话
|
||||
if (currentSession === name) {
|
||||
const remaining = Object.keys(sessions);
|
||||
if (remaining.length > 0) {
|
||||
switchToSession(remaining[0]);
|
||||
} else {
|
||||
currentSession = null;
|
||||
updateSessionInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换会话
|
||||
function switchToSession(name) {
|
||||
currentSession = name;
|
||||
updateSessionTabs();
|
||||
updateSessionInfo();
|
||||
|
||||
// 清空终端并显示切换信息
|
||||
term.clear();
|
||||
term.writeln(`\x1b[36m[切换到终端: ${name}]\x1b[0m`);
|
||||
term.writeln('');
|
||||
|
||||
// 请求该会话的历史输出
|
||||
socket.emit('get_terminal_output', { session: name });
|
||||
}
|
||||
|
||||
// 更新会话标签
|
||||
function updateSessionTabs() {
|
||||
const container = document.getElementById('sessionTabs');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (Object.keys(sessions).length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="tab">
|
||||
<span class="tab-icon">⏳</span>
|
||||
<span>等待终端会话...</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, info] of Object.entries(sessions)) {
|
||||
const tab = document.createElement('div');
|
||||
tab.className = `tab ${name === currentSession ? 'active' : ''}`;
|
||||
tab.innerHTML = `
|
||||
<span class="tab-icon">📟</span>
|
||||
<span>${name}</span>
|
||||
`;
|
||||
tab.onclick = () => switchToSession(name);
|
||||
container.appendChild(tab);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新会话信息
|
||||
function updateSessionInfo() {
|
||||
if (!currentSession || !sessions[currentSession]) {
|
||||
document.getElementById('sessionName').textContent = '-';
|
||||
document.getElementById('workingDir').textContent = '-';
|
||||
document.getElementById('shellType').textContent = '-';
|
||||
document.getElementById('terminalSession').textContent = '无活动会话';
|
||||
document.getElementById('terminalPath').textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
const info = sessions[currentSession];
|
||||
document.getElementById('sessionName').textContent = currentSession;
|
||||
document.getElementById('workingDir').textContent = info.working_dir || '-';
|
||||
document.getElementById('shellType').textContent = info.shell || '-';
|
||||
document.getElementById('terminalSession').textContent = currentSession;
|
||||
document.getElementById('terminalPath').textContent = info.working_dir || '-';
|
||||
}
|
||||
|
||||
// 添加命令到历史
|
||||
function addCommandToHistory(command) {
|
||||
commandHistory.unshift(command);
|
||||
if (commandHistory.length > 20) {
|
||||
commandHistory.pop();
|
||||
}
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
function resetCommandHistory() {
|
||||
commandHistory = [];
|
||||
updateCommandHistory();
|
||||
}
|
||||
|
||||
// 更新命令历史显示
|
||||
function updateCommandHistory() {
|
||||
const container = document.getElementById('commandHistory');
|
||||
if (commandHistory.length === 0) {
|
||||
container.innerHTML = '<div style="color: #666; text-align: center;">暂无命令</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = commandHistory
|
||||
.map(cmd => `<div class="command-item">${cmd}</div>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
document.getElementById('commandCount').textContent = stats.commandCount;
|
||||
document.getElementById('outputLines').textContent = stats.outputLines;
|
||||
document.getElementById('activeCount').textContent = Object.keys(sessions).length;
|
||||
document.getElementById('bufferSize').textContent = (stats.dataReceived / 1024).toFixed(1) + ' KB';
|
||||
|
||||
// 更新运行时间
|
||||
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
||||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
document.getElementById('uptime').textContent =
|
||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(connected) {
|
||||
const indicator = document.getElementById('connectionStatus');
|
||||
const text = document.getElementById('connectionText');
|
||||
|
||||
if (connected) {
|
||||
indicator.classList.remove('disconnected');
|
||||
text.textContent = '已连接';
|
||||
text.style.color = '#4ade80';
|
||||
} else {
|
||||
indicator.classList.add('disconnected');
|
||||
text.textContent = '未连接';
|
||||
text.style.color = '#ef4444';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
document.getElementById('currentTime').textContent =
|
||||
now.toLocaleTimeString('zh-CN');
|
||||
}
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {
|
||||
if (fitAddon) {
|
||||
fitAddon.fit();
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTerminal();
|
||||
initWebSocket();
|
||||
|
||||
// 定时更新
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
setInterval(updateStats, 1000);
|
||||
updateCurrentTime();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -46,6 +46,9 @@ from config import (
|
||||
AGENT_VERSION,
|
||||
SUB_AGENT_MAX_ACTIVE,
|
||||
SUB_AGENT_DEFAULT_TIMEOUT,
|
||||
SUB_AGENT_STATE_FILE,
|
||||
DATA_DIR,
|
||||
SUB_AGENT_TASKS_BASE_DIR,
|
||||
)
|
||||
from modules.user_manager import UserManager, UserWorkspace
|
||||
from modules.gui_file_manager import GuiFileManager
|
||||
@ -173,28 +176,207 @@ def format_tool_result_notice(tool_name: str, tool_call_id: Optional[str], conte
|
||||
return f"{header}\n{body}"
|
||||
|
||||
|
||||
def _format_relative_path(path: Optional[str], workspace: Optional[str]) -> str:
|
||||
"""将绝对路径转换为相对 workspace 的表示,默认返回原始路径。"""
|
||||
if not path:
|
||||
return ""
|
||||
try:
|
||||
target = Path(path).resolve()
|
||||
if workspace:
|
||||
base = Path(workspace).resolve()
|
||||
rel = target.relative_to(base)
|
||||
rel_text = rel.as_posix()
|
||||
if not rel_text or rel_text == ".":
|
||||
return "."
|
||||
return f"./{rel_text}"
|
||||
except Exception:
|
||||
pass
|
||||
return Path(path).as_posix()
|
||||
|
||||
|
||||
def build_sub_agent_instruction(meta: Dict[str, Any]) -> str:
|
||||
"""根据任务元数据构建初始指令消息。"""
|
||||
workspace_dir = meta.get("workspace_dir")
|
||||
references_dir = meta.get("references_dir")
|
||||
deliverables_dir = meta.get("deliverables_dir")
|
||||
workspace_label = _format_relative_path(workspace_dir, workspace_dir) or "."
|
||||
references_label = _format_relative_path(references_dir, workspace_dir) or "references/"
|
||||
deliverables_label = _format_relative_path(deliverables_dir, workspace_dir) or "deliverables/"
|
||||
|
||||
lines = [
|
||||
f"子智能体任务 - 代号 {meta.get('agent_id')} (task_id={meta.get('task_id')})",
|
||||
"",
|
||||
meta.get("summary", ""),
|
||||
meta.get("task", ""),
|
||||
"",
|
||||
"工作目录:",
|
||||
f"- workspace: {meta.get('workspace_dir')}",
|
||||
f"- references: {meta.get('references_dir')}",
|
||||
f"- deliverables: {meta.get('deliverables_dir')}",
|
||||
"工作区与目录说明:",
|
||||
f"- workspace: {workspace_label}(唯一可写根目录,所有操作只能在此路径内进行)",
|
||||
f"- references: {references_label}(系统已自动创建,仅供查阅,不得修改原文件)",
|
||||
f"- deliverables: {deliverables_label}(系统已自动创建,用于存放交付文件与 result.md)",
|
||||
"",
|
||||
"请立即分析任务并开始执行。必要时可向主智能体请求澄清。",
|
||||
"交付要求:",
|
||||
"- 所有成果放入 deliverables/ 下,保持清晰的目录结构;",
|
||||
"- deliverables/ 必须包含 result.md,说明完成情况、交付列表、遗留风险和下一步建议;",
|
||||
"- references/ 只读,如需引用请复制到 workspace/ 再处理;",
|
||||
"- 子智能体无法与主智能体实时沟通,如信息不足请在 result.md 中说明。",
|
||||
"- 最后必须调用 finish_sub_agent,并在 reason 中概括完成情况;若暂未完成,也要说明阻塞原因并继续执行直至可交付。",
|
||||
"",
|
||||
"提示:references/ 与 deliverables/ 目录均已自动创建,如需额外子目录请在 workspace/ 内自行管理。",
|
||||
"",
|
||||
"请立即分析任务、规划步骤并开始执行,过程中记录关键操作,确保交付即可直接被主智能体使用。",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_finish_tool_reminder(meta: Dict[str, Any]) -> str:
|
||||
"""构建提醒子智能体调用结束工具的提示语。"""
|
||||
workspace_dir = meta.get("workspace_dir")
|
||||
deliverables_dir = meta.get("deliverables_dir")
|
||||
deliverables_label = _format_relative_path(deliverables_dir, workspace_dir) or "deliverables"
|
||||
if deliverables_label in {"", "."}:
|
||||
result_path = "./result.md"
|
||||
else:
|
||||
normalized = deliverables_label.rstrip("/")
|
||||
result_path = f"{normalized}/result.md"
|
||||
return (
|
||||
f"⚠️ 检测到你已停止输出,但尚未调用 finish_sub_agent。请确认 {deliverables_label} 内的交付文件与 {result_path} 已准备完毕,"
|
||||
"再调用 finish_sub_agent(reason=...) 正式结束任务;如果任务仍未完成,请继续完成剩余步骤并在完成后立即调用 finish_sub_agent。"
|
||||
)
|
||||
|
||||
|
||||
def get_active_sub_agent_count() -> int:
|
||||
return len([task for task in sub_agent_tasks.values() if task.get("status") in {"pending", "running"}])
|
||||
|
||||
|
||||
def find_sub_agent_conversation_file(conv_id: str) -> Optional[Path]:
|
||||
"""在已知目录中搜索子智能体对话文件。"""
|
||||
possible_dirs = []
|
||||
tasks_root = Path(SUB_AGENT_TASKS_BASE_DIR).expanduser().resolve()
|
||||
if tasks_root.exists():
|
||||
possible_dirs.append(tasks_root)
|
||||
data_root = Path(DATA_DIR).expanduser().resolve()
|
||||
if data_root.exists():
|
||||
possible_dirs.append(data_root)
|
||||
users_root = Path("users").resolve()
|
||||
if users_root.exists():
|
||||
possible_dirs.append(users_root)
|
||||
|
||||
for base in possible_dirs:
|
||||
try:
|
||||
matches = list(base.rglob(f"{conv_id}.json"))
|
||||
except Exception:
|
||||
matches = []
|
||||
for match in matches:
|
||||
try:
|
||||
if match.name == f"{conv_id}.json":
|
||||
return match
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def build_workspace_tree(root_path: str, max_depth: int = 5) -> Dict[str, Any]:
|
||||
"""构建工作目录的文件树,用于子智能体只读视图。"""
|
||||
root = Path(root_path).expanduser().resolve()
|
||||
if not root.exists():
|
||||
return {
|
||||
"path": str(root),
|
||||
"tree": {},
|
||||
"folders": [],
|
||||
"files": [],
|
||||
"total_files": 0,
|
||||
"total_size": 0
|
||||
}
|
||||
|
||||
structure = {
|
||||
"path": str(root),
|
||||
"tree": {},
|
||||
"folders": [],
|
||||
"files": [],
|
||||
"total_files": 0,
|
||||
"total_size": 0
|
||||
}
|
||||
|
||||
def scan_directory(path: Path, tree: Dict[str, Any], depth: int = 0):
|
||||
if depth > max_depth:
|
||||
return
|
||||
try:
|
||||
entries = sorted(
|
||||
[p for p in path.iterdir() if not p.name.startswith('.')],
|
||||
key=lambda p: (not p.is_dir(), p.name.lower())
|
||||
)
|
||||
except PermissionError:
|
||||
return
|
||||
|
||||
for entry in entries:
|
||||
relative_path = str(entry.relative_to(root))
|
||||
if entry.is_dir():
|
||||
structure["folders"].append({
|
||||
"name": entry.name,
|
||||
"path": relative_path
|
||||
})
|
||||
tree[entry.name] = {
|
||||
"type": "folder",
|
||||
"path": relative_path,
|
||||
"children": {}
|
||||
}
|
||||
scan_directory(entry, tree[entry.name]["children"], depth + 1)
|
||||
else:
|
||||
try:
|
||||
size = entry.stat().st_size
|
||||
modified = datetime.fromtimestamp(entry.stat().st_mtime).isoformat()
|
||||
except OSError:
|
||||
size = 0
|
||||
modified = ""
|
||||
structure["files"].append({
|
||||
"name": entry.name,
|
||||
"path": relative_path,
|
||||
"size": size,
|
||||
"modified": modified
|
||||
})
|
||||
structure["total_files"] += 1
|
||||
structure["total_size"] += size
|
||||
tree[entry.name] = {
|
||||
"type": "file",
|
||||
"path": relative_path,
|
||||
"size": size,
|
||||
"modified": modified
|
||||
}
|
||||
|
||||
scan_directory(root, structure["tree"])
|
||||
return structure
|
||||
|
||||
|
||||
def _load_state_data() -> Dict[str, Any]:
|
||||
state_file = Path(SUB_AGENT_STATE_FILE).expanduser().resolve()
|
||||
if not state_file.exists():
|
||||
return {}
|
||||
try:
|
||||
with state_file.open('r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def load_persisted_task(task_id: str) -> Optional[Dict[str, Any]]:
|
||||
data = _load_state_data()
|
||||
tasks = data.get("tasks") or {}
|
||||
return tasks.get(task_id)
|
||||
|
||||
|
||||
def iter_persisted_tasks():
|
||||
data = _load_state_data()
|
||||
tasks = data.get("tasks") or {}
|
||||
return list(tasks.values())
|
||||
|
||||
|
||||
def get_task_record(task_id: str) -> Optional[Dict[str, Any]]:
|
||||
task = sub_agent_tasks.get(task_id)
|
||||
if task:
|
||||
return task
|
||||
return load_persisted_task(task_id)
|
||||
|
||||
|
||||
def broadcast_sub_agent_event(task_id: str, event_type: str, payload: Optional[Dict[str, Any]] = None):
|
||||
room = f"sub_agent_{task_id}"
|
||||
data = {"task_id": task_id}
|
||||
@ -231,6 +413,23 @@ def _normalize_conversation_id(value: Optional[str]) -> Optional[str]:
|
||||
return value if value.startswith("conv_") else f"conv_{value}"
|
||||
|
||||
|
||||
def _extract_parent_conversation(info: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
"""尝试从任务记录中提取父对话ID。"""
|
||||
if not info:
|
||||
return None
|
||||
service_payload = info.get("service_payload") or {}
|
||||
candidates = [
|
||||
info.get("parent_conversation_id"),
|
||||
info.get("conversation_id"),
|
||||
service_payload.get("parent_conversation_id"),
|
||||
]
|
||||
for candidate in candidates:
|
||||
normalized = _normalize_conversation_id(candidate)
|
||||
if normalized:
|
||||
return normalized
|
||||
return None
|
||||
|
||||
|
||||
def _inject_sub_agent_script(html: str, task_id: str, parent_conv: Optional[str], sub_conv: Optional[str]) -> str:
|
||||
script = (
|
||||
"<script>"
|
||||
@ -257,21 +456,40 @@ def _resolve_task_by_conv(conv_slug: Optional[str], task_label: str):
|
||||
suffix = task_label[len("sub_agent"):]
|
||||
if suffix.isdigit():
|
||||
agent_id = int(suffix)
|
||||
record = None
|
||||
if normalized_conv:
|
||||
for info in sub_agent_tasks.values():
|
||||
if _normalize_conversation_id(info.get("parent_conversation_id")) != normalized_conv:
|
||||
continue
|
||||
if agent_id is not None and info.get("agent_id") == agent_id:
|
||||
record = info
|
||||
break
|
||||
if info.get("task_id") == task_label:
|
||||
record = info
|
||||
break
|
||||
state_data = _load_state_data()
|
||||
stored_tasks = state_data.get("tasks") or {}
|
||||
conv_map = state_data.get("conversation_agents") or {}
|
||||
|
||||
def _matches(info: Optional[Dict[str, Any]]) -> bool:
|
||||
if not info:
|
||||
return False
|
||||
parent_conv = _extract_parent_conversation(info)
|
||||
if normalized_conv and parent_conv != normalized_conv:
|
||||
return False
|
||||
if agent_id is not None and info.get("agent_id") != agent_id:
|
||||
return False
|
||||
if not normalized_conv and agent_id is None and info.get("task_id") != task_label:
|
||||
return False
|
||||
return True
|
||||
|
||||
record = sub_agent_tasks.get(task_label) or stored_tasks.get(task_label)
|
||||
|
||||
if not record:
|
||||
record = sub_agent_tasks.get(task_label)
|
||||
record = next((info for info in sub_agent_tasks.values() if _matches(info)), None)
|
||||
|
||||
if not record:
|
||||
record = next((info for info in stored_tasks.values() if _matches(info)), None)
|
||||
|
||||
if not record and normalized_conv and agent_id is not None:
|
||||
agent_list = conv_map.get(normalized_conv, [])
|
||||
if agent_list and agent_id in agent_list:
|
||||
for info in stored_tasks.values():
|
||||
if info.get("agent_id") == agent_id and _extract_parent_conversation(info) == normalized_conv:
|
||||
record = info
|
||||
break
|
||||
|
||||
actual_task_id = record.get("task_id") if record else task_label
|
||||
parent_conv = _normalize_conversation_id(record.get("parent_conversation_id")) if record else normalized_conv
|
||||
parent_conv = _extract_parent_conversation(record) or normalized_conv
|
||||
sub_conv = record.get("sub_conversation_id") if record else None
|
||||
return actual_task_id, parent_conv, sub_conv
|
||||
|
||||
@ -446,7 +664,7 @@ def terminal_broadcast(event_type, data):
|
||||
"""广播终端事件到所有订阅者"""
|
||||
try:
|
||||
# 对于全局事件,发送给所有连接的客户端
|
||||
if event_type in ('token_update', 'todo_updated'):
|
||||
if event_type in ('todo_updated',):
|
||||
socketio.emit(event_type, data) # 全局广播,不限制房间
|
||||
debug_log(f"全局广播{event_type}: {data}")
|
||||
else:
|
||||
@ -1822,9 +2040,9 @@ def create_sub_agent_task(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
initial_message = build_sub_agent_instruction(metadata)
|
||||
terminal.context_manager.add_conversation("user", initial_message)
|
||||
|
||||
sender = make_sub_agent_sender(task_id)
|
||||
terminal.context_manager._web_terminal_callback = sender
|
||||
client_sid = f"sub_agent::{task_id}::{int(time.time()*1000)}"
|
||||
update_sub_agent_task(task_id, last_client_sid=client_sid)
|
||||
socketio.start_background_task(process_message_task, terminal, initial_message, sender, client_sid)
|
||||
@ -1846,7 +2064,6 @@ def send_message_to_sub_agent(task_id: str, message: str) -> Dict[str, Any]:
|
||||
if sub_agent_tasks.get(task_id, {}).get("status") in {"completed", "failed", "timeout"}:
|
||||
return {"success": False, "error": "任务已结束,无法继续发送消息"}
|
||||
|
||||
terminal.context_manager.add_conversation("user", message)
|
||||
sender = make_sub_agent_sender(task_id)
|
||||
client_sid = f"sub_agent::{task_id}::{int(time.time()*1000)}"
|
||||
update_sub_agent_task(task_id, last_client_sid=client_sid)
|
||||
@ -1880,14 +2097,15 @@ def api_create_sub_agent_task():
|
||||
|
||||
@app.route('/tasks/<task_id>', methods=['GET'])
|
||||
def api_get_sub_agent_task(task_id: str):
|
||||
task = sub_agent_tasks.get(task_id)
|
||||
task = get_task_record(task_id)
|
||||
from_store = task_id not in sub_agent_tasks
|
||||
if not task:
|
||||
return jsonify({"success": False, "status": "unknown", "message": "任务不存在"}), 404
|
||||
payload = {
|
||||
"success": True,
|
||||
"task_id": task_id,
|
||||
"status": task.get("status"),
|
||||
"message": task.get("error") or task.get("completion_reason") or "",
|
||||
"status": task.get("status") or (task.get("final_result") or {}).get("status") or "unknown",
|
||||
"message": task.get("error") or task.get("completion_reason") or (task.get("final_result") or {}).get("message") or "",
|
||||
"deliverables_dir": task.get("deliverables_dir"),
|
||||
"workspace_dir": task.get("workspace_dir"),
|
||||
"references_dir": task.get("references_dir"),
|
||||
@ -1899,6 +2117,8 @@ def api_get_sub_agent_task(task_id: str):
|
||||
}
|
||||
if task.get("status") == "completed":
|
||||
payload["message"] = task.get("completion_reason") or "子智能体已完成任务"
|
||||
if from_store:
|
||||
payload["archived"] = True
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
@ -1922,16 +2142,23 @@ def api_stop_sub_agent(task_id: str):
|
||||
|
||||
@app.route('/tasks/<task_id>/conversation', methods=['GET'])
|
||||
def api_get_sub_agent_conversation(task_id: str):
|
||||
info = sub_agent_tasks.get(task_id)
|
||||
info = get_task_record(task_id)
|
||||
from_store = task_id not in sub_agent_tasks
|
||||
if not info:
|
||||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||||
conv_id = info.get("sub_conversation_id")
|
||||
conv_id = info.get("sub_conversation_id") or info.get("conversation_id")
|
||||
if not conv_id:
|
||||
return jsonify({"success": True, "conversation_id": None, "messages": []})
|
||||
data_dir = Path(info.get("conversation_data_dir") or "").expanduser()
|
||||
conv_file = data_dir / "conversations" / f"{conv_id}.json"
|
||||
if not conv_file.exists():
|
||||
return jsonify({"success": True, "conversation_id": conv_id, "messages": []})
|
||||
conv_file = None
|
||||
if not from_store:
|
||||
data_dir = Path(info.get("conversation_data_dir") or "").expanduser()
|
||||
candidate = data_dir / "conversations" / f"{conv_id}.json"
|
||||
if candidate.exists():
|
||||
conv_file = candidate
|
||||
if conv_file is None:
|
||||
conv_file = find_sub_agent_conversation_file(conv_id)
|
||||
if conv_file is None:
|
||||
return jsonify({"success": True, "conversation_id": conv_id, "messages": []})
|
||||
try:
|
||||
content = json.loads(conv_file.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
@ -1944,10 +2171,46 @@ def api_get_sub_agent_conversation(task_id: str):
|
||||
})
|
||||
|
||||
|
||||
@app.route('/sub_agent/conversations/<conv_id>', methods=['GET'])
|
||||
def api_get_archived_sub_agent_conversation(conv_id: str):
|
||||
"""按conversation_id直接读取历史子智能体对话文件。"""
|
||||
normalized = _normalize_conversation_id(conv_id)
|
||||
file_path = find_sub_agent_conversation_file(normalized)
|
||||
if not file_path or not file_path.exists():
|
||||
return jsonify({"success": False, "error": "对话不存在"}), 404
|
||||
try:
|
||||
content = json.loads(file_path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": f"读取对话失败: {exc}"}), 500
|
||||
messages = content.get("messages", []) if isinstance(content, dict) else []
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"conversation_id": normalized,
|
||||
"messages": messages
|
||||
})
|
||||
|
||||
|
||||
@app.route('/tasks/<task_id>/files', methods=['GET'])
|
||||
def api_get_sub_agent_files(task_id: str):
|
||||
"""返回子智能体工作目录的文件树,只读展示使用。"""
|
||||
task = get_task_record(task_id)
|
||||
if not task:
|
||||
return jsonify({"success": False, "error": "任务不存在"}), 404
|
||||
workspace_dir = task.get("workspace_dir")
|
||||
if not workspace_dir:
|
||||
return jsonify({"success": False, "error": "任务未设置工作目录"}), 400
|
||||
try:
|
||||
tree = build_workspace_tree(workspace_dir)
|
||||
return jsonify({"success": True, "data": tree})
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": f'构建文件树失败: {exc}'}), 500
|
||||
|
||||
|
||||
@app.route('/sub_agent/<task_id>')
|
||||
def serve_sub_agent_page(task_id: str):
|
||||
"""子智能体监控页面(仅任务ID)。"""
|
||||
return _render_sub_agent_index(task_id, parent_conv=None, sub_conv=None)
|
||||
actual_id, parent_conv, sub_conv = _resolve_task_by_conv(None, task_id)
|
||||
return _render_sub_agent_index(actual_id, parent_conv, sub_conv)
|
||||
|
||||
|
||||
@app.route('/<conv_slug>/sub_agent_<task_label>')
|
||||
@ -2160,8 +2423,10 @@ def detect_malformed_tool_call(text):
|
||||
return False
|
||||
|
||||
async def handle_task_with_sender(terminal: WebTerminal, message, sender, client_sid):
|
||||
"""处理任务并发送消息 - 集成token统计版本"""
|
||||
"""处理任务并发送消息(子智能体Web终端)"""
|
||||
web_terminal = terminal
|
||||
finish_called = False
|
||||
finish_prompt_sent = False
|
||||
|
||||
# 如果是思考模式,重置状态
|
||||
if web_terminal.thinking_mode:
|
||||
@ -2170,7 +2435,7 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
# 添加到对话历史
|
||||
web_terminal.context_manager.add_conversation("user", message)
|
||||
|
||||
# === 移除:不在这里计算输入token,改为在每次API调用前计算 ===
|
||||
# === 子智能体版本:跳过额外统计 ===
|
||||
|
||||
# 构建上下文和消息(用于API调用)
|
||||
context = web_terminal.build_context()
|
||||
@ -2807,16 +3072,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
})
|
||||
break
|
||||
|
||||
# === 修改:每次API调用前都计算输入token ===
|
||||
try:
|
||||
input_tokens = web_terminal.context_manager.calculate_input_tokens(messages, tools)
|
||||
debug_log(f"第{iteration + 1}次API调用输入token: {input_tokens}")
|
||||
|
||||
# 更新输入token统计
|
||||
web_terminal.context_manager.update_token_statistics(input_tokens, 0)
|
||||
except Exception as e:
|
||||
debug_log(f"输入token统计失败: {e}")
|
||||
|
||||
full_response = ""
|
||||
tool_calls = []
|
||||
current_thinking = ""
|
||||
@ -3223,28 +3478,6 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
debug_log("任务在流处理完成后检测到停止状态")
|
||||
return
|
||||
|
||||
# === API响应完成后只计算输出token ===
|
||||
try:
|
||||
# 计算AI输出的token(包括thinking、文本内容、工具调用)
|
||||
ai_output_content = ""
|
||||
if current_thinking:
|
||||
ai_output_content += f"<think>\n{current_thinking}\n</think>\n"
|
||||
if full_response:
|
||||
ai_output_content += full_response
|
||||
if tool_calls:
|
||||
ai_output_content += json.dumps(tool_calls, ensure_ascii=False)
|
||||
|
||||
if ai_output_content.strip():
|
||||
output_tokens = web_terminal.context_manager.calculate_output_tokens(ai_output_content)
|
||||
debug_log(f"第{iteration + 1}次API调用输出token: {output_tokens}")
|
||||
|
||||
# 只更新输出token统计
|
||||
web_terminal.context_manager.update_token_statistics(0, output_tokens)
|
||||
else:
|
||||
debug_log("没有AI输出内容,跳过输出token统计")
|
||||
except Exception as e:
|
||||
debug_log(f"输出token统计失败: {e}")
|
||||
|
||||
# 流结束后的处理
|
||||
await flush_text_buffer(force=True)
|
||||
debug_log(f"\n流结束统计:")
|
||||
@ -3527,6 +3760,19 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
|
||||
if not tool_calls:
|
||||
debug_log("没有工具调用,结束迭代")
|
||||
if not finish_called and not finish_prompt_sent:
|
||||
reminder = build_finish_tool_reminder(getattr(web_terminal, "sub_agent_meta", {}))
|
||||
web_terminal.context_manager.add_conversation("user", reminder)
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": reminder
|
||||
})
|
||||
sender('system_message', {
|
||||
'content': reminder
|
||||
})
|
||||
finish_prompt_sent = True
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
break
|
||||
|
||||
# 检查连续相同工具调用
|
||||
@ -3586,6 +3832,8 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
||||
last_tool_call_time = time.time()
|
||||
|
||||
function_name = tool_call["function"]["name"]
|
||||
if function_name == "finish_sub_agent":
|
||||
finish_called = True
|
||||
arguments_str = tool_call["function"]["arguments"]
|
||||
tool_call_id = tool_call["id"]
|
||||
|
||||
@ -3869,62 +4117,6 @@ def handle_command(data):
|
||||
'message': f'未知命令: {cmd}'
|
||||
})
|
||||
|
||||
@app.route('/api/conversations/<conversation_id>/token-statistics', methods=['GET'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def get_conversation_token_statistics(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
"""获取特定对话的token统计"""
|
||||
try:
|
||||
stats = terminal.context_manager.get_conversation_token_statistics(conversation_id)
|
||||
|
||||
if stats:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": stats
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Conversation not found",
|
||||
"message": f"对话 {conversation_id} 不存在"
|
||||
}), 404
|
||||
|
||||
except Exception as e:
|
||||
print(f"[API] 获取token统计错误: {e}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "获取token统计时发生异常"
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/conversations/<conversation_id>/tokens', methods=['GET'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def get_conversation_tokens(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
"""获取对话的当前完整上下文token数(包含所有动态内容)"""
|
||||
try:
|
||||
# 获取当前聚焦文件状态
|
||||
focused_files = terminal.get_focused_files_info()
|
||||
|
||||
# 计算完整token
|
||||
tokens = terminal.context_manager.conversation_manager.calculate_conversation_tokens(
|
||||
conversation_id=conversation_id,
|
||||
context_manager=terminal.context_manager,
|
||||
focused_files=focused_files,
|
||||
terminal_content=""
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": tokens
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}), 500
|
||||
|
||||
def initialize_system(path: str, thinking_mode: bool = False):
|
||||
"""初始化系统(多用户版本仅负责写日志和配置)"""
|
||||
# 清空或创建调试日志
|
||||
|
||||
6408
web_server.log
6408
web_server.log
File diff suppressed because it is too large
Load Diff
1584
webapp.log
1584
webapp.log
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user