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,280 @@
|
|
|
1
|
+
function parseLocalCommand(input = '') {
|
|
2
|
+
const text = String(input ?? '').trim();
|
|
3
|
+
if (!text) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const match = text.match(/^\/local\s+(\w+)(?:\s+([\s\S]+))?$/i);
|
|
8
|
+
if (!match) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const action = match[1].toLowerCase();
|
|
13
|
+
const rest = String(match[2] || '').trim();
|
|
14
|
+
|
|
15
|
+
switch (action) {
|
|
16
|
+
case 'ls':
|
|
17
|
+
case 'dir':
|
|
18
|
+
case 'list':
|
|
19
|
+
return {
|
|
20
|
+
matched: true,
|
|
21
|
+
reason: 'list-workspace',
|
|
22
|
+
command: {
|
|
23
|
+
name: 'list_files',
|
|
24
|
+
args: { path: rest || '.' }
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
case 'exists': {
|
|
28
|
+
if (!rest) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
matched: true,
|
|
33
|
+
reason: 'file-exists',
|
|
34
|
+
command: {
|
|
35
|
+
name: 'file_exists',
|
|
36
|
+
args: { path: rest }
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
case 'read': {
|
|
41
|
+
if (!rest) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
matched: true,
|
|
46
|
+
reason: 'read-file',
|
|
47
|
+
command: {
|
|
48
|
+
name: 'read_file',
|
|
49
|
+
args: { path: rest }
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'write': {
|
|
54
|
+
const writeMatch = rest.match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
55
|
+
const path = writeMatch?.[1] || '';
|
|
56
|
+
const content = writeMatch?.[2] || '';
|
|
57
|
+
if (!path) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
matched: true,
|
|
62
|
+
reason: 'write-file',
|
|
63
|
+
command: {
|
|
64
|
+
name: 'write_file',
|
|
65
|
+
args: {
|
|
66
|
+
path,
|
|
67
|
+
content
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case 'rm':
|
|
73
|
+
case 'delete': {
|
|
74
|
+
if (!rest) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
matched: true,
|
|
79
|
+
reason: 'delete-file',
|
|
80
|
+
command: {
|
|
81
|
+
name: 'delete_file',
|
|
82
|
+
args: { path: rest }
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
case 'shell': {
|
|
87
|
+
if (!rest) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
matched: true,
|
|
92
|
+
reason: 'explicit-shell',
|
|
93
|
+
command: {
|
|
94
|
+
name: 'run_shell',
|
|
95
|
+
args: { command: rest }
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeText(text = '') {
|
|
105
|
+
return String(text ?? '')
|
|
106
|
+
.trim()
|
|
107
|
+
.toLowerCase()
|
|
108
|
+
.replace(/\s+/g, ' ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function detectControlIntent(input = '') {
|
|
112
|
+
const raw = String(input ?? '').trim();
|
|
113
|
+
const text = normalizeText(raw);
|
|
114
|
+
if (!text) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const providerPatterns = [
|
|
119
|
+
{ id: 'gemini', match: /(切换|换|改).*(gemini|谷歌)|^(gemini|切到gemini|切换到gemini)$/i },
|
|
120
|
+
{ id: 'duckai', match: /(切换|换|改).*(duck\.?ai|duckai)|^(duck\.?ai|duckai|切到duckai|切换到duckai)$/i },
|
|
121
|
+
{ id: 'deepseek', match: /(切换|换|改).*(deepseek)|^(deepseek|切到deepseek|切换到deepseek)$/i },
|
|
122
|
+
{ id: 'chatgpt', match: /(切换|换|改).*(chatgpt)|^(chatgpt|切到chatgpt|切换到chatgpt)$/i },
|
|
123
|
+
{ id: 'copilot', match: /(切换|换|改).*(copilot)|^(copilot|切到copilot|切换到copilot)$/i },
|
|
124
|
+
{ id: 'qwen', match: /(切换|换|改).*(qwen|通义)|^(qwen|通义|切到qwen|切换到qwen)$/i },
|
|
125
|
+
{ id: 'you', match: /(切换|换|改).*(you\.com|you)|^(you\.com|you|切到you|切换到you)$/i },
|
|
126
|
+
{ id: 'huggingchat', match: /(切换|换|改).*(huggingchat)|^(huggingchat|切到huggingchat|切换到huggingchat)$/i },
|
|
127
|
+
{ id: 'deepai', match: /(切换|换|改).*(deepai)|^(deepai|切到deepai|切换到deepai)$/i }
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const provider of providerPatterns) {
|
|
131
|
+
if (provider.match.test(raw)) {
|
|
132
|
+
return {
|
|
133
|
+
matched: true,
|
|
134
|
+
reason: 'switch-provider-natural',
|
|
135
|
+
control: {
|
|
136
|
+
action: 'switch_provider',
|
|
137
|
+
providerId: provider.id
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (/(当前.*(provider|模型|服务商|提供方))|((provider|模型|服务商|提供方).*(是啥|是什么|是谁|状态))/.test(raw)) {
|
|
144
|
+
return {
|
|
145
|
+
matched: true,
|
|
146
|
+
reason: 'show-provider-status',
|
|
147
|
+
control: { action: 'show_provider_status' }
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/(当前.*(状态|运行状态|应用状态|app state))|((状态|运行状态|应用状态).*(看看|查看|读取|显示))/.test(raw)) {
|
|
152
|
+
return {
|
|
153
|
+
matched: true,
|
|
154
|
+
reason: 'show-app-state',
|
|
155
|
+
control: { action: 'show_app_state' }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (/(配置|设置).*(看看|查看|读取|显示|摘要)|((看看|查看|读取|显示).*(配置|设置))/.test(raw)) {
|
|
160
|
+
return {
|
|
161
|
+
matched: true,
|
|
162
|
+
reason: 'show-config-summary',
|
|
163
|
+
control: { action: 'show_config_summary' }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (/(打开|进入|看看|查看).*(任务|task)/.test(raw)) {
|
|
168
|
+
return {
|
|
169
|
+
matched: true,
|
|
170
|
+
reason: 'open-task-panel',
|
|
171
|
+
control: { action: 'open_task_panel' }
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (/(打开|进入|看看|查看).*(记忆|memory)/.test(raw)) {
|
|
176
|
+
return {
|
|
177
|
+
matched: true,
|
|
178
|
+
reason: 'open-memory-panel',
|
|
179
|
+
control: { action: 'open_memory_panel' }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (/(打开|进入|看看|查看).*(会话|历史|sessions)/.test(raw)) {
|
|
184
|
+
return {
|
|
185
|
+
matched: true,
|
|
186
|
+
reason: 'open-sessions-panel',
|
|
187
|
+
control: { action: 'open_sessions_panel' }
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (/(打开|进入|看看|查看).*(设置|配置|config)/.test(raw)) {
|
|
192
|
+
return {
|
|
193
|
+
matched: true,
|
|
194
|
+
reason: 'open-settings',
|
|
195
|
+
control: { action: 'open_settings' }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (/(打开|进入|看看|查看).*(命令面板|命令|cmd)/.test(raw)) {
|
|
200
|
+
return {
|
|
201
|
+
matched: true,
|
|
202
|
+
reason: 'open-command-palette',
|
|
203
|
+
control: { action: 'open_command_palette' }
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function detectLocalIntent(message = '') {
|
|
211
|
+
return parseLocalCommand(message) || detectControlIntent(message) || { matched: false };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function formatLocalIntentResult(intent, result) {
|
|
215
|
+
if (!result?.ok) {
|
|
216
|
+
return `本地执行失败:${result?.error || '未知错误'}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (intent?.reason === 'show-app-state') {
|
|
220
|
+
try {
|
|
221
|
+
const state = JSON.parse(result.output || '{}');
|
|
222
|
+
return [
|
|
223
|
+
'当前应用状态:',
|
|
224
|
+
'',
|
|
225
|
+
`- Provider: ${state.provider || '(无)'}`,
|
|
226
|
+
`- Session: ${state.sessionTitle || '(无会话)'}`,
|
|
227
|
+
`- Messages: ${String(state.messageCount ?? 0)}`,
|
|
228
|
+
`- Chat URL: ${state.chatUrl || '(无)'}`,
|
|
229
|
+
`- Agent URL: ${state.agentUrl || '(无)'}`,
|
|
230
|
+
`- Chat Ready: ${String(Boolean(state.browser?.chatReady))}`,
|
|
231
|
+
`- Agent Ready: ${String(Boolean(state.browser?.agentReady))}`,
|
|
232
|
+
`- Share Auth: ${String(Boolean(state.browser?.shareAuthState))}`,
|
|
233
|
+
`- Agent Mode: ${String(Boolean(state.agent?.enabled))}`,
|
|
234
|
+
`- Auto Execute: ${String(Boolean(state.agent?.autoExecuteCommands))}`,
|
|
235
|
+
`- Allow Shell: ${String(Boolean(state.agent?.allowShell))}`,
|
|
236
|
+
`- Allow File Write: ${String(Boolean(state.agent?.allowFileWrite))}`,
|
|
237
|
+
`- Multi Agent: ${String(Boolean(state.agent?.multiAgent))}`,
|
|
238
|
+
`- Workspace: ${state.workspaceRoot || '(无)'}`
|
|
239
|
+
].join('\n');
|
|
240
|
+
} catch {
|
|
241
|
+
return result.output || result.message || '已读取应用状态。';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
switch (intent?.reason) {
|
|
246
|
+
case 'file-exists':
|
|
247
|
+
return result.output || result.message || '已完成文件检查。';
|
|
248
|
+
case 'list-workspace':
|
|
249
|
+
return [
|
|
250
|
+
'当前工作区文件如下:',
|
|
251
|
+
'',
|
|
252
|
+
result.output || '(空目录)'
|
|
253
|
+
].join('\n');
|
|
254
|
+
case 'read-file':
|
|
255
|
+
return [
|
|
256
|
+
`已读取 ${intent.command?.args?.path || '文件'}:`,
|
|
257
|
+
'',
|
|
258
|
+
result.output || '(空文件)'
|
|
259
|
+
].join('\n');
|
|
260
|
+
case 'write-file':
|
|
261
|
+
return result.message || '文件已写入。';
|
|
262
|
+
case 'delete-file':
|
|
263
|
+
return result.message || '文件已删除。';
|
|
264
|
+
case 'explicit-shell':
|
|
265
|
+
return result.output || result.message || '系统命令已执行。';
|
|
266
|
+
case 'switch-provider-natural':
|
|
267
|
+
return result.output || result.message || '已切换提供方。';
|
|
268
|
+
case 'show-provider-status':
|
|
269
|
+
case 'show-app-state':
|
|
270
|
+
case 'show-config-summary':
|
|
271
|
+
case 'open-task-panel':
|
|
272
|
+
case 'open-memory-panel':
|
|
273
|
+
case 'open-sessions-panel':
|
|
274
|
+
case 'open-settings':
|
|
275
|
+
case 'open-command-palette':
|
|
276
|
+
return result.output || result.message || '已执行应用控制操作。';
|
|
277
|
+
default:
|
|
278
|
+
return result.output || result.message || '本地任务已执行。';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import config from '../utils/config.mjs';
|
|
4
|
+
|
|
5
|
+
export class Logger {
|
|
6
|
+
constructor({
|
|
7
|
+
dir = config.logger.dir,
|
|
8
|
+
filename = config.logger.filename,
|
|
9
|
+
maxSize = config.logger.maxSize,
|
|
10
|
+
maxFiles = config.logger.maxFiles,
|
|
11
|
+
fsModule = fs,
|
|
12
|
+
clock = () => new Date()
|
|
13
|
+
} = {}) {
|
|
14
|
+
this.dir = dir;
|
|
15
|
+
this.filename = filename;
|
|
16
|
+
this.maxSize = maxSize;
|
|
17
|
+
this.maxFiles = maxFiles;
|
|
18
|
+
this.fs = fsModule;
|
|
19
|
+
this.clock = clock;
|
|
20
|
+
this.logFile = resolve(this.dir, this.filename);
|
|
21
|
+
this.queue = [];
|
|
22
|
+
this.writing = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async init() {
|
|
26
|
+
try {
|
|
27
|
+
await this.fs.mkdir(this.dir, { recursive: true });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Failed to create log directory:', error.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async log(role, content) {
|
|
34
|
+
const timestamp = this.clock().toLocaleString('zh-CN');
|
|
35
|
+
const entry = `[${timestamp}] ${role}: ${content}\n`;
|
|
36
|
+
|
|
37
|
+
this.queue.push(entry);
|
|
38
|
+
|
|
39
|
+
if (!this.writing) {
|
|
40
|
+
await this._flush();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _flush() {
|
|
45
|
+
if (this.queue.length === 0) return;
|
|
46
|
+
|
|
47
|
+
this.writing = true;
|
|
48
|
+
|
|
49
|
+
while (this.queue.length > 0) {
|
|
50
|
+
const entries = this.queue.splice(0, 10);
|
|
51
|
+
try {
|
|
52
|
+
await this.fs.appendFile(this.logFile, entries.join(''));
|
|
53
|
+
await this._rotateIfNeeded();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Failed to write log:', error.message);
|
|
56
|
+
this.queue.unshift(...entries);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.writing = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _rotateIfNeeded() {
|
|
65
|
+
try {
|
|
66
|
+
const stats = await this.fs.stat(this.logFile);
|
|
67
|
+
|
|
68
|
+
if (stats.size > this.maxSize) {
|
|
69
|
+
const timestamp = this.clock().toISOString().replace(/[:.]/g, '-');
|
|
70
|
+
const archivePath = this.logFile.replace('.txt', `_${timestamp}.txt`);
|
|
71
|
+
await this.fs.rename(this.logFile, archivePath);
|
|
72
|
+
|
|
73
|
+
await this._cleanOldLogs();
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error.code !== 'ENOENT') {
|
|
77
|
+
console.error('Failed to rotate log:', error.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _cleanOldLogs() {
|
|
83
|
+
try {
|
|
84
|
+
const files = await this.fs.readdir(this.dir);
|
|
85
|
+
const logFiles = files
|
|
86
|
+
.filter((file) => file.startsWith('chat_history') && file.endsWith('.txt'))
|
|
87
|
+
.map((file) => ({
|
|
88
|
+
name: file,
|
|
89
|
+
path: resolve(this.dir, file)
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
if (logFiles.length > this.maxFiles) {
|
|
93
|
+
const stats = await Promise.all(
|
|
94
|
+
logFiles.map(async (file) => ({
|
|
95
|
+
...file,
|
|
96
|
+
mtime: (await this.fs.stat(file.path)).mtime
|
|
97
|
+
}))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
stats.sort((a, b) => a.mtime - b.mtime);
|
|
101
|
+
|
|
102
|
+
const toDelete = stats.slice(0, stats.length - this.maxFiles);
|
|
103
|
+
await Promise.all(toDelete.map((file) => this.fs.unlink(file.path)));
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Failed to clean old logs:', error.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default new Logger();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const AGENT_ROLE_DEFINITIONS = {
|
|
2
|
+
orchestrator: {
|
|
3
|
+
id: 'orchestrator',
|
|
4
|
+
label: '主控',
|
|
5
|
+
description: '负责拆解目标、选择下一步和汇总结果'
|
|
6
|
+
},
|
|
7
|
+
browser: {
|
|
8
|
+
id: 'browser',
|
|
9
|
+
label: '网页侦察',
|
|
10
|
+
description: '负责打开网页、读取页面、提取内容和浏览器操作'
|
|
11
|
+
},
|
|
12
|
+
shell: {
|
|
13
|
+
id: 'shell',
|
|
14
|
+
label: '系统执行',
|
|
15
|
+
description: '负责系统命令、脚本运行和工作区环境探查'
|
|
16
|
+
},
|
|
17
|
+
files: {
|
|
18
|
+
id: 'files',
|
|
19
|
+
label: '文件执行',
|
|
20
|
+
description: '负责读取、写入、删除和整理工作区文件'
|
|
21
|
+
},
|
|
22
|
+
operator: {
|
|
23
|
+
id: 'operator',
|
|
24
|
+
label: '人工接管',
|
|
25
|
+
description: '负责登录、验证码、授权和必须人工判断的步骤'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ROLE_ALIASES = new Map([
|
|
30
|
+
['planner', 'orchestrator'],
|
|
31
|
+
['supervisor', 'orchestrator'],
|
|
32
|
+
['manager', 'orchestrator'],
|
|
33
|
+
['brain', 'orchestrator'],
|
|
34
|
+
['browser', 'browser'],
|
|
35
|
+
['web', 'browser'],
|
|
36
|
+
['research', 'browser'],
|
|
37
|
+
['researcher', 'browser'],
|
|
38
|
+
['navigator', 'browser'],
|
|
39
|
+
['shell', 'shell'],
|
|
40
|
+
['terminal', 'shell'],
|
|
41
|
+
['command', 'shell'],
|
|
42
|
+
['executor', 'shell'],
|
|
43
|
+
['files', 'files'],
|
|
44
|
+
['file', 'files'],
|
|
45
|
+
['writer', 'files'],
|
|
46
|
+
['editor', 'files'],
|
|
47
|
+
['operator', 'operator'],
|
|
48
|
+
['handoff', 'operator'],
|
|
49
|
+
['human', 'operator'],
|
|
50
|
+
['manual', 'operator']
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const COMMAND_ROLE_HINTS = new Map([
|
|
54
|
+
['open_url', 'browser'],
|
|
55
|
+
['read_agent_page', 'browser'],
|
|
56
|
+
['send_agent_page', 'browser'],
|
|
57
|
+
['click_selector', 'browser'],
|
|
58
|
+
['type_selector', 'browser'],
|
|
59
|
+
['wait_for_text', 'browser'],
|
|
60
|
+
['extract_selector_text', 'browser'],
|
|
61
|
+
['wait_for_selector', 'browser'],
|
|
62
|
+
['wait_for_navigation', 'browser'],
|
|
63
|
+
['hover_selector', 'browser'],
|
|
64
|
+
['press_key', 'browser'],
|
|
65
|
+
['check_checkbox', 'browser'],
|
|
66
|
+
['uncheck_checkbox', 'browser'],
|
|
67
|
+
['select_option', 'browser'],
|
|
68
|
+
['scroll_page', 'browser'],
|
|
69
|
+
['screenshot_agent_page', 'browser'],
|
|
70
|
+
['inspect_dom', 'browser'],
|
|
71
|
+
['run_shell', 'shell'],
|
|
72
|
+
['list_files', 'files'],
|
|
73
|
+
['file_exists', 'files'],
|
|
74
|
+
['read_file', 'files'],
|
|
75
|
+
['write_file', 'files'],
|
|
76
|
+
['delete_file', 'files'],
|
|
77
|
+
['get_app_state', 'orchestrator'],
|
|
78
|
+
['send_message', 'orchestrator'],
|
|
79
|
+
['focus_chat_input', 'browser'],
|
|
80
|
+
['send_chat_input', 'browser'],
|
|
81
|
+
['submit_current_input', 'browser'],
|
|
82
|
+
['start_new_chat', 'browser'],
|
|
83
|
+
['handoff', 'operator']
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
export function normalizeAgentRole(value) {
|
|
87
|
+
const normalized = String(value ?? '')
|
|
88
|
+
.trim()
|
|
89
|
+
.toLowerCase();
|
|
90
|
+
|
|
91
|
+
if (!normalized) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return ROLE_ALIASES.get(normalized) || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function inferAgentRoleFromCommand(command) {
|
|
99
|
+
if (!command?.name) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return COMMAND_ROLE_HINTS.get(command.name) || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveEnvelopeAgentRole(payload = {}) {
|
|
107
|
+
return (
|
|
108
|
+
normalizeAgentRole(payload.agent) ||
|
|
109
|
+
normalizeAgentRole(payload.role) ||
|
|
110
|
+
normalizeAgentRole(payload.specialist) ||
|
|
111
|
+
inferAgentRoleFromCommand(payload.command) ||
|
|
112
|
+
(payload.type === 'handoff' ? 'operator' : null)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getAgentRoleLabel(role) {
|
|
117
|
+
return AGENT_ROLE_DEFINITIONS[role]?.label || '未指定';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getAgentRoleDescription(role) {
|
|
121
|
+
return AGENT_ROLE_DEFINITIONS[role]?.description || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getEnabledAgentRoster({
|
|
125
|
+
multiAgentEnabled = false,
|
|
126
|
+
allowShell = true,
|
|
127
|
+
allowFileWrite = true
|
|
128
|
+
} = {}) {
|
|
129
|
+
const roster = [
|
|
130
|
+
AGENT_ROLE_DEFINITIONS.orchestrator,
|
|
131
|
+
AGENT_ROLE_DEFINITIONS.browser
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
if (multiAgentEnabled && allowShell) {
|
|
135
|
+
roster.push(AGENT_ROLE_DEFINITIONS.shell);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (multiAgentEnabled && allowFileWrite) {
|
|
139
|
+
roster.push(AGENT_ROLE_DEFINITIONS.files);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
roster.push(AGENT_ROLE_DEFINITIONS.operator);
|
|
143
|
+
return roster;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function buildMultiAgentPromptSection(options = {}) {
|
|
147
|
+
const {
|
|
148
|
+
multiAgentEnabled = false,
|
|
149
|
+
allowShell = true,
|
|
150
|
+
allowFileWrite = true
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
if (!multiAgentEnabled) {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const roster = getEnabledAgentRoster({
|
|
158
|
+
multiAgentEnabled,
|
|
159
|
+
allowShell,
|
|
160
|
+
allowFileWrite
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const rosterText = roster
|
|
164
|
+
.map((agent) => `${agent.id}: ${agent.description}`)
|
|
165
|
+
.join(';');
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
'当前启用了多 Agent 协作模式。',
|
|
169
|
+
`可用角色: ${rosterText}。`,
|
|
170
|
+
'你仍然只代表一个总控 Agent 输出一个 JSON 对象,但要像在调度一支小队那样工作。',
|
|
171
|
+
'请优先思考哪一个角色最适合执行当前这一步,并尽量在 JSON 中附带可选字段 agent,值只能是 orchestrator、browser、shell、files、operator 之一。',
|
|
172
|
+
'如果这一步是观察网页、读取页面、点击或提取内容,agent 应优先是 browser。',
|
|
173
|
+
'如果这一步是系统命令或脚本执行,agent 应优先是 shell。',
|
|
174
|
+
'如果这一步是读写删文件,agent 应优先是 files。',
|
|
175
|
+
'如果这一步是需要你汇总、判断、继续规划或向模型发送后续消息,agent 应优先是 orchestrator。',
|
|
176
|
+
'如果这一步需要用户登录、验证码、授权或人工判断,agent 应优先是 operator,并使用 handoff。'
|
|
177
|
+
].join(' ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
AGENT_ROLE_DEFINITIONS,
|
|
182
|
+
COMMAND_ROLE_HINTS
|
|
183
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function isRecoverableNavigationError(error) {
|
|
2
|
+
const message = String(error?.message || '');
|
|
3
|
+
|
|
4
|
+
return /ERR_CONNECTION_TIMED_OUT|ERR_TIMED_OUT|ERR_NAME_NOT_RESOLVED|ERR_CONNECTION_RESET|ERR_NETWORK_CHANGED|ERR_INTERNET_DISCONNECTED|ERR_CONNECTION_CLOSED|ERR_ABORTED|Timeout/i.test(
|
|
5
|
+
message
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function summarizeNavigationError(error) {
|
|
10
|
+
const message = String(error?.message || '未知错误');
|
|
11
|
+
return message.split('\n')[0].trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildNavigationRecoveryRows({
|
|
15
|
+
title = '页面连接失败',
|
|
16
|
+
url = '',
|
|
17
|
+
attempt = 1,
|
|
18
|
+
maxAttempts = 1,
|
|
19
|
+
error = null
|
|
20
|
+
} = {}) {
|
|
21
|
+
return {
|
|
22
|
+
title,
|
|
23
|
+
rows: [
|
|
24
|
+
{ label: '目标', value: url || '(未知 URL)' },
|
|
25
|
+
{ label: '尝试', value: `${attempt}/${maxAttempts}` },
|
|
26
|
+
{ label: '错误', value: summarizeNavigationError(error) }
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
isRecoverableNavigationError,
|
|
33
|
+
summarizeNavigationError,
|
|
34
|
+
buildNavigationRecoveryRows
|
|
35
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function sanitizeFilePart(value) {
|
|
5
|
+
return String(value ?? '')
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-')
|
|
8
|
+
.replace(/\s+/g, '-')
|
|
9
|
+
.slice(0, 80);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseProviderFilter(raw = '') {
|
|
13
|
+
return raw
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((item) => item.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function filterProviders(providers, filterIds = []) {
|
|
20
|
+
if (filterIds.length === 0) {
|
|
21
|
+
return providers.filter((provider) => provider.enabled !== false);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const allow = new Set(filterIds);
|
|
25
|
+
return providers.filter((provider) => allow.has(provider.id));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function ensureArtifactDir(dir) {
|
|
29
|
+
await fs.mkdir(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function saveProviderArtifacts({
|
|
33
|
+
artifactRoot,
|
|
34
|
+
providerId,
|
|
35
|
+
page,
|
|
36
|
+
payload
|
|
37
|
+
}) {
|
|
38
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
39
|
+
const safeProviderId = sanitizeFilePart(providerId);
|
|
40
|
+
const baseDir = resolve(artifactRoot, `${safeProviderId}-${stamp}`);
|
|
41
|
+
|
|
42
|
+
await ensureArtifactDir(baseDir);
|
|
43
|
+
|
|
44
|
+
const artifact = {
|
|
45
|
+
dir: baseDir
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (payload) {
|
|
49
|
+
const payloadPath = resolve(baseDir, 'result.json');
|
|
50
|
+
await fs.writeFile(payloadPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
51
|
+
artifact.payloadPath = payloadPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const browserManager = page?.__xshatBrowserManager;
|
|
55
|
+
const channelName = page?.__xshatChannelName || 'chat';
|
|
56
|
+
if (browserManager && typeof browserManager.readChannelSnapshot === 'function') {
|
|
57
|
+
try {
|
|
58
|
+
const snapshot = await browserManager.readChannelSnapshot(channelName, {
|
|
59
|
+
maxChars: 3000,
|
|
60
|
+
includeDomSummary: true,
|
|
61
|
+
includeObservation: true,
|
|
62
|
+
includeHtml: true
|
|
63
|
+
});
|
|
64
|
+
const observationPath = resolve(baseDir, 'page-observation.json');
|
|
65
|
+
await fs.writeFile(
|
|
66
|
+
observationPath,
|
|
67
|
+
`${JSON.stringify(snapshot, null, 2)}\n`,
|
|
68
|
+
'utf-8'
|
|
69
|
+
);
|
|
70
|
+
artifact.observationPath = observationPath;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
artifact.observationError = error.message;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (page?.content) {
|
|
77
|
+
try {
|
|
78
|
+
const html = await page.content();
|
|
79
|
+
const htmlPath = resolve(baseDir, 'page.html');
|
|
80
|
+
await fs.writeFile(htmlPath, html, 'utf-8');
|
|
81
|
+
artifact.htmlPath = htmlPath;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
artifact.htmlError = error.message;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (page?.screenshot) {
|
|
88
|
+
try {
|
|
89
|
+
const screenshotPath = resolve(baseDir, 'page.png');
|
|
90
|
+
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
91
|
+
artifact.screenshotPath = screenshotPath;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
artifact.screenshotError = error.message;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return artifact;
|
|
98
|
+
}
|