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.
@@ -0,0 +1,511 @@
1
+ import config from '../utils/config.mjs';
2
+ import { runAgentLoop } from './agent-loop.mjs';
3
+ import { buildAgentPrimer } from './chat-helpers.mjs';
4
+ import { requiresBrowserForProvider } from './provider-runtime.mjs';
5
+ import { getAgentSystemPrompt } from './response-parser.mjs';
6
+ import { normalizeResponseEnvelope } from './response-pipeline.mjs';
7
+ import {
8
+ getAgentRoleLabel,
9
+ inferAgentRoleFromCommand
10
+ } from './multi-agent.mjs';
11
+
12
+ function sleep(ms) {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
15
+
16
+ function nowIso() {
17
+ return new Date().toISOString();
18
+ }
19
+
20
+ function isDue(task) {
21
+ if (task.status !== 'queued') {
22
+ return false;
23
+ }
24
+
25
+ if (!task.nextRunAt) {
26
+ return true;
27
+ }
28
+
29
+ return new Date(task.nextRunAt).getTime() <= Date.now();
30
+ }
31
+
32
+ function normalizeTaskMode(mode = 'once') {
33
+ const value = String(mode || 'once').trim().toLowerCase();
34
+ if (['loop', 'watch', 'interval'].includes(value)) {
35
+ return value;
36
+ }
37
+ return 'once';
38
+ }
39
+
40
+ function resolveNextRun(task, { success = true } = {}) {
41
+ const mode = normalizeTaskMode(task.mode);
42
+ const intervalMs = Math.max(0, Number(task.intervalMs) || 0);
43
+ const retryDelayMs = Math.max(
44
+ 0,
45
+ Number(task.retryDelayMs || intervalMs || 5000) || 5000
46
+ );
47
+
48
+ if (!success) {
49
+ const retryLimit = Math.max(0, Number(task.retryLimit) || 0);
50
+ const retryCount = Math.max(0, Number(task.retryCount) || 0);
51
+ if (retryCount < retryLimit) {
52
+ return new Date(Date.now() + retryDelayMs).toISOString();
53
+ }
54
+ if (mode !== 'once' && intervalMs > 0) {
55
+ return new Date(Date.now() + intervalMs).toISOString();
56
+ }
57
+ return null;
58
+ }
59
+
60
+ if (mode === 'once') {
61
+ return null;
62
+ }
63
+
64
+ if (mode === 'loop' || mode === 'watch') {
65
+ return nowIso();
66
+ }
67
+
68
+ if (mode === 'interval' && intervalMs > 0) {
69
+ return new Date(Date.now() + intervalMs).toISOString();
70
+ }
71
+
72
+ return nowIso();
73
+ }
74
+
75
+ function taskPrompt(task) {
76
+ return [
77
+ `你正在执行一个长期任务。任务标题: ${task.title}`,
78
+ `任务目标: ${task.goal}`,
79
+ '优先做最小必要动作,并在完成后给出简短总结。',
80
+ '如果需要继续任务,请输出 action。',
81
+ '如果任务已经完成,请输出 reply。'
82
+ ].join('\n');
83
+ }
84
+
85
+ function isMultiAgentEnabled() {
86
+ return Boolean(config.agent?.enabled) && Boolean(config.agent?.multiAgent);
87
+ }
88
+
89
+ function resolveAgentRole(command = null, explicitRole = null) {
90
+ return explicitRole || inferAgentRoleFromCommand(command) || 'orchestrator';
91
+ }
92
+
93
+ function formatHistoryDetail(parsed) {
94
+ const roleLabel = parsed.agentLabel || getAgentRoleLabel(resolveAgentRole(parsed.command, parsed.agent));
95
+ const main = parsed.displayText || parsed.text || parsed.command?.name || parsed.kind;
96
+ return roleLabel ? `${roleLabel} · ${main}` : main;
97
+ }
98
+
99
+ function getTaskErrorMessage(error, fallback = '任务执行失败') {
100
+ if (!error) {
101
+ return fallback;
102
+ }
103
+
104
+ const message = String(error?.message || error).trim();
105
+ return message || fallback;
106
+ }
107
+
108
+ export class TaskRunner {
109
+ constructor({
110
+ taskStore,
111
+ browserManager,
112
+ logger,
113
+ getProvider,
114
+ createChatManager,
115
+ createAgentExecutor,
116
+ sessionCompressionOptions,
117
+ pollInterval = config.tasks.pollInterval
118
+ }) {
119
+ this.taskStore = taskStore;
120
+ this.browserManager = browserManager;
121
+ this.logger = logger;
122
+ this.getProvider = getProvider;
123
+ this.createChatManager = createChatManager;
124
+ this.createAgentExecutor = createAgentExecutor;
125
+ this.sessionCompressionOptions = sessionCompressionOptions;
126
+ this.pollInterval = pollInterval;
127
+ this.activeRuns = new Map();
128
+ this.loopTimer = null;
129
+ this.running = false;
130
+ }
131
+
132
+ async getRuntimeSnapshot() {
133
+ const tasks = await this.taskStore.list();
134
+ const activeIds = new Set(this.activeRuns.keys());
135
+ return {
136
+ total: tasks.length,
137
+ activeTaskIds: [...activeIds],
138
+ idle: tasks.filter((task) => task.status === 'idle').length,
139
+ queued: tasks.filter((task) => task.status === 'queued').length,
140
+ running: tasks.filter((task) => task.status === 'running').length,
141
+ completed: tasks.filter((task) => task.status === 'completed').length,
142
+ stopped: tasks.filter((task) => task.status === 'stopped').length,
143
+ handoff: tasks.filter((task) => task.status === 'handoff').length,
144
+ failed: tasks.filter((task) => task.status === 'failed').length,
145
+ tasks
146
+ };
147
+ }
148
+
149
+ async init() {
150
+ await this.taskStore.init();
151
+ }
152
+
153
+ startLoop() {
154
+ if (this.loopTimer) {
155
+ return;
156
+ }
157
+
158
+ this.loopTimer = setInterval(() => {
159
+ this.tick().catch(() => {});
160
+ }, this.pollInterval);
161
+ }
162
+
163
+ stopLoop() {
164
+ if (this.loopTimer) {
165
+ clearInterval(this.loopTimer);
166
+ this.loopTimer = null;
167
+ }
168
+ }
169
+
170
+ async tick() {
171
+ const tasks = await this.taskStore.list();
172
+ const dueTasks = tasks.filter((task) => isDue(task));
173
+
174
+ for (const task of dueTasks) {
175
+ if (this.activeRuns.has(task.id)) {
176
+ continue;
177
+ }
178
+ this.runTask(task.id).catch(() => {});
179
+ }
180
+ }
181
+
182
+ async createTask({
183
+ title,
184
+ goal,
185
+ provider,
186
+ mode = 'once',
187
+ intervalMs = null,
188
+ retryLimit = 0,
189
+ retryDelayMs = null
190
+ }) {
191
+ const task = this.taskStore.create({
192
+ title,
193
+ goal,
194
+ provider,
195
+ mode,
196
+ intervalMs,
197
+ retryLimit,
198
+ retryDelayMs
199
+ });
200
+ return this.taskStore.save(task);
201
+ }
202
+
203
+ async updateTask(id, updater) {
204
+ return this.taskStore.update(id, updater);
205
+ }
206
+
207
+ async queueTask(id) {
208
+ return this.taskStore.update(id, (task) => ({
209
+ ...task,
210
+ status: 'queued',
211
+ stopRequested: false,
212
+ retryCount: 0,
213
+ lastError: null,
214
+ handoffUrl: null,
215
+ nextRunAt: task.nextRunAt || nowIso()
216
+ }));
217
+ }
218
+
219
+ async retryTask(id, { run = true } = {}) {
220
+ const updated = await this.taskStore.update(id, (task) => ({
221
+ ...task,
222
+ status: 'queued',
223
+ stopRequested: false,
224
+ retryCount: 0,
225
+ lastError: null,
226
+ handoffUrl: null,
227
+ nextRunAt: nowIso()
228
+ }));
229
+
230
+ if (updated && run) {
231
+ this.runTask(id).catch(() => {});
232
+ }
233
+
234
+ return updated;
235
+ }
236
+
237
+ async stopTask(id) {
238
+ return this.taskStore.update(id, {
239
+ stopRequested: true,
240
+ status: 'stopping'
241
+ });
242
+ }
243
+
244
+ async runTask(id) {
245
+ if (this.activeRuns.has(id)) {
246
+ return this.activeRuns.get(id);
247
+ }
248
+
249
+ const promise = this.#runTaskInternal(id).finally(() => {
250
+ this.activeRuns.delete(id);
251
+ });
252
+ this.activeRuns.set(id, promise);
253
+ return promise;
254
+ }
255
+
256
+ async #recordFailure(id, task, error, {
257
+ fallbackMessage = '任务执行失败',
258
+ appendHistory = true
259
+ } = {}) {
260
+ const currentTask = (await this.taskStore.load(id)) || task;
261
+ if (!currentTask) {
262
+ return null;
263
+ }
264
+
265
+ const errorMessage = getTaskErrorMessage(error, fallbackMessage);
266
+ const retryCount = Math.max(0, Number(currentTask.retryCount) || 0) + 1;
267
+ const nextRunAt = resolveNextRun({
268
+ ...currentTask,
269
+ retryCount
270
+ }, { success: false });
271
+
272
+ const updated = await this.taskStore.update(id, {
273
+ status: nextRunAt ? 'queued' : 'failed',
274
+ retryCount,
275
+ nextRunAt,
276
+ lastError: errorMessage,
277
+ lastResult: errorMessage
278
+ });
279
+
280
+ if (appendHistory) {
281
+ await this.taskStore.appendHistory(id, {
282
+ type: 'error',
283
+ label: nextRunAt ? '任务失败,等待重试' : '任务失败',
284
+ detail: errorMessage
285
+ });
286
+ }
287
+
288
+ return updated;
289
+ }
290
+
291
+ async #runTaskInternal(id) {
292
+ let task = await this.taskStore.load(id);
293
+ if (!task) {
294
+ return null;
295
+ }
296
+
297
+ try {
298
+ task = await this.taskStore.update(id, {
299
+ status: 'running',
300
+ stopRequested: false,
301
+ lastError: null,
302
+ lastRunAt: new Date().toISOString(),
303
+ runCount: (task.runCount || 0) + 1
304
+ });
305
+
306
+ await this.taskStore.appendHistory(id, {
307
+ type: 'status',
308
+ label: '任务启动',
309
+ detail: task.goal
310
+ });
311
+
312
+ const provider = this.getProvider(task.provider);
313
+ if (!provider) {
314
+ throw new Error(`未找到提供方: ${task.provider || '(空)'}`);
315
+ }
316
+
317
+ if (requiresBrowserForProvider(provider)) {
318
+ if (!this.browserManager.isReady()) {
319
+ await this.browserManager.init();
320
+ }
321
+
322
+ await this.browserManager.goto(provider.url);
323
+ await sleep(1200);
324
+ }
325
+
326
+ const chatManager = this.createChatManager(provider);
327
+ const agentExecutor = this.createAgentExecutor();
328
+
329
+ const primer = buildAgentPrimer(
330
+ getAgentSystemPrompt({
331
+ providerId: provider?.id || '',
332
+ multiAgentEnabled: isMultiAgentEnabled(),
333
+ allowShell: Boolean(config.agent?.allowShell),
334
+ allowFileWrite: Boolean(config.agent?.allowFileWrite)
335
+ }),
336
+ [
337
+ { role: 'user', content: taskPrompt(task) }
338
+ ],
339
+ this.sessionCompressionOptions()
340
+ );
341
+
342
+ const primerSent = await chatManager.sendMessage(primer);
343
+ if (primerSent) {
344
+ try {
345
+ await sleep(800);
346
+ await chatManager.waitForResponse(() => {});
347
+ } catch {
348
+ // ignore primer ack timeout
349
+ }
350
+ }
351
+
352
+ const sent = await chatManager.sendMessage(taskPrompt(task));
353
+ if (!sent) {
354
+ await this.#recordFailure(id, task, '任务消息发送失败');
355
+ return null;
356
+ }
357
+
358
+ await this.taskStore.appendMessage(id, 'user', task.goal);
359
+
360
+ let step = 0;
361
+ const maxSteps = Math.max(2, Number(config.agent?.maxAutoSteps) || 4);
362
+ let lastResult = null;
363
+
364
+ while (step < maxSteps) {
365
+ const latestTask = await this.taskStore.load(id);
366
+ if (!latestTask || latestTask.stopRequested) {
367
+ await this.taskStore.update(id, {
368
+ status: 'stopped',
369
+ nextRunAt: null,
370
+ lastResult: lastResult?.message || '任务已停止'
371
+ });
372
+ await this.taskStore.appendHistory(id, {
373
+ type: 'status',
374
+ label: '任务停止',
375
+ detail: '收到停止请求'
376
+ });
377
+ return null;
378
+ }
379
+
380
+ const response = await chatManager.waitForResponse(() => {});
381
+ await this.taskStore.appendMessage(id, 'assistant', response);
382
+
383
+ const parsed = normalizeResponseEnvelope(response, { agentMode: true });
384
+
385
+ await this.taskStore.appendHistory(id, {
386
+ type: 'model',
387
+ label: `第 ${step + 1} 步`,
388
+ detail: formatHistoryDetail(parsed),
389
+ agentRole: resolveAgentRole(parsed.command, parsed.agent),
390
+ agentLabel: parsed.agentLabel || getAgentRoleLabel(resolveAgentRole(parsed.command, parsed.agent))
391
+ });
392
+
393
+ const outcome = await runAgentLoop({
394
+ initialEnvelope: parsed,
395
+ initialStep: step,
396
+ maxSteps,
397
+ agentExecutor,
398
+ requestFollowupResponse: async (followupMessage) => {
399
+ const followupSent = await chatManager.sendMessage(followupMessage);
400
+ if (!followupSent) {
401
+ throw new Error('后续消息发送失败');
402
+ }
403
+
404
+ return chatManager.waitForResponse(() => {});
405
+ },
406
+ onAfterCommand: async ({ envelope, result }) => {
407
+ lastResult = result;
408
+ await this.taskStore.appendHistory(id, {
409
+ type: 'action',
410
+ label: envelope.command.name,
411
+ detail: result.ok
412
+ ? result.message || '执行完成'
413
+ : result.error || '执行失败',
414
+ agentRole: result.agent,
415
+ agentLabel: getAgentRoleLabel(result.agent)
416
+ });
417
+ },
418
+ onFollowupResponse: async ({ rawResponse, envelope, step }) => {
419
+ await this.taskStore.appendMessage(id, 'assistant', rawResponse);
420
+ await this.taskStore.appendHistory(id, {
421
+ type: 'model',
422
+ label: `第 ${step + 1} 步`,
423
+ detail: formatHistoryDetail(envelope),
424
+ agentRole: resolveAgentRole(envelope.command, envelope.agent),
425
+ agentLabel:
426
+ envelope.agentLabel ||
427
+ getAgentRoleLabel(resolveAgentRole(envelope.command, envelope.agent))
428
+ });
429
+ }
430
+ });
431
+
432
+ step = outcome.step;
433
+
434
+ if (outcome.status === 'error') {
435
+ await this.#recordFailure(id, await this.taskStore.load(id), outcome.result?.error || '命令执行失败');
436
+ return outcome.result;
437
+ }
438
+
439
+ if (outcome.status === 'handoff') {
440
+ await this.taskStore.update(id, {
441
+ status: 'handoff',
442
+ nextRunAt: null,
443
+ handoffUrl: this.browserManager.getCurrentUrl(),
444
+ lastResult:
445
+ outcome.result?.message ||
446
+ outcome.envelope?.text ||
447
+ '需要人工接管'
448
+ });
449
+ await this.taskStore.appendHistory(id, {
450
+ type: 'status',
451
+ label: '等待接管',
452
+ detail: outcome.result?.message || outcome.envelope?.text || '需要人工接管'
453
+ });
454
+ return outcome.result || outcome.envelope;
455
+ }
456
+
457
+ if (outcome.status === 'completed' || outcome.status === 'message') {
458
+ const currentTask = await this.taskStore.load(id);
459
+ const nextRunAt = resolveNextRun(currentTask, { success: true });
460
+ await this.taskStore.update(id, {
461
+ status: nextRunAt ? 'queued' : 'completed',
462
+ retryCount: 0,
463
+ nextRunAt,
464
+ lastCompletedAt: nowIso(),
465
+ lastResult:
466
+ outcome.result?.message ||
467
+ outcome.envelope?.text ||
468
+ '任务已完成'
469
+ });
470
+ await this.taskStore.appendHistory(id, {
471
+ type: 'status',
472
+ label: nextRunAt ? '本轮完成,等待下一次运行' : '任务完成',
473
+ detail:
474
+ outcome.result?.message ||
475
+ outcome.envelope?.text ||
476
+ '任务已完成'
477
+ });
478
+ return outcome.result || outcome.envelope;
479
+ }
480
+
481
+ if (outcome.status === 'limit') {
482
+ lastResult = outcome.result || lastResult;
483
+ }
484
+ }
485
+
486
+ const currentTask = await this.taskStore.load(id);
487
+ const nextRunAt = resolveNextRun(currentTask, { success: true });
488
+ await this.taskStore.update(id, {
489
+ status: nextRunAt ? 'queued' : 'completed',
490
+ retryCount: 0,
491
+ nextRunAt,
492
+ lastCompletedAt: nowIso(),
493
+ lastResult: lastResult?.message || '达到最大自动步数后结束'
494
+ });
495
+ await this.taskStore.appendHistory(id, {
496
+ type: 'status',
497
+ label: nextRunAt ? '本轮结束,等待下一次运行' : '任务完成',
498
+ detail: lastResult?.message || '达到最大自动步数后结束'
499
+ });
500
+ return lastResult;
501
+ } catch (error) {
502
+ await this.#recordFailure(id, task, error);
503
+ return {
504
+ ok: false,
505
+ error: getTaskErrorMessage(error)
506
+ };
507
+ }
508
+ }
509
+ }
510
+
511
+ export default TaskRunner;