xshat-lite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -0
- package/bin/xshat.mjs +36 -0
- package/package.json +60 -0
- package/src/config/default.mjs +620 -0
- package/src/index.mjs +2865 -0
- package/src/modules/agent-executor.mjs +1184 -0
- package/src/modules/agent-helpers.mjs +132 -0
- package/src/modules/agent-loop.mjs +181 -0
- package/src/modules/agent.mjs +152 -0
- package/src/modules/api-chat.mjs +212 -0
- package/src/modules/browser.mjs +1384 -0
- package/src/modules/chat-helpers.mjs +275 -0
- package/src/modules/chat.mjs +589 -0
- package/src/modules/docs.mjs +88 -0
- package/src/modules/local-intent-router.mjs +280 -0
- package/src/modules/logger.mjs +111 -0
- package/src/modules/multi-agent.mjs +183 -0
- package/src/modules/navigation-recovery.mjs +35 -0
- package/src/modules/provider-debug.mjs +98 -0
- package/src/modules/provider-pool.mjs +66 -0
- package/src/modules/provider-runtime.mjs +26 -0
- package/src/modules/provider-strategies.mjs +102 -0
- package/src/modules/response-parser.mjs +511 -0
- package/src/modules/response-pipeline.mjs +211 -0
- package/src/modules/runtime-maintenance.mjs +195 -0
- package/src/modules/session-browser.mjs +180 -0
- package/src/modules/session.mjs +370 -0
- package/src/modules/settings-menu.mjs +367 -0
- package/src/modules/task-runner.mjs +511 -0
- package/src/modules/task-store.mjs +262 -0
- package/src/modules/ui.mjs +2204 -0
- package/src/utils/config.mjs +257 -0
- package/src/utils/key-input.mjs +86 -0
- package/src/utils/storage.mjs +55 -0
|
@@ -0,0 +1,1184 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { resolve, relative } from 'node:path';
|
|
3
|
+
import { exec as execCallback } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const exec = promisify(execCallback);
|
|
7
|
+
|
|
8
|
+
function normalizeAnswer(answer) {
|
|
9
|
+
return String(answer ?? '')
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isAffirmative(answer) {
|
|
15
|
+
return ['y', 'yes', 'ok', 'done', '是', '好', '执行', '确认'].includes(
|
|
16
|
+
normalizeAnswer(answer)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPathInside(rootDir, targetPath) {
|
|
21
|
+
const rel = relative(rootDir, targetPath);
|
|
22
|
+
return rel === '' || (!rel.startsWith('..') && !rel.includes(':'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveWorkspacePath(workspaceRoot, targetPath = '') {
|
|
26
|
+
const absolute = resolve(workspaceRoot, targetPath);
|
|
27
|
+
|
|
28
|
+
if (!isPathInside(workspaceRoot, absolute)) {
|
|
29
|
+
throw new Error(`目标路径超出工作区: ${targetPath}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return absolute;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function describeCommand(command) {
|
|
36
|
+
if (!command) {
|
|
37
|
+
return '执行模型建议的动作';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
switch (command.name) {
|
|
41
|
+
case 'open_url':
|
|
42
|
+
return `打开链接 ${command.args?.url || ''}`.trim();
|
|
43
|
+
case 'send_message':
|
|
44
|
+
return `发送消息: ${command.args?.content || ''}`.trim();
|
|
45
|
+
case 'handoff':
|
|
46
|
+
return '切换到人工接管流程';
|
|
47
|
+
case 'run_shell':
|
|
48
|
+
return `执行系统命令: ${command.args?.command || ''}`.trim();
|
|
49
|
+
case 'read_file':
|
|
50
|
+
return `读取文件: ${command.args?.path || ''}`.trim();
|
|
51
|
+
case 'file_exists':
|
|
52
|
+
return `检查文件是否存在: ${command.args?.path || ''}`.trim();
|
|
53
|
+
case 'list_files':
|
|
54
|
+
return `列出目录: ${command.args?.path || '.'}`.trim();
|
|
55
|
+
case 'write_file':
|
|
56
|
+
return `写入文件: ${command.args?.path || ''}`.trim();
|
|
57
|
+
case 'delete_file':
|
|
58
|
+
return `删除文件: ${command.args?.path || ''}`.trim();
|
|
59
|
+
case 'read_agent_page':
|
|
60
|
+
return '读取当前 Agent 浏览器页面内容';
|
|
61
|
+
case 'send_agent_page':
|
|
62
|
+
return '读取 Agent 页面并发送给当前对话';
|
|
63
|
+
case 'get_app_state':
|
|
64
|
+
return '读取应用程序内部状态';
|
|
65
|
+
case 'focus_chat_input':
|
|
66
|
+
return '聚焦当前聊天输入框';
|
|
67
|
+
case 'send_chat_input':
|
|
68
|
+
return '向当前聊天输入框填写内容';
|
|
69
|
+
case 'submit_current_input':
|
|
70
|
+
return '提交当前聊天输入框内容';
|
|
71
|
+
case 'start_new_chat':
|
|
72
|
+
return '在当前提供方页面中新建对话';
|
|
73
|
+
case 'click_selector':
|
|
74
|
+
return `点击选择器: ${command.args?.selector || ''}`.trim();
|
|
75
|
+
case 'type_selector':
|
|
76
|
+
return `向选择器输入内容: ${command.args?.selector || ''}`.trim();
|
|
77
|
+
case 'wait_for_text':
|
|
78
|
+
return `等待页面文本出现: ${command.args?.text || ''}`.trim();
|
|
79
|
+
case 'extract_selector_text':
|
|
80
|
+
return `提取选择器文本: ${command.args?.selector || ''}`.trim();
|
|
81
|
+
case 'wait_for_selector':
|
|
82
|
+
return `等待选择器出现: ${command.args?.selector || ''}`.trim();
|
|
83
|
+
case 'wait_for_navigation':
|
|
84
|
+
return '等待页面导航完成';
|
|
85
|
+
case 'hover_selector':
|
|
86
|
+
return `悬停选择器: ${command.args?.selector || ''}`.trim();
|
|
87
|
+
case 'press_key':
|
|
88
|
+
return `按下按键: ${command.args?.key || ''}`.trim();
|
|
89
|
+
case 'check_checkbox':
|
|
90
|
+
return `勾选复选框: ${command.args?.selector || ''}`.trim();
|
|
91
|
+
case 'uncheck_checkbox':
|
|
92
|
+
return `取消勾选复选框: ${command.args?.selector || ''}`.trim();
|
|
93
|
+
case 'select_option':
|
|
94
|
+
return `选择下拉项: ${command.args?.selector || ''}`.trim();
|
|
95
|
+
case 'scroll_page':
|
|
96
|
+
return '滚动 Agent 页面';
|
|
97
|
+
case 'screenshot_agent_page':
|
|
98
|
+
return `保存 Agent 页面截图: ${command.args?.path || ''}`.trim();
|
|
99
|
+
case 'inspect_dom':
|
|
100
|
+
return `检查页面 DOM: ${command.args?.selector || 'body'}`.trim();
|
|
101
|
+
default:
|
|
102
|
+
return `执行命令 ${command.name}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function summarizeCommand(command) {
|
|
107
|
+
if (!command) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (command.name) {
|
|
112
|
+
case 'open_url':
|
|
113
|
+
return [
|
|
114
|
+
{ label: '操作', value: '打开链接' },
|
|
115
|
+
{ label: '目标', value: command.args?.url || '(缺少 url)' }
|
|
116
|
+
];
|
|
117
|
+
case 'send_message':
|
|
118
|
+
return [
|
|
119
|
+
{ label: '操作', value: '发送消息' },
|
|
120
|
+
{ label: '内容', value: command.args?.content || '(缺少 content)' }
|
|
121
|
+
];
|
|
122
|
+
case 'handoff':
|
|
123
|
+
return [
|
|
124
|
+
{ label: '操作', value: '人工接管' },
|
|
125
|
+
{ label: '说明', value: '需要你手动完成页面交互' }
|
|
126
|
+
];
|
|
127
|
+
case 'run_shell':
|
|
128
|
+
return [
|
|
129
|
+
{ label: '操作', value: '系统命令' },
|
|
130
|
+
{ label: '命令', value: command.args?.command || '(缺少 command)' }
|
|
131
|
+
];
|
|
132
|
+
case 'read_file':
|
|
133
|
+
return [
|
|
134
|
+
{ label: '操作', value: '读取文件' },
|
|
135
|
+
{ label: '路径', value: command.args?.path || '(缺少 path)' }
|
|
136
|
+
];
|
|
137
|
+
case 'file_exists':
|
|
138
|
+
return [
|
|
139
|
+
{ label: '操作', value: '检查文件' },
|
|
140
|
+
{ label: '路径', value: command.args?.path || '(缺少 path)' }
|
|
141
|
+
];
|
|
142
|
+
case 'list_files':
|
|
143
|
+
return [
|
|
144
|
+
{ label: '操作', value: '列出目录' },
|
|
145
|
+
{ label: '路径', value: command.args?.path || '.' }
|
|
146
|
+
];
|
|
147
|
+
case 'write_file':
|
|
148
|
+
return [
|
|
149
|
+
{ label: '操作', value: '写入文件' },
|
|
150
|
+
{ label: '路径', value: command.args?.path || '(缺少 path)' }
|
|
151
|
+
];
|
|
152
|
+
case 'delete_file':
|
|
153
|
+
return [
|
|
154
|
+
{ label: '操作', value: '删除文件' },
|
|
155
|
+
{ label: '路径', value: command.args?.path || '(缺少 path)' }
|
|
156
|
+
];
|
|
157
|
+
case 'read_agent_page':
|
|
158
|
+
return [
|
|
159
|
+
{ label: '操作', value: '读取 Agent 页面' },
|
|
160
|
+
{ label: '来源', value: 'agent 浏览器通道' }
|
|
161
|
+
];
|
|
162
|
+
case 'send_agent_page':
|
|
163
|
+
return [
|
|
164
|
+
{ label: '操作', value: '发送 Agent 页面' },
|
|
165
|
+
{ label: '目标', value: '当前聊天上下文' }
|
|
166
|
+
];
|
|
167
|
+
case 'get_app_state':
|
|
168
|
+
return [
|
|
169
|
+
{ label: '操作', value: '读取应用状态' },
|
|
170
|
+
{ label: '范围', value: 'provider / session / browser / agent 配置' }
|
|
171
|
+
];
|
|
172
|
+
case 'focus_chat_input':
|
|
173
|
+
return [
|
|
174
|
+
{ label: '操作', value: '聚焦聊天输入框' },
|
|
175
|
+
{ label: '目标', value: '当前 provider 聊天页面' }
|
|
176
|
+
];
|
|
177
|
+
case 'send_chat_input':
|
|
178
|
+
return [
|
|
179
|
+
{ label: '操作', value: '填写聊天输入框' },
|
|
180
|
+
{ label: '内容', value: command.args?.text || command.args?.content || '(空内容)' }
|
|
181
|
+
];
|
|
182
|
+
case 'submit_current_input':
|
|
183
|
+
return [
|
|
184
|
+
{ label: '操作', value: '提交输入框' },
|
|
185
|
+
{ label: '目标', value: '当前 provider 聊天页面' }
|
|
186
|
+
];
|
|
187
|
+
case 'start_new_chat':
|
|
188
|
+
return [
|
|
189
|
+
{ label: '操作', value: '新建对话' },
|
|
190
|
+
{ label: '目标', value: '当前 provider 聊天页面' }
|
|
191
|
+
];
|
|
192
|
+
case 'click_selector':
|
|
193
|
+
return [
|
|
194
|
+
{ label: '操作', value: '点击选择器' },
|
|
195
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
196
|
+
];
|
|
197
|
+
case 'type_selector':
|
|
198
|
+
return [
|
|
199
|
+
{ label: '操作', value: '输入内容' },
|
|
200
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
201
|
+
];
|
|
202
|
+
case 'wait_for_text':
|
|
203
|
+
return [
|
|
204
|
+
{ label: '操作', value: '等待文本' },
|
|
205
|
+
{ label: '文本', value: command.args?.text || '(缺少 text)' }
|
|
206
|
+
];
|
|
207
|
+
case 'extract_selector_text':
|
|
208
|
+
return [
|
|
209
|
+
{ label: '操作', value: '提取文本' },
|
|
210
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
211
|
+
];
|
|
212
|
+
case 'wait_for_selector':
|
|
213
|
+
return [
|
|
214
|
+
{ label: '操作', value: '等待选择器' },
|
|
215
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
216
|
+
];
|
|
217
|
+
case 'wait_for_navigation':
|
|
218
|
+
return [
|
|
219
|
+
{ label: '操作', value: '等待导航' },
|
|
220
|
+
{ label: '状态', value: command.args?.state || 'domcontentloaded' }
|
|
221
|
+
];
|
|
222
|
+
case 'hover_selector':
|
|
223
|
+
return [
|
|
224
|
+
{ label: '操作', value: '悬停选择器' },
|
|
225
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
226
|
+
];
|
|
227
|
+
case 'press_key':
|
|
228
|
+
return [
|
|
229
|
+
{ label: '操作', value: '按下按键' },
|
|
230
|
+
{ label: '按键', value: command.args?.key || '(缺少 key)' }
|
|
231
|
+
];
|
|
232
|
+
case 'check_checkbox':
|
|
233
|
+
return [
|
|
234
|
+
{ label: '操作', value: '勾选复选框' },
|
|
235
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
236
|
+
];
|
|
237
|
+
case 'uncheck_checkbox':
|
|
238
|
+
return [
|
|
239
|
+
{ label: '操作', value: '取消勾选复选框' },
|
|
240
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
241
|
+
];
|
|
242
|
+
case 'select_option':
|
|
243
|
+
return [
|
|
244
|
+
{ label: '操作', value: '选择下拉项' },
|
|
245
|
+
{ label: '选择器', value: command.args?.selector || '(缺少 selector)' }
|
|
246
|
+
];
|
|
247
|
+
case 'scroll_page':
|
|
248
|
+
return [
|
|
249
|
+
{ label: '操作', value: '滚动页面' },
|
|
250
|
+
{ label: '目标', value: command.args?.selector || '窗口' }
|
|
251
|
+
];
|
|
252
|
+
case 'screenshot_agent_page':
|
|
253
|
+
return [
|
|
254
|
+
{ label: '操作', value: '保存截图' },
|
|
255
|
+
{ label: '路径', value: command.args?.path || '(缺少 path)' }
|
|
256
|
+
];
|
|
257
|
+
case 'inspect_dom':
|
|
258
|
+
return [
|
|
259
|
+
{ label: '操作', value: '检查 DOM' },
|
|
260
|
+
{ label: '根选择器', value: command.args?.selector || 'body' }
|
|
261
|
+
];
|
|
262
|
+
default:
|
|
263
|
+
return [
|
|
264
|
+
{ label: '操作', value: command.name },
|
|
265
|
+
{ label: '参数', value: JSON.stringify(command.args || {}) }
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getCommandRiskLevel(command) {
|
|
271
|
+
switch (command?.name) {
|
|
272
|
+
case 'delete_file':
|
|
273
|
+
return '高';
|
|
274
|
+
case 'write_file':
|
|
275
|
+
case 'run_shell':
|
|
276
|
+
case 'open_url':
|
|
277
|
+
case 'focus_chat_input':
|
|
278
|
+
case 'send_chat_input':
|
|
279
|
+
case 'submit_current_input':
|
|
280
|
+
case 'start_new_chat':
|
|
281
|
+
case 'type_selector':
|
|
282
|
+
case 'click_selector':
|
|
283
|
+
case 'press_key':
|
|
284
|
+
case 'check_checkbox':
|
|
285
|
+
case 'uncheck_checkbox':
|
|
286
|
+
case 'select_option':
|
|
287
|
+
return '中';
|
|
288
|
+
case 'handoff':
|
|
289
|
+
return '人工接管';
|
|
290
|
+
default:
|
|
291
|
+
return '低';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getCommandTarget(command) {
|
|
296
|
+
switch (command?.name) {
|
|
297
|
+
case 'open_url':
|
|
298
|
+
return command.args?.url || '(缺少 url)';
|
|
299
|
+
case 'send_message':
|
|
300
|
+
return command.args?.content || '(缺少 content)';
|
|
301
|
+
case 'run_shell':
|
|
302
|
+
return command.args?.command || '(缺少 command)';
|
|
303
|
+
case 'read_file':
|
|
304
|
+
case 'file_exists':
|
|
305
|
+
case 'list_files':
|
|
306
|
+
case 'write_file':
|
|
307
|
+
case 'delete_file':
|
|
308
|
+
case 'screenshot_agent_page':
|
|
309
|
+
return command.args?.path || '(缺少 path)';
|
|
310
|
+
case 'click_selector':
|
|
311
|
+
case 'type_selector':
|
|
312
|
+
case 'wait_for_selector':
|
|
313
|
+
case 'hover_selector':
|
|
314
|
+
case 'check_checkbox':
|
|
315
|
+
case 'uncheck_checkbox':
|
|
316
|
+
case 'select_option':
|
|
317
|
+
case 'extract_selector_text':
|
|
318
|
+
return command.args?.selector || '(缺少 selector)';
|
|
319
|
+
case 'wait_for_text':
|
|
320
|
+
return command.args?.text || '(缺少 text)';
|
|
321
|
+
case 'press_key':
|
|
322
|
+
return command.args?.key || '(缺少 key)';
|
|
323
|
+
case 'inspect_dom':
|
|
324
|
+
return command.args?.selector || 'body';
|
|
325
|
+
case 'scroll_page':
|
|
326
|
+
return command.args?.selector || `y=${command.args?.y ?? 0}`;
|
|
327
|
+
case 'get_app_state':
|
|
328
|
+
return 'provider / session / browser / agent';
|
|
329
|
+
case 'focus_chat_input':
|
|
330
|
+
case 'submit_current_input':
|
|
331
|
+
case 'start_new_chat':
|
|
332
|
+
return '当前 provider 聊天页面';
|
|
333
|
+
case 'send_chat_input':
|
|
334
|
+
return command.args?.text || command.args?.content || '(空内容)';
|
|
335
|
+
case 'read_agent_page':
|
|
336
|
+
case 'send_agent_page':
|
|
337
|
+
return 'agent 浏览器通道';
|
|
338
|
+
case 'handoff':
|
|
339
|
+
return '需要用户继续操作';
|
|
340
|
+
default:
|
|
341
|
+
return JSON.stringify(command?.args || {});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getCommandNextHint(command) {
|
|
346
|
+
switch (command?.name) {
|
|
347
|
+
case 'open_url':
|
|
348
|
+
return '可能继续等待页面加载、提取文本或点击元素';
|
|
349
|
+
case 'run_shell':
|
|
350
|
+
return '可能继续读取命令输出,或再修改文件/执行下一步';
|
|
351
|
+
case 'file_exists':
|
|
352
|
+
case 'list_files':
|
|
353
|
+
case 'read_file':
|
|
354
|
+
return '通常会基于本地结果继续追问或执行下一步';
|
|
355
|
+
case 'read_agent_page':
|
|
356
|
+
case 'extract_selector_text':
|
|
357
|
+
case 'inspect_dom':
|
|
358
|
+
case 'get_app_state':
|
|
359
|
+
case 'focus_chat_input':
|
|
360
|
+
case 'send_chat_input':
|
|
361
|
+
case 'start_new_chat':
|
|
362
|
+
return '通常会基于观察结果继续决策下一步';
|
|
363
|
+
case 'submit_current_input':
|
|
364
|
+
return '会等待模型下一轮回复';
|
|
365
|
+
case 'send_agent_page':
|
|
366
|
+
case 'send_message':
|
|
367
|
+
return '会等待模型下一轮回复';
|
|
368
|
+
case 'write_file':
|
|
369
|
+
case 'delete_file':
|
|
370
|
+
return '可能继续验证文件结果或执行相关命令';
|
|
371
|
+
case 'handoff':
|
|
372
|
+
return '等待你在浏览器中完成登录、验证或人工操作';
|
|
373
|
+
default:
|
|
374
|
+
return '可能继续执行更多自动化步骤';
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildConfirmationRows(command, { autoExecuteCommands = false } = {}) {
|
|
379
|
+
return [
|
|
380
|
+
{ label: '当前动作', value: describeCommand(command) },
|
|
381
|
+
{ label: '风险级别', value: getCommandRiskLevel(command) },
|
|
382
|
+
{ label: '目标对象', value: getCommandTarget(command) },
|
|
383
|
+
{ label: '自动执行', value: autoExecuteCommands ? '开启' : '关闭' },
|
|
384
|
+
{ label: '后续可能', value: getCommandNextHint(command) },
|
|
385
|
+
...summarizeCommand(command)
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function normalizeBrowserCommandArgs(command = {}) {
|
|
390
|
+
const args = {
|
|
391
|
+
...(command.args || {})
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (command.name === 'type_selector') {
|
|
395
|
+
if (args.pressEnter === undefined && args.submit === undefined) {
|
|
396
|
+
const text = String(args.text ?? args.content ?? '');
|
|
397
|
+
if (text.includes('\n') === false) {
|
|
398
|
+
args.pressEnter = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!args.retryCount) {
|
|
403
|
+
args.retryCount = 2;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (command.name === 'click_selector') {
|
|
408
|
+
if (!args.retryCount) {
|
|
409
|
+
args.retryCount = 3;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return args;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export class AgentExecutor {
|
|
417
|
+
constructor({
|
|
418
|
+
browserManager,
|
|
419
|
+
ui,
|
|
420
|
+
prompt,
|
|
421
|
+
sendMessage,
|
|
422
|
+
getAppState,
|
|
423
|
+
workspaceRoot,
|
|
424
|
+
allowShell = true,
|
|
425
|
+
allowFileWrite = true,
|
|
426
|
+
autoExecuteCommands = false,
|
|
427
|
+
fsModule = fs,
|
|
428
|
+
execImpl = exec
|
|
429
|
+
}) {
|
|
430
|
+
this.browserManager = browserManager;
|
|
431
|
+
this.ui = ui;
|
|
432
|
+
this.prompt = prompt;
|
|
433
|
+
this.sendMessage = sendMessage;
|
|
434
|
+
this.getAppState = getAppState;
|
|
435
|
+
this.workspaceRoot = workspaceRoot;
|
|
436
|
+
this.allowShell = allowShell;
|
|
437
|
+
this.allowFileWrite = allowFileWrite;
|
|
438
|
+
this.autoExecuteCommands = autoExecuteCommands;
|
|
439
|
+
this.fs = fsModule;
|
|
440
|
+
this.execImpl = execImpl;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
getBrowserCommandChannel(command = {}) {
|
|
444
|
+
const requested = String(command.args?.channel || '').trim().toLowerCase();
|
|
445
|
+
if (requested === 'chat' || requested === 'agent') {
|
|
446
|
+
return requested;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (typeof this.browserManager?.isChannelReady === 'function') {
|
|
450
|
+
if (this.browserManager.isChannelReady('agent')) {
|
|
451
|
+
return 'agent';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (this.browserManager.isChannelReady('chat')) {
|
|
455
|
+
return 'chat';
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return 'agent';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async ensureBrowserCommandChannel(command = {}) {
|
|
463
|
+
const channel = this.getBrowserCommandChannel(command);
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
channel === 'chat' &&
|
|
467
|
+
!this.browserManager?.isChannelReady?.('chat') &&
|
|
468
|
+
typeof this.browserManager?.initChannel === 'function'
|
|
469
|
+
) {
|
|
470
|
+
await this.browserManager.initChannel('chat');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return channel;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
buildRecoverableErrorResult(command, error) {
|
|
477
|
+
const message = String(error?.message || error || '').trim();
|
|
478
|
+
const recoverable = [
|
|
479
|
+
'Element is not visible',
|
|
480
|
+
'element is outside of the viewport',
|
|
481
|
+
'another element',
|
|
482
|
+
'intercepts pointer events',
|
|
483
|
+
'not attached to the DOM',
|
|
484
|
+
'detached from document',
|
|
485
|
+
'timeout'
|
|
486
|
+
].some((pattern) => message.toLowerCase().includes(pattern.toLowerCase()));
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
error: message || '命令执行失败',
|
|
491
|
+
recoverable,
|
|
492
|
+
commandName: command?.name || '',
|
|
493
|
+
output: JSON.stringify(
|
|
494
|
+
{
|
|
495
|
+
command: command?.name || '',
|
|
496
|
+
args: command?.args || {},
|
|
497
|
+
recoverable,
|
|
498
|
+
error: message || '命令执行失败'
|
|
499
|
+
},
|
|
500
|
+
null,
|
|
501
|
+
2
|
|
502
|
+
)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
requiresManualApproval(command) {
|
|
507
|
+
return ![
|
|
508
|
+
'get_app_state',
|
|
509
|
+
'file_exists',
|
|
510
|
+
'list_files',
|
|
511
|
+
'read_file',
|
|
512
|
+
'focus_chat_input',
|
|
513
|
+
'send_chat_input',
|
|
514
|
+
'submit_current_input'
|
|
515
|
+
].includes(
|
|
516
|
+
command?.name
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async confirm(command) {
|
|
521
|
+
if (!this.requiresManualApproval(command)) {
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (this.ui?.printKeyValueTable) {
|
|
526
|
+
this.ui.printKeyValueTable(
|
|
527
|
+
'待确认操作',
|
|
528
|
+
buildConfirmationRows(command, {
|
|
529
|
+
autoExecuteCommands: this.autoExecuteCommands
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
} else if (this.ui?.printInfo) {
|
|
533
|
+
this.ui.printInfo(describeCommand(command));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const question = `${describeCommand(command)},是否执行?(y/n) `;
|
|
537
|
+
const answer = await this.prompt(question);
|
|
538
|
+
return isAffirmative(answer);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
shouldContinue(result) {
|
|
542
|
+
if (!result?.ok) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (result.handoff) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (result.commandName === 'send_message') {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (result.commandName === 'handoff') {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (typeof result.message === 'string' && /已发送消息/.test(result.message)) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
evaluateCompletion(result) {
|
|
566
|
+
if (!result?.ok) {
|
|
567
|
+
return {
|
|
568
|
+
status: 'error',
|
|
569
|
+
reason: result?.error || '命令执行失败'
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (result.handoff) {
|
|
574
|
+
return {
|
|
575
|
+
status: 'handoff',
|
|
576
|
+
reason: result.message || '需要人工介入'
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (result.commandName === 'send_message') {
|
|
581
|
+
return {
|
|
582
|
+
status: 'completed',
|
|
583
|
+
reason: '消息已经发送给目标模型'
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (
|
|
588
|
+
[
|
|
589
|
+
'get_app_state',
|
|
590
|
+
'read_agent_page',
|
|
591
|
+
'inspect_dom',
|
|
592
|
+
'extract_selector_text',
|
|
593
|
+
'focus_chat_input',
|
|
594
|
+
'send_chat_input',
|
|
595
|
+
'start_new_chat'
|
|
596
|
+
].includes(
|
|
597
|
+
result.commandName
|
|
598
|
+
)
|
|
599
|
+
) {
|
|
600
|
+
return {
|
|
601
|
+
status: 'continue',
|
|
602
|
+
reason: '已获得新的观察结果,适合继续决策'
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
status: this.shouldContinue(result) ? 'continue' : 'completed',
|
|
608
|
+
reason: this.shouldContinue(result)
|
|
609
|
+
? '动作已执行,建议结合结果继续判断'
|
|
610
|
+
: '当前动作已完成'
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async execute(command) {
|
|
615
|
+
if (!command?.name) {
|
|
616
|
+
return {
|
|
617
|
+
ok: false,
|
|
618
|
+
error: '缺少命令名称'
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
switch (command.name) {
|
|
624
|
+
case 'open_url': {
|
|
625
|
+
const url = command.args?.url;
|
|
626
|
+
if (!url) {
|
|
627
|
+
return { ok: false, error: '缺少 url 参数' };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (typeof this.browserManager.openAgentPage === 'function') {
|
|
631
|
+
await this.browserManager.openAgentPage(url);
|
|
632
|
+
} else {
|
|
633
|
+
await this.browserManager.goto(url);
|
|
634
|
+
}
|
|
635
|
+
return { ok: true, message: `已打开 ${url}`, commandName: command.name };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
case 'send_message': {
|
|
639
|
+
const content = command.args?.content;
|
|
640
|
+
if (!content) {
|
|
641
|
+
return { ok: false, error: '缺少 content 参数' };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (typeof this.sendMessage !== 'function') {
|
|
645
|
+
return { ok: false, error: '发送能力未注入' };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
await this.sendMessage(content);
|
|
649
|
+
return { ok: true, message: '已发送消息', commandName: command.name };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
case 'handoff': {
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
handoff: true,
|
|
656
|
+
message: '请使用 /agent 进入人工接管',
|
|
657
|
+
commandName: command.name
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
case 'run_shell': {
|
|
662
|
+
if (!this.allowShell) {
|
|
663
|
+
return { ok: false, error: '当前配置禁止执行系统命令' };
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const shellCommand = command.args?.command;
|
|
667
|
+
if (!shellCommand) {
|
|
668
|
+
return { ok: false, error: '缺少 command 参数' };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const result = await this.execImpl(shellCommand, {
|
|
672
|
+
cwd: this.workspaceRoot
|
|
673
|
+
});
|
|
674
|
+
return {
|
|
675
|
+
ok: true,
|
|
676
|
+
message: '系统命令执行完成',
|
|
677
|
+
commandName: command.name,
|
|
678
|
+
output: [result.stdout, result.stderr].filter(Boolean).join('\n').trim()
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
case 'read_file': {
|
|
683
|
+
const filePath = command.args?.path;
|
|
684
|
+
if (!filePath) {
|
|
685
|
+
return { ok: false, error: '缺少 path 参数' };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, filePath);
|
|
689
|
+
const content = await this.fs.readFile(absolute, 'utf-8');
|
|
690
|
+
return {
|
|
691
|
+
ok: true,
|
|
692
|
+
message: `已读取 ${filePath}`,
|
|
693
|
+
commandName: command.name,
|
|
694
|
+
output: content
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case 'file_exists': {
|
|
699
|
+
const filePath = command.args?.path;
|
|
700
|
+
if (!filePath) {
|
|
701
|
+
return { ok: false, error: '缺少 path 参数' };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, filePath);
|
|
705
|
+
let exists = true;
|
|
706
|
+
try {
|
|
707
|
+
await this.fs.access(absolute);
|
|
708
|
+
} catch {
|
|
709
|
+
exists = false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
ok: true,
|
|
714
|
+
message: exists ? `已确认 ${filePath} 存在` : `已确认 ${filePath} 不存在`,
|
|
715
|
+
commandName: command.name,
|
|
716
|
+
output: exists ? `存在: ${filePath}` : `不存在: ${filePath}`,
|
|
717
|
+
exists
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
case 'list_files': {
|
|
722
|
+
const dirPath = command.args?.path || '.';
|
|
723
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, dirPath);
|
|
724
|
+
const entries = await this.fs.readdir(absolute, { withFileTypes: true });
|
|
725
|
+
const output = entries
|
|
726
|
+
.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
|
|
727
|
+
.map((entry) => `${entry.isDirectory() ? '[DIR] ' : ''}${entry.name}`)
|
|
728
|
+
.join('\n');
|
|
729
|
+
return {
|
|
730
|
+
ok: true,
|
|
731
|
+
message: `已列出 ${dirPath} 下的文件`,
|
|
732
|
+
commandName: command.name,
|
|
733
|
+
output
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
case 'write_file': {
|
|
738
|
+
if (!this.allowFileWrite) {
|
|
739
|
+
return { ok: false, error: '当前配置禁止写入文件' };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const filePath = command.args?.path;
|
|
743
|
+
const content = String(command.args?.content ?? '');
|
|
744
|
+
|
|
745
|
+
if (!filePath) {
|
|
746
|
+
return { ok: false, error: '缺少 path 参数' };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, filePath);
|
|
750
|
+
await this.fs.mkdir(resolve(absolute, '..'), { recursive: true });
|
|
751
|
+
await this.fs.writeFile(absolute, content, 'utf-8');
|
|
752
|
+
return {
|
|
753
|
+
ok: true,
|
|
754
|
+
message: `已写入 ${filePath}`,
|
|
755
|
+
commandName: command.name
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
case 'delete_file': {
|
|
760
|
+
if (!this.allowFileWrite) {
|
|
761
|
+
return { ok: false, error: '当前配置禁止删除文件' };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const filePath = command.args?.path;
|
|
765
|
+
if (!filePath) {
|
|
766
|
+
return { ok: false, error: '缺少 path 参数' };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, filePath);
|
|
770
|
+
await this.fs.rm(absolute, { force: true, recursive: true });
|
|
771
|
+
return {
|
|
772
|
+
ok: true,
|
|
773
|
+
message: `已删除 ${filePath}`,
|
|
774
|
+
commandName: command.name
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
case 'read_agent_page': {
|
|
779
|
+
if (typeof this.browserManager.readChannelSnapshot !== 'function') {
|
|
780
|
+
return { ok: false, error: '当前浏览器管理器不支持页面读取' };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
784
|
+
const snapshot = await this.browserManager.readChannelSnapshot(channel, {
|
|
785
|
+
includeDomSummary: true,
|
|
786
|
+
includeObservation: true,
|
|
787
|
+
maxChars: 2500
|
|
788
|
+
});
|
|
789
|
+
return {
|
|
790
|
+
ok: true,
|
|
791
|
+
message: `已读取${channel === 'agent' ? 'Agent' : '当前聊天'}页面`,
|
|
792
|
+
commandName: command.name,
|
|
793
|
+
output: [
|
|
794
|
+
`Title: ${snapshot.title}`,
|
|
795
|
+
`URL: ${snapshot.url}`,
|
|
796
|
+
'',
|
|
797
|
+
snapshot.text,
|
|
798
|
+
snapshot.observation
|
|
799
|
+
? `\n最近观测:\n${JSON.stringify(snapshot.observation, null, 2)}`
|
|
800
|
+
: '',
|
|
801
|
+
Array.isArray(snapshot.dom) && snapshot.dom.length > 0
|
|
802
|
+
? `\nDOM 摘要:\n${JSON.stringify(snapshot.dom, null, 2)}`
|
|
803
|
+
: ''
|
|
804
|
+
]
|
|
805
|
+
.filter(Boolean)
|
|
806
|
+
.join('\n')
|
|
807
|
+
.trim(),
|
|
808
|
+
silentOutput: true
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
case 'send_agent_page': {
|
|
813
|
+
if (typeof this.browserManager.readChannelSnapshot !== 'function') {
|
|
814
|
+
return { ok: false, error: '当前浏览器管理器不支持页面读取' };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (typeof this.sendMessage !== 'function') {
|
|
818
|
+
return { ok: false, error: '发送能力未注入' };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
822
|
+
const snapshot = await this.browserManager.readChannelSnapshot(channel, {
|
|
823
|
+
includeDomSummary: true,
|
|
824
|
+
includeObservation: true,
|
|
825
|
+
maxChars: 2500
|
|
826
|
+
});
|
|
827
|
+
const content = [
|
|
828
|
+
'以下是你刚刚打开的网页内容与观测结果,请基于这些内容继续判断并给出下一步:',
|
|
829
|
+
`Title: ${snapshot.title}`,
|
|
830
|
+
`URL: ${snapshot.url}`,
|
|
831
|
+
'',
|
|
832
|
+
snapshot.text,
|
|
833
|
+
snapshot.observation
|
|
834
|
+
? `\n最近观测(JSON):\n${JSON.stringify(snapshot.observation, null, 2)}`
|
|
835
|
+
: '',
|
|
836
|
+
Array.isArray(snapshot.dom) && snapshot.dom.length > 0
|
|
837
|
+
? `\nDOM 摘要(JSON):\n${JSON.stringify(snapshot.dom, null, 2)}`
|
|
838
|
+
: ''
|
|
839
|
+
].join('\n');
|
|
840
|
+
|
|
841
|
+
await this.sendMessage(content);
|
|
842
|
+
return {
|
|
843
|
+
ok: true,
|
|
844
|
+
message:
|
|
845
|
+
channel === 'agent'
|
|
846
|
+
? '已将 Agent 页面内容发送给当前对话'
|
|
847
|
+
: '已将当前聊天页面内容发送给当前对话',
|
|
848
|
+
commandName: command.name
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
case 'get_app_state': {
|
|
853
|
+
if (typeof this.getAppState !== 'function') {
|
|
854
|
+
return { ok: false, error: '应用状态读取能力未注入' };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const state = this.getAppState();
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
message: '已读取应用状态',
|
|
861
|
+
commandName: command.name,
|
|
862
|
+
output: JSON.stringify(state, null, 2),
|
|
863
|
+
silentOutput: true
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
case 'focus_chat_input': {
|
|
868
|
+
const chatManager = this.browserManager?.getChatManager?.();
|
|
869
|
+
if (!chatManager?.focusInput) {
|
|
870
|
+
return { ok: false, error: '当前聊天管理器不支持聚焦输入框' };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const selector = await chatManager.focusInput();
|
|
874
|
+
return {
|
|
875
|
+
ok: true,
|
|
876
|
+
message: '已聚焦当前聊天输入框',
|
|
877
|
+
commandName: command.name,
|
|
878
|
+
output: selector,
|
|
879
|
+
silentOutput: true
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
case 'send_chat_input': {
|
|
884
|
+
const chatManager = this.browserManager?.getChatManager?.();
|
|
885
|
+
if (!chatManager?.typeInInput) {
|
|
886
|
+
return { ok: false, error: '当前聊天管理器不支持填写输入框' };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const selector = await chatManager.typeInInput(
|
|
890
|
+
command.args?.text ?? command.args?.content ?? '',
|
|
891
|
+
command.args || {}
|
|
892
|
+
);
|
|
893
|
+
return {
|
|
894
|
+
ok: true,
|
|
895
|
+
message: '已填写当前聊天输入框',
|
|
896
|
+
commandName: command.name,
|
|
897
|
+
output: selector,
|
|
898
|
+
silentOutput: true
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
case 'submit_current_input': {
|
|
903
|
+
const chatManager = this.browserManager?.getChatManager?.();
|
|
904
|
+
if (!chatManager?.submitInput) {
|
|
905
|
+
return { ok: false, error: '当前聊天管理器不支持提交输入框' };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const target = await chatManager.submitInput();
|
|
909
|
+
return {
|
|
910
|
+
ok: true,
|
|
911
|
+
message: '已提交当前聊天输入框内容',
|
|
912
|
+
commandName: command.name,
|
|
913
|
+
output: String(target || ''),
|
|
914
|
+
silentOutput: true
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
case 'start_new_chat': {
|
|
919
|
+
const chatManager = this.browserManager?.getChatManager?.();
|
|
920
|
+
if (!chatManager?.startNewChat) {
|
|
921
|
+
return { ok: false, error: '当前聊天管理器不支持新建对话' };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const selector = await chatManager.startNewChat();
|
|
925
|
+
return {
|
|
926
|
+
ok: true,
|
|
927
|
+
message: '已尝试在当前提供方页面中新建对话',
|
|
928
|
+
commandName: command.name,
|
|
929
|
+
output: String(selector || ''),
|
|
930
|
+
silentOutput: true
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
case 'click_selector': {
|
|
935
|
+
const selector = command.args?.selector;
|
|
936
|
+
if (!selector) {
|
|
937
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const actionArgs = normalizeBrowserCommandArgs(command);
|
|
941
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
942
|
+
await this.browserManager.clickSelector(channel, selector, actionArgs);
|
|
943
|
+
return {
|
|
944
|
+
ok: true,
|
|
945
|
+
message: `已点击 ${selector}`,
|
|
946
|
+
commandName: command.name
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
case 'type_selector': {
|
|
951
|
+
const selector = command.args?.selector;
|
|
952
|
+
if (!selector) {
|
|
953
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const actionArgs = normalizeBrowserCommandArgs(command);
|
|
957
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
958
|
+
await this.browserManager.typeSelector(
|
|
959
|
+
channel,
|
|
960
|
+
selector,
|
|
961
|
+
command.args?.text ?? command.args?.content ?? '',
|
|
962
|
+
actionArgs
|
|
963
|
+
);
|
|
964
|
+
return {
|
|
965
|
+
ok: true,
|
|
966
|
+
message: `已向 ${selector} 输入内容`,
|
|
967
|
+
commandName: command.name
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
case 'wait_for_text': {
|
|
972
|
+
const text = command.args?.text;
|
|
973
|
+
if (!text) {
|
|
974
|
+
return { ok: false, error: '缺少 text 参数' };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
978
|
+
await this.browserManager.waitForText(channel, text, command.args || {});
|
|
979
|
+
return {
|
|
980
|
+
ok: true,
|
|
981
|
+
message: `已等到文本出现: ${text}`,
|
|
982
|
+
commandName: command.name
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
case 'extract_selector_text': {
|
|
987
|
+
const selector = command.args?.selector;
|
|
988
|
+
if (!selector) {
|
|
989
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
993
|
+
const output = await this.browserManager.extractSelectorText(
|
|
994
|
+
channel,
|
|
995
|
+
selector,
|
|
996
|
+
command.args || {}
|
|
997
|
+
);
|
|
998
|
+
return {
|
|
999
|
+
ok: true,
|
|
1000
|
+
message: `已提取 ${selector} 的文本`,
|
|
1001
|
+
commandName: command.name,
|
|
1002
|
+
output: Array.isArray(output) ? output.join('\n\n') : String(output),
|
|
1003
|
+
silentOutput: true
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
case 'wait_for_selector': {
|
|
1008
|
+
const selector = command.args?.selector;
|
|
1009
|
+
if (!selector) {
|
|
1010
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1014
|
+
await this.browserManager.waitForSelector(channel, selector, command.args || {});
|
|
1015
|
+
return {
|
|
1016
|
+
ok: true,
|
|
1017
|
+
message: `已等到选择器出现: ${selector}`,
|
|
1018
|
+
commandName: command.name
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
case 'wait_for_navigation': {
|
|
1023
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1024
|
+
await this.browserManager.waitForNavigation(channel, command.args || {});
|
|
1025
|
+
return {
|
|
1026
|
+
ok: true,
|
|
1027
|
+
message: '已等待页面导航完成',
|
|
1028
|
+
commandName: command.name
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
case 'hover_selector': {
|
|
1033
|
+
const selector = command.args?.selector;
|
|
1034
|
+
if (!selector) {
|
|
1035
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1039
|
+
await this.browserManager.hoverSelector(channel, selector, command.args || {});
|
|
1040
|
+
return {
|
|
1041
|
+
ok: true,
|
|
1042
|
+
message: `已悬停 ${selector}`,
|
|
1043
|
+
commandName: command.name
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
case 'press_key': {
|
|
1048
|
+
const key = command.args?.key;
|
|
1049
|
+
if (!key) {
|
|
1050
|
+
return { ok: false, error: '缺少 key 参数' };
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1054
|
+
await this.browserManager.pressKey(channel, key, command.args || {});
|
|
1055
|
+
return {
|
|
1056
|
+
ok: true,
|
|
1057
|
+
message: `已按下按键: ${key}`,
|
|
1058
|
+
commandName: command.name
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
case 'check_checkbox': {
|
|
1063
|
+
const selector = command.args?.selector;
|
|
1064
|
+
if (!selector) {
|
|
1065
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1069
|
+
await this.browserManager.setCheckbox(channel, selector, true, command.args || {});
|
|
1070
|
+
return {
|
|
1071
|
+
ok: true,
|
|
1072
|
+
message: `已勾选 ${selector}`,
|
|
1073
|
+
commandName: command.name
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
case 'uncheck_checkbox': {
|
|
1078
|
+
const selector = command.args?.selector;
|
|
1079
|
+
if (!selector) {
|
|
1080
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1084
|
+
await this.browserManager.setCheckbox(channel, selector, false, command.args || {});
|
|
1085
|
+
return {
|
|
1086
|
+
ok: true,
|
|
1087
|
+
message: `已取消勾选 ${selector}`,
|
|
1088
|
+
commandName: command.name
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
case 'select_option': {
|
|
1093
|
+
const selector = command.args?.selector;
|
|
1094
|
+
const value =
|
|
1095
|
+
command.args?.value ??
|
|
1096
|
+
command.args?.label ??
|
|
1097
|
+
command.args?.index;
|
|
1098
|
+
|
|
1099
|
+
if (!selector) {
|
|
1100
|
+
return { ok: false, error: '缺少 selector 参数' };
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (value === undefined || value === null || value === '') {
|
|
1104
|
+
return { ok: false, error: '缺少 value/label/index 参数' };
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
let optionValue = value;
|
|
1108
|
+
if (command.args?.label) {
|
|
1109
|
+
optionValue = { label: String(command.args.label) };
|
|
1110
|
+
} else if (command.args?.index !== undefined) {
|
|
1111
|
+
optionValue = { index: Number(command.args.index) };
|
|
1112
|
+
} else {
|
|
1113
|
+
optionValue = { value: String(command.args.value ?? value) };
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1117
|
+
await this.browserManager.selectOption(channel, selector, optionValue, command.args || {});
|
|
1118
|
+
return {
|
|
1119
|
+
ok: true,
|
|
1120
|
+
message: `已选择 ${selector} 的下拉项`,
|
|
1121
|
+
commandName: command.name
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
case 'scroll_page': {
|
|
1126
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1127
|
+
await this.browserManager.scrollPage(channel, command.args || {});
|
|
1128
|
+
return {
|
|
1129
|
+
ok: true,
|
|
1130
|
+
message: '已滚动 Agent 页面',
|
|
1131
|
+
commandName: command.name
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
case 'screenshot_agent_page': {
|
|
1136
|
+
if (!this.allowFileWrite) {
|
|
1137
|
+
return { ok: false, error: '当前配置禁止写入截图文件' };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const filePath = command.args?.path;
|
|
1141
|
+
if (!filePath) {
|
|
1142
|
+
return { ok: false, error: '缺少 path 参数' };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const absolute = resolveWorkspacePath(this.workspaceRoot, filePath);
|
|
1146
|
+
await this.fs.mkdir(resolve(absolute, '..'), { recursive: true });
|
|
1147
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1148
|
+
await this.browserManager.screenshotChannel(channel, {
|
|
1149
|
+
...command.args,
|
|
1150
|
+
path: absolute
|
|
1151
|
+
});
|
|
1152
|
+
return {
|
|
1153
|
+
ok: true,
|
|
1154
|
+
message: `已保存截图到 ${filePath}`,
|
|
1155
|
+
commandName: command.name
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
case 'inspect_dom': {
|
|
1160
|
+
const channel = await this.ensureBrowserCommandChannel(command);
|
|
1161
|
+
const output = await this.browserManager.inspectDom(channel, command.args || {});
|
|
1162
|
+
return {
|
|
1163
|
+
ok: true,
|
|
1164
|
+
message: '已读取页面 DOM 结构',
|
|
1165
|
+
commandName: command.name,
|
|
1166
|
+
output: JSON.stringify(output, null, 2),
|
|
1167
|
+
silentOutput: true
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
default:
|
|
1172
|
+
return {
|
|
1173
|
+
ok: false,
|
|
1174
|
+
error: `暂不支持命令: ${command.name}`
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
return this.buildRecoverableErrorResult(command, error);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export { describeCommand, resolveWorkspacePath, summarizeCommand };
|
|
1184
|
+
export default AgentExecutor;
|