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/src/index.mjs ADDED
@@ -0,0 +1,2865 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'process';
3
+ import { resolve } from 'node:path';
4
+ import { confirm, input as promptInput, select } from '@inquirer/prompts';
5
+ import AgentExecutor from './modules/agent-executor.mjs';
6
+ import browserManager from './modules/browser.mjs';
7
+ import ConversationAgent from './modules/agent.mjs';
8
+ import { buildQuickStartDoc } from './modules/docs.mjs';
9
+ import sessionManager from './modules/session.mjs';
10
+ import taskStore from './modules/task-store.mjs';
11
+ import TaskRunner from './modules/task-runner.mjs';
12
+ import logger from './modules/logger.mjs';
13
+ import ui from './modules/ui.mjs';
14
+ import { showSettingsMenu } from './modules/settings-menu.mjs';
15
+ import {
16
+ browseSessions,
17
+ resumeLatestSession
18
+ } from './modules/session-browser.mjs';
19
+ import {
20
+ buildNavigationRecoveryRows,
21
+ isRecoverableNavigationError
22
+ } from './modules/navigation-recovery.mjs';
23
+ import {
24
+ buildAgentPrimer,
25
+ buildHistoryPrimer,
26
+ wrapAgentTurnMessage
27
+ } from './modules/chat-helpers.mjs';
28
+ import { runAgentLoop } from './modules/agent-loop.mjs';
29
+ import {
30
+ getEnabledAgentRoster,
31
+ getAgentRoleLabel,
32
+ inferAgentRoleFromCommand
33
+ } from './modules/multi-agent.mjs';
34
+ import {
35
+ detectLocalIntent,
36
+ formatLocalIntentResult
37
+ } from './modules/local-intent-router.mjs';
38
+ import {
39
+ createProviderChatManager,
40
+ getProviderSessionUrl,
41
+ isApiProvider,
42
+ requiresBrowserForProvider
43
+ } from './modules/provider-runtime.mjs';
44
+ import { getFallbackProvider } from './modules/provider-pool.mjs';
45
+ import { getAgentSystemPrompt } from './modules/response-parser.mjs';
46
+ import {
47
+ isInterruptedTurnResponse,
48
+ normalizeResponseEnvelope
49
+ } from './modules/response-pipeline.mjs';
50
+ import { runRuntimeMaintenance } from './modules/runtime-maintenance.mjs';
51
+ import { waitForKeypress } from './utils/key-input.mjs';
52
+ import config, {
53
+ getProvider,
54
+ listProviders
55
+ } from './utils/config.mjs';
56
+
57
+ let chatManager = null;
58
+ let conversationAgent = null;
59
+ let agentExecutor = null;
60
+ let isShuttingDown = false;
61
+ let hasActiveSession = false;
62
+ let agentMode = Boolean(config.agent?.enabled);
63
+ let agentPrimedSessionId = null;
64
+ let taskRunner = null;
65
+ let chatChromeDirty = true;
66
+ let lastSubmittedQuestion = '';
67
+ let lastAgentOrReplySummary = '';
68
+ const inputHistory = [];
69
+
70
+ function getSessionCompressionOptions() {
71
+ return {
72
+ compressThreshold: Number(config.session?.compressThreshold) || 12,
73
+ keepRecentMessages: Number(config.session?.keepRecentMessages) || 6,
74
+ maxSummaryChars: Number(config.session?.maxSummaryChars) || 1200,
75
+ maxMessageChars: Number(config.session?.maxMessageChars) || 160
76
+ };
77
+ }
78
+
79
+ function isMultiAgentEnabled() {
80
+ return Boolean(config.agent?.enabled) && Boolean(config.agent?.multiAgent);
81
+ }
82
+
83
+ function getMultiAgentRoster() {
84
+ return getEnabledAgentRoster({
85
+ multiAgentEnabled: isMultiAgentEnabled(),
86
+ allowShell: Boolean(config.agent?.allowShell),
87
+ allowFileWrite: Boolean(config.agent?.allowFileWrite)
88
+ });
89
+ }
90
+
91
+ function resolveAgentRole(command = null, explicitRole = null) {
92
+ return explicitRole || inferAgentRoleFromCommand(command) || 'orchestrator';
93
+ }
94
+
95
+ function resolveAgentLabel(command = null, explicitRole = null) {
96
+ return getAgentRoleLabel(resolveAgentRole(command, explicitRole));
97
+ }
98
+
99
+ function buildAgentRuntimeState({
100
+ forceEnabled = null,
101
+ forceAutoExecute = null
102
+ } = {}) {
103
+ const enabled = forceEnabled ?? agentMode;
104
+ const autoExecuteCommands =
105
+ forceAutoExecute ?? Boolean(config.agent?.autoExecuteCommands);
106
+
107
+ return {
108
+ enabled,
109
+ autoExecuteCommands,
110
+ allowShell: Boolean(config.agent?.allowShell),
111
+ allowFileWrite: Boolean(config.agent?.allowFileWrite),
112
+ longRunning: Boolean(config.agent?.longRunning),
113
+ multiAgent: isMultiAgentEnabled(),
114
+ roster: getMultiAgentRoster().map((item) => ({
115
+ id: item.id,
116
+ label: item.label,
117
+ description: item.description
118
+ }))
119
+ };
120
+ }
121
+
122
+ async function setupGracefulShutdown() {
123
+ const shutdown = async (signal, silent = false) => {
124
+ if (isShuttingDown) return;
125
+ isShuttingDown = true;
126
+
127
+ if (!silent) {
128
+ ui.clear();
129
+ ui.printSuccess('再见!');
130
+ }
131
+
132
+ if (chatManager) {
133
+ chatManager.cleanup();
134
+ }
135
+
136
+ await browserManager.close();
137
+ process.exit(0);
138
+ };
139
+
140
+ process.on('SIGINT', () => shutdown('SIGINT', true));
141
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
142
+
143
+ process.on('uncaughtException', async (error) => {
144
+ if (error.name === 'AbortError') {
145
+ await shutdown('SIGINT', true);
146
+ return;
147
+ }
148
+ ui.printError(`未捕获的异常: ${error.message}`);
149
+ console.error(error.stack);
150
+ await shutdown('uncaughtException');
151
+ });
152
+
153
+ process.on('unhandledRejection', async (reason, promise) => {
154
+ ui.printError(`未处理的 Promise 拒绝: ${reason}`);
155
+ console.error('Promise:', promise);
156
+ await shutdown('unhandledRejection');
157
+ });
158
+
159
+ return shutdown;
160
+ }
161
+
162
+ async function askInput(prompt, allowEmpty = false) {
163
+ const rl = readline.createInterface({ input, output });
164
+ let answer = '';
165
+
166
+ while (!allowEmpty && !answer.trim()) {
167
+ answer = await rl.question(prompt);
168
+ }
169
+
170
+ if (allowEmpty && !answer) {
171
+ answer = await rl.question(prompt);
172
+ }
173
+
174
+ rl.close();
175
+ return answer.trim();
176
+ }
177
+
178
+ async function showStartupMenu() {
179
+ let selectedIndex = 0;
180
+
181
+ while (true) {
182
+ const choices = [];
183
+ if (hasActiveSession) {
184
+ choices.push({
185
+ label: '继续聊天 回到当前会话',
186
+ value: 'c'
187
+ });
188
+ }
189
+ choices.push(
190
+ { label: '新建聊天 开始全新对话', value: '1' },
191
+ { label: '历史会话 查找并恢复之前的对话', value: '2' },
192
+ { label: `系统设置 默认提供方: ${getProvider().name}`, value: 's' },
193
+ { label: '文档 查看简要使用说明', value: 'd' },
194
+ { label: '退出', value: 'q' }
195
+ );
196
+
197
+ selectedIndex = Math.min(Math.max(selectedIndex, 0), choices.length - 1);
198
+
199
+ ui.clear();
200
+ ui.printTitle('XShat - AI 聊天工具');
201
+ ui.printNewline();
202
+ ui.printMuted(`默认提供方: ${getProvider().name} · 默认模式: ${config.agent?.enabled ? 'Agent' : 'Chat'} · 浏览器: ${config.browser?.headless ? 'Headless' : 'Visible'}`);
203
+ if (hasActiveSession && sessionManager.current?.title) {
204
+ ui.printMuted(`当前会话: ${sessionManager.current.title}`);
205
+ }
206
+ ui.printInputHints([
207
+ 'W/S 或 ↑↓ 选择',
208
+ 'Enter 确认',
209
+ 'D 文档',
210
+ 'S 设置',
211
+ 'Q/Esc 退出'
212
+ ]);
213
+ ui.printNewline();
214
+ ui.printSelectableList('主菜单', choices.map((item) => item.label), selectedIndex);
215
+ ui.printNewline();
216
+
217
+ const key = await waitForKeypress();
218
+
219
+ if (key === 'up' || key === 'w') {
220
+ selectedIndex = Math.max(0, selectedIndex - 1);
221
+ continue;
222
+ }
223
+
224
+ if (key === 'down' || key === 's') {
225
+ selectedIndex = Math.min(choices.length - 1, selectedIndex + 1);
226
+ continue;
227
+ }
228
+
229
+ if (key === 'enter') {
230
+ return choices[selectedIndex].value;
231
+ }
232
+
233
+ if (key === 'd') {
234
+ return 'd';
235
+ }
236
+
237
+ if (key === 'q' || key === 'escape') {
238
+ return 'q';
239
+ }
240
+ }
241
+ }
242
+
243
+ function showQuickDoc() {
244
+ ui.clearPinnedHeader();
245
+ ui.exitWorkspace();
246
+ ui.clear();
247
+ ui.printTitle('XShat 文档');
248
+ ui.printNewline();
249
+ ui.printMarkdown(buildQuickStartDoc());
250
+ ui.printNewline();
251
+ }
252
+
253
+ async function resumeSession(sessionMeta) {
254
+ ui.clearPinnedHeader();
255
+ ui.exitWorkspace();
256
+ const session = await sessionManager.resume(sessionMeta.id);
257
+ if (!session || !session.url) {
258
+ ui.printError('无法恢复该会话(缺少 URL)');
259
+ await new Promise(resolve => setTimeout(resolve, 2000));
260
+ return false;
261
+ }
262
+
263
+ const provider = getProvider(session.provider);
264
+
265
+ ui.clear();
266
+ ui.printTitle(`恢复会话: ${session.title}`);
267
+ ui.printNewline();
268
+ ui.printInfo(`AI 提供方: ${provider.name}`);
269
+ ui.printInfo(`消息数量: ${session.messages.length} 条`);
270
+ ui.printNewline();
271
+ if (requiresBrowserForProvider(provider)) {
272
+ ui.printWarning('正在加载会话...');
273
+
274
+ const opened = await navigateWithRecovery(session.url, {
275
+ title: '恢复会话失败'
276
+ });
277
+ if (!opened) {
278
+ ui.printMuted('已取消恢复会话');
279
+ await new Promise((resolve) => setTimeout(resolve, 800));
280
+ return false;
281
+ }
282
+ await new Promise(resolve => setTimeout(resolve, 2000));
283
+ }
284
+
285
+ rebuildChatManager(provider);
286
+
287
+ if (session.messages.length > 0) {
288
+ ui.printWarning('正在恢复上下文...');
289
+ const primer = buildHistoryPrimer(
290
+ session.messages,
291
+ getSessionCompressionOptions()
292
+ );
293
+
294
+ const success = await chatManager.sendMessage(primer);
295
+ if (success) {
296
+ await new Promise(resolve => setTimeout(resolve, 800));
297
+ try {
298
+ await chatManager.waitForResponse(() => {});
299
+ } catch {
300
+ // 静默消费回复失败也继续
301
+ }
302
+ }
303
+ }
304
+
305
+ ui.clear();
306
+ ui.printTitle(`${session.title}`);
307
+ ui.printRenderedHistory(session.messages, {
308
+ renderMessage: (message) => {
309
+ if (message.role !== 'assistant') {
310
+ return null;
311
+ }
312
+
313
+ const envelope = normalizeResponseEnvelope(message.content, {
314
+ agentMode: false
315
+ });
316
+ const presentation = getHistoryEnvelopePresentation(envelope);
317
+
318
+ return {
319
+ title: presentation.title,
320
+ badges: presentation.badges,
321
+ text:
322
+ envelope.displayText ||
323
+ envelope.consoleText ||
324
+ envelope.text ||
325
+ (envelope.command?.name ? `动作: ${envelope.command.name}` : ''),
326
+ renderAs: envelope.renderAs
327
+ };
328
+ }
329
+ });
330
+ ui.printNewline();
331
+ ui.printSuccess('会话已恢复,可以继续对话');
332
+ printChatOnboarding();
333
+ ui.printNewline();
334
+ chatChromeDirty = true;
335
+
336
+ return true;
337
+ }
338
+
339
+ async function startNewChat(provider) {
340
+ ui.clearPinnedHeader();
341
+ ui.exitWorkspace();
342
+ sessionManager.create(provider);
343
+
344
+ ui.clear();
345
+ ui.printTitle(`新建会话 - ${provider.name}`);
346
+ ui.printNewline();
347
+ if (requiresBrowserForProvider(provider)) {
348
+ ui.printWarning('正在初始化浏览器...');
349
+
350
+ const opened = await navigateWithRecovery(provider.url, {
351
+ title: `连接 ${provider.name} 失败`
352
+ });
353
+ if (!opened) {
354
+ ui.printMuted('已取消新建会话');
355
+ await new Promise((resolve) => setTimeout(resolve, 800));
356
+ return false;
357
+ }
358
+ await new Promise(resolve => setTimeout(resolve, 2000));
359
+ } else {
360
+ sessionManager.setUrl(`api://${provider.id}`);
361
+ }
362
+
363
+ rebuildChatManager(provider);
364
+
365
+ ui.clear();
366
+ ui.printSuccess(`${provider.name} 已准备好!`);
367
+ printChatOnboarding();
368
+ ui.printNewline();
369
+ chatChromeDirty = true;
370
+ return true;
371
+ }
372
+
373
+ async function askQuestion() {
374
+ if (chatChromeDirty) {
375
+ const hints = [
376
+ '/cmd 面板',
377
+ '/paste 多行',
378
+ '/menu 主菜单'
379
+ ];
380
+ const renderedPinned = await printRuntimeStatus({ pinned: true, hints });
381
+ if (!renderedPinned) {
382
+ ui.clearPinnedHeader();
383
+ await printRuntimeStatus();
384
+ ui.printInputHints(hints);
385
+ }
386
+ chatChromeDirty = false;
387
+ }
388
+
389
+ const rl = readline.createInterface({ input, output });
390
+ let question = '';
391
+
392
+ while (!question.trim()) {
393
+ question = await rl.question(ui.printPrompt());
394
+ }
395
+
396
+ rl.close();
397
+ return question.trim();
398
+ }
399
+
400
+ async function askMultiline(promptText = '请输入多行内容,输入 /end 结束:') {
401
+ const rl = readline.createInterface({ input, output });
402
+ const lines = [];
403
+
404
+ ui.printNewline();
405
+ ui.printInfo(promptText);
406
+ ui.printInfo('粘贴内容后请单独输入 /end 结束,输入 /cancel 取消。');
407
+ ui.printPromptLine();
408
+
409
+ while (true) {
410
+ const line = await rl.question('');
411
+ if (line.trim() === '/end') {
412
+ break;
413
+ }
414
+ if (line.trim() === '/cancel') {
415
+ rl.close();
416
+ return '';
417
+ }
418
+ lines.push(line);
419
+ }
420
+
421
+ rl.close();
422
+ return lines.join('\n').trim();
423
+ }
424
+
425
+ function getTaskModeLabel(mode = 'once') {
426
+ return {
427
+ once: '单次执行',
428
+ loop: '持续循环',
429
+ watch: '轮询监控',
430
+ interval: '定时间隔'
431
+ }[mode] || mode;
432
+ }
433
+
434
+ function buildTaskTitleFromGoal(goal = '') {
435
+ const firstLine = String(goal ?? '')
436
+ .split('\n')
437
+ .map((line) => line.trim())
438
+ .find(Boolean);
439
+ return (firstLine || '未命名任务').slice(0, 60);
440
+ }
441
+
442
+ async function collectTaskGoal() {
443
+ const initial = (await promptInput({
444
+ message: '任务目标(直接回车可切换到多行模式)'
445
+ })).trim();
446
+
447
+ if (initial) {
448
+ const useMultiline = await confirm({
449
+ message: '要继续补充为多行任务描述吗?',
450
+ default: false
451
+ });
452
+ if (!useMultiline) {
453
+ return initial;
454
+ }
455
+ }
456
+
457
+ return askMultiline('输入任务目标,单独输入 /end 结束');
458
+ }
459
+
460
+ async function runTaskCreationWizard(runner) {
461
+ ui.clear();
462
+ ui.printTitle('新建任务');
463
+ ui.printNewline();
464
+ ui.printInfo('先输入任务目标;短任务可直接一行创建,复杂任务可切到多行。');
465
+ ui.printNewline();
466
+
467
+ const goal = await collectTaskGoal();
468
+ if (!goal) {
469
+ ui.printMuted('已取消');
470
+ await new Promise((resolve) => setTimeout(resolve, 500));
471
+ return null;
472
+ }
473
+
474
+ const mode = await select({
475
+ message: '选择任务模式',
476
+ choices: [
477
+ { name: '单次执行 创建后按需手动启动', value: 'once' },
478
+ { name: '持续循环 适合长期反复执行', value: 'loop' },
479
+ { name: '轮询监控 固定频率检查变化', value: 'watch' },
480
+ { name: '定时间隔 每隔一段时间运行', value: 'interval' }
481
+ ],
482
+ default: 'once'
483
+ });
484
+
485
+ let intervalMs = null;
486
+ if (mode === 'interval') {
487
+ const intervalAnswer = (await promptInput({
488
+ message: '输入间隔毫秒数',
489
+ default: '30000'
490
+ })).trim();
491
+ intervalMs = Math.max(1000, Number(intervalAnswer) || 30000);
492
+ } else if (mode === 'watch') {
493
+ const intervalAnswer = (await promptInput({
494
+ message: '输入监控间隔毫秒数',
495
+ default: '10000'
496
+ })).trim();
497
+ intervalMs = Math.max(1000, Number(intervalAnswer) || 10000);
498
+ }
499
+
500
+ const retryAnswer = (await promptInput({
501
+ message: '失败自动重试次数',
502
+ default: '1'
503
+ })).trim();
504
+ const retryLimit = retryAnswer ? Math.max(0, Number(retryAnswer) || 0) : 1;
505
+ const runNow = await confirm({
506
+ message: '创建后立即执行吗?',
507
+ default: mode === 'once'
508
+ });
509
+
510
+ const summaryRows = [
511
+ { label: '标题', value: buildTaskTitleFromGoal(goal) },
512
+ { label: '模式', value: getTaskModeLabel(mode) },
513
+ intervalMs ? { label: '间隔', value: `${intervalMs} ms` } : null,
514
+ { label: '重试', value: `${retryLimit} 次` },
515
+ { label: '提供方', value: getProvider().name },
516
+ { label: '立即执行', value: runNow ? '是' : '否' },
517
+ { label: '目标', value: goal }
518
+ ].filter(Boolean);
519
+
520
+ ui.clear();
521
+ ui.printTitle('确认新任务');
522
+ ui.printNewline();
523
+ ui.printKeyValueTable('任务配置', summaryRows);
524
+ ui.printNewline();
525
+
526
+ const approved = await confirm({
527
+ message: '确认创建这个任务吗?',
528
+ default: true
529
+ });
530
+ if (!approved) {
531
+ ui.printMuted('已取消');
532
+ await new Promise((resolve) => setTimeout(resolve, 500));
533
+ return null;
534
+ }
535
+
536
+ const task = await runner.createTask({
537
+ title: buildTaskTitleFromGoal(goal),
538
+ goal,
539
+ provider: getProvider().id,
540
+ mode,
541
+ intervalMs,
542
+ retryLimit,
543
+ retryDelayMs: intervalMs
544
+ });
545
+
546
+ if (runNow) {
547
+ await runner.queueTask(task.id);
548
+ runner.runTask(task.id).catch(() => {});
549
+ }
550
+
551
+ ui.printSuccess(`任务已创建: ${task.title}`);
552
+ ui.printMuted(
553
+ `${getProvider().name} · ${getTaskModeLabel(mode)}${runNow ? ' · 已开始执行' : ''}`
554
+ );
555
+ await new Promise((resolve) => setTimeout(resolve, 700));
556
+ return {
557
+ task,
558
+ runNow
559
+ };
560
+ }
561
+
562
+ function rebuildChatManager(provider) {
563
+ chatManager = createProviderChatManager(provider, browserManager);
564
+ if (typeof browserManager.setChatManager === 'function') {
565
+ browserManager.setChatManager(chatManager);
566
+ }
567
+ if (!conversationAgent) {
568
+ conversationAgent = new ConversationAgent({
569
+ browserManager,
570
+ ui,
571
+ prompt: (prompt) => askInput(prompt, false)
572
+ });
573
+ }
574
+ if (!agentExecutor) {
575
+ agentExecutor = new AgentExecutor({
576
+ browserManager,
577
+ ui,
578
+ prompt: (prompt) => askInput(prompt, false),
579
+ sendMessage: (content) => sendAgentCommandMessage(content),
580
+ getAppState: () => ({
581
+ provider: chatManager?.provider?.id || null,
582
+ sessionId: sessionManager.current?.id || null,
583
+ sessionTitle: sessionManager.current?.title || null,
584
+ messageCount: sessionManager.current?.messages?.length || 0,
585
+ chatUrl: browserManager.getCurrentUrl(),
586
+ agentUrl: browserManager.getCurrentUrlFor?.('agent') || null,
587
+ browser: {
588
+ chatReady: browserManager.isChannelReady?.('chat') || false,
589
+ chatState: browserManager.getChannelState?.('chat') || null,
590
+ agentReady: browserManager.isChannelReady?.('agent') || false,
591
+ agentState: browserManager.getChannelState?.('agent') || null,
592
+ shareAuthState: Boolean(config.browser?.shareAuthState)
593
+ },
594
+ agent: {
595
+ ...buildAgentRuntimeState()
596
+ },
597
+ workspaceRoot: resolve(process.cwd())
598
+ }),
599
+ workspaceRoot: resolve(process.cwd()),
600
+ allowShell: Boolean(config.agent?.allowShell),
601
+ allowFileWrite: Boolean(config.agent?.allowFileWrite),
602
+ autoExecuteCommands: Boolean(config.agent?.autoExecuteCommands)
603
+ });
604
+ }
605
+ }
606
+
607
+ function ensureTaskRunner() {
608
+ if (taskRunner) {
609
+ return taskRunner;
610
+ }
611
+
612
+ taskRunner = new TaskRunner({
613
+ taskStore,
614
+ browserManager,
615
+ logger,
616
+ getProvider,
617
+ createChatManager: (provider) => createProviderChatManager(provider, browserManager),
618
+ createAgentExecutor: () =>
619
+ new AgentExecutor({
620
+ browserManager,
621
+ ui,
622
+ prompt: (prompt) => askInput(prompt, false),
623
+ sendMessage: (content) => sendAgentCommandMessage(content),
624
+ getAppState: () => ({
625
+ provider: chatManager?.provider?.id || null,
626
+ sessionId: sessionManager.current?.id || null,
627
+ sessionTitle: sessionManager.current?.title || null,
628
+ messageCount: sessionManager.current?.messages?.length || 0,
629
+ chatUrl: browserManager.getCurrentUrl(),
630
+ agentUrl: browserManager.getCurrentUrlFor?.('agent') || null,
631
+ browser: {
632
+ chatReady: browserManager.isChannelReady?.('chat') || false,
633
+ chatState: browserManager.getChannelState?.('chat') || null,
634
+ agentReady: browserManager.isChannelReady?.('agent') || false,
635
+ agentState: browserManager.getChannelState?.('agent') || null,
636
+ shareAuthState: Boolean(config.browser?.shareAuthState)
637
+ },
638
+ agent: {
639
+ ...buildAgentRuntimeState({
640
+ forceEnabled: true,
641
+ forceAutoExecute: true
642
+ })
643
+ },
644
+ workspaceRoot: resolve(process.cwd())
645
+ }),
646
+ workspaceRoot: resolve(process.cwd()),
647
+ allowShell: Boolean(config.agent?.allowShell),
648
+ allowFileWrite: Boolean(config.agent?.allowFileWrite),
649
+ autoExecuteCommands: true
650
+ }),
651
+ sessionCompressionOptions: getSessionCompressionOptions
652
+ });
653
+
654
+ return taskRunner;
655
+ }
656
+
657
+ function resetAgentPrimer() {
658
+ agentPrimedSessionId = null;
659
+ }
660
+
661
+ function printAgentHelp() {
662
+ ui.printNewline();
663
+ ui.printInfo('Agent 模式已开启:');
664
+ ui.printInfo('- 会要求网页模型尽量只返回 JSON');
665
+ ui.printInfo('- 能解析 reply/action/handoff/error 四种结构化结果');
666
+ if (isMultiAgentEnabled()) {
667
+ ui.printInfo('- 已启用多 Agent 协作,主控会按网页 / Shell / 文件 / 人工接管分工');
668
+ }
669
+ ui.printInfo('- 解析失败时会自动退回普通文本');
670
+ ui.printInfo('- 输入 /paste 可粘贴多行内容');
671
+ ui.printInfo('- 输入 /help 可查看聊天内命令');
672
+ ui.printNewline();
673
+ }
674
+
675
+ function printChatOnboarding() {
676
+ const provider = chatManager?.provider?.name || getProvider().name;
677
+ const mode = agentMode ? 'Agent' : 'Chat';
678
+ ui.printMuted(`当前: ${provider} · ${mode}`);
679
+ ui.printMuted('快捷命令: /cmd /paste /memory /menu /config');
680
+ chatChromeDirty = true;
681
+ }
682
+
683
+ function getWorkspaceSidebarSections() {
684
+ const memory = sessionManager.current?.memory || {};
685
+ const pinned = Array.isArray(memory.pinned) ? memory.pinned.slice(0, 5) : [];
686
+ const plan = Array.isArray(memory.plan) ? memory.plan.slice(0, 5) : [];
687
+
688
+ return {
689
+ leftSections: [
690
+ {
691
+ title: 'Session',
692
+ rows: [
693
+ { label: 'Provider', value: chatManager?.provider?.name || getProvider().name },
694
+ { label: 'Mode', value: agentMode ? 'Agent' : 'Chat' },
695
+ { label: 'Title', value: sessionManager.current?.title || '(无标题)' },
696
+ { label: 'Msgs', value: String(sessionManager.current?.messages?.length || 0) }
697
+ ]
698
+ },
699
+ {
700
+ title: 'Quick',
701
+ lines: [
702
+ '/cmd 命令面板',
703
+ '/memory 会话记忆',
704
+ '/retry-last 重试上一条',
705
+ '/switch-provider 切换提供方',
706
+ '/menu 返回主菜单'
707
+ ]
708
+ }
709
+ ],
710
+ rightSections: [
711
+ {
712
+ title: 'Memory',
713
+ rows: [
714
+ { label: 'Summary', value: memory.summary || '(空)' },
715
+ { label: 'Last User', value: memory.lastUserMessage || '(空)' },
716
+ { label: 'Last AI', value: memory.lastAssistantMessage || '(空)' }
717
+ ]
718
+ },
719
+ ...(pinned.length
720
+ ? [{
721
+ title: 'Pinned',
722
+ lines: pinned.map((item, index) => `${index + 1}. ${item?.text || ''}`)
723
+ }]
724
+ : []),
725
+ ...(plan.length
726
+ ? [{
727
+ title: 'Plan',
728
+ lines: plan.map((item, index) => `${index + 1}. ${item}`)
729
+ }]
730
+ : []),
731
+ ...(lastAgentOrReplySummary
732
+ ? [{
733
+ title: 'Last Action',
734
+ lines: [lastAgentOrReplySummary]
735
+ }]
736
+ : [])
737
+ ]
738
+ };
739
+ }
740
+
741
+ async function showRuntimeOverview() {
742
+ ui.clearPinnedHeader();
743
+ ui.exitWorkspace();
744
+ const taskSnapshot = await ensureTaskRunner().getRuntimeSnapshot();
745
+ const appState = {
746
+ provider: chatManager?.provider?.name || getProvider().name,
747
+ providerId: chatManager?.provider?.id || getProvider().id,
748
+ sessionTitle: sessionManager.current?.title || '(无会话)',
749
+ sessionMessages: sessionManager.current?.messages?.length || 0,
750
+ browserMode: config.browser?.headless ? 'Headless' : 'Visible',
751
+ visibleAllowed: config.browser?.allowVisibleBrowser !== false,
752
+ chatUrl: browserManager.getCurrentUrl() || '(无)',
753
+ agentUrl: browserManager.getCurrentUrlFor?.('agent') || '(无)',
754
+ channel:
755
+ browserManager.isChannelReady?.('agent')
756
+ ? 'agent'
757
+ : browserManager.isChannelReady?.('chat')
758
+ ? 'chat'
759
+ : '-',
760
+ agentMode: agentMode ? '开启' : '关闭',
761
+ autoExecute: config.agent?.autoExecuteCommands ? '开启' : '关闭',
762
+ multiAgent: config.agent?.multiAgent ? '开启' : '关闭'
763
+ };
764
+
765
+ const inspect = chatManager?.inspectSite
766
+ ? await chatManager.inspectSite().catch(() => null)
767
+ : null;
768
+
769
+ ui.clear();
770
+ ui.printTitle('运行状态');
771
+ ui.printNewline();
772
+ ui.printKeyValueTable('当前上下文', [
773
+ { label: '提供方', value: `${appState.provider} (${appState.providerId})` },
774
+ { label: '会话', value: appState.sessionTitle },
775
+ { label: '消息数', value: String(appState.sessionMessages) },
776
+ { label: '浏览器', value: `${appState.browserMode} / Channel ${appState.channel}` },
777
+ { label: '允许可见', value: appState.visibleAllowed ? '开启' : '关闭' },
778
+ { label: 'Agent 模式', value: appState.agentMode },
779
+ { label: '自动执行', value: appState.autoExecute },
780
+ { label: '多 Agent', value: appState.multiAgent },
781
+ { label: '聊天页', value: appState.chatUrl },
782
+ { label: 'Agent 页', value: appState.agentUrl }
783
+ ]);
784
+ ui.printNewline();
785
+ ui.printTaskRuntime(taskSnapshot);
786
+ if (sessionManager.current?.memory) {
787
+ const pinnedCount = Array.isArray(sessionManager.current.memory.pinned)
788
+ ? sessionManager.current.memory.pinned.length
789
+ : 0;
790
+ ui.printNewline();
791
+ ui.printKeyValueTable('会话记忆', [
792
+ { label: 'Pinned', value: String(pinnedCount) },
793
+ { label: 'Last User', value: sessionManager.current.memory.lastUserMessage || '(空)' },
794
+ { label: 'Last AI', value: sessionManager.current.memory.lastAssistantMessage || '(空)' }
795
+ ]);
796
+ }
797
+
798
+ if (inspect?.intervention?.blocked) {
799
+ ui.printNewline();
800
+ ui.printActionPanel('当前阻塞', [
801
+ { label: '类型', value: inspect.intervention.label || inspect.intervention.kind || '人工介入' },
802
+ { label: '原因', value: inspect.intervention.reason || '(未知)' },
803
+ { label: '页面', value: inspect.intervention.url || '(未知)' },
804
+ {
805
+ label: '建议',
806
+ value: config.browser?.allowVisibleBrowser === false
807
+ ? '当前禁止可见浏览器;可在 /config 切到 API provider,或开启允许可见浏览器。'
808
+ : '可输入 /agent 进入人工接管,完成登录/验证后继续。'
809
+ }
810
+ ]);
811
+ }
812
+
813
+ const suggestions = [];
814
+
815
+ if (inspect?.intervention?.blocked && config.browser?.allowVisibleBrowser === false) {
816
+ suggestions.push({ label: '/config', hint: '开启允许可见浏览器或调整策略' });
817
+ suggestions.push({ label: '/config', hint: '切换默认提供方或启用 API 兜底' });
818
+ }
819
+
820
+ if (taskSnapshot.handoff > 0) {
821
+ suggestions.push({ label: '/task', hint: '查看卡在 handoff 的任务' });
822
+ }
823
+
824
+ if (!agentMode) {
825
+ suggestions.push({ label: '/mode', hint: '切换到 Agent 模式' });
826
+ }
827
+
828
+ if (!suggestions.length) {
829
+ suggestions.push({ label: '/inspect', hint: '检查当前页面适配状态' });
830
+ suggestions.push({ label: '/sessions', hint: '查看历史会话' });
831
+ }
832
+
833
+ ui.printNewline();
834
+ ui.printSuggestedActions(suggestions);
835
+ ui.printNewline();
836
+ ui.printInfo('按回车返回聊天...');
837
+ await askInput('', true);
838
+ }
839
+
840
+ function createTimelineEvent(label, detail = '', options = {}) {
841
+ const roleLabel = options.agentRole ? getAgentRoleLabel(options.agentRole) : '';
842
+ return {
843
+ time: new Date().toLocaleTimeString('zh-CN', { hour12: false }),
844
+ label,
845
+ detail,
846
+ agentRole: options.agentRole || null,
847
+ agentLabel: roleLabel
848
+ };
849
+ }
850
+
851
+ async function showCommandPalette() {
852
+ ui.clearPinnedHeader();
853
+ ui.exitWorkspace();
854
+ const choice = await select({
855
+ message: '命令面板',
856
+ choices: [
857
+ { name: '/help 查看聊天命令', value: '/help' },
858
+ { name: '/status 查看当前运行状态', value: '/status' },
859
+ { name: '/task 打开任务面板', value: '/task' },
860
+ { name: '/sessions 打开历史会话', value: '/sessions' },
861
+ { name: '/resume 恢复最近会话', value: '/resume' },
862
+ { name: '/memory 查看会话记忆', value: '/memory' },
863
+ { name: '/pin 固定一条会话记忆', value: '/pin' },
864
+ { name: '/retry-last 重试上一条消息', value: '/retry-last' },
865
+ { name: '/plan 查看当前轻量计划', value: '/plan' },
866
+ { name: '/switch-provider 切换当前会话提供方', value: '/switch-provider' },
867
+ { name: '/mode 切换 Agent 模式', value: '/mode' },
868
+ { name: '/inspect 检查页面适配', value: '/inspect' },
869
+ { name: '/config 系统设置 / 默认提供方', value: '/config' },
870
+ { name: '/rename-session 修改当前会话标题', value: '/rename-session' },
871
+ { name: '/delete-session 删除当前会话', value: '/delete-session' },
872
+ { name: '/doc 查看文档', value: '/doc' },
873
+ { name: '/menu 返回主菜单', value: '/menu' },
874
+ { name: '取消', value: '__cancel__' }
875
+ ]
876
+ });
877
+
878
+ return choice;
879
+ }
880
+
881
+ async function switchCurrentSessionProvider() {
882
+ const providers = listProviders();
883
+ const currentProviderId = chatManager?.provider?.id || getProvider().id;
884
+ const choice = await select({
885
+ message: '切换当前会话提供方',
886
+ choices: [
887
+ ...providers.map((provider) => ({
888
+ name: `${provider.name}${provider.id === currentProviderId ? '(当前)' : ''}`,
889
+ value: provider.id
890
+ })),
891
+ { name: '取消', value: '__cancel__' }
892
+ ],
893
+ default: currentProviderId
894
+ });
895
+
896
+ if (choice === '__cancel__' || choice === currentProviderId) {
897
+ if (choice === currentProviderId) {
898
+ ui.printInfo('当前会话已在使用该提供方。');
899
+ await new Promise((resolve) => setTimeout(resolve, 700));
900
+ }
901
+ return false;
902
+ }
903
+
904
+ const targetProvider = getProvider(choice);
905
+ return switchProviderWithContext(targetProvider, {
906
+ reason: '正在切换当前会话 provider'
907
+ });
908
+ }
909
+
910
+ async function openModelSwitcher() {
911
+ const choice = await select({
912
+ message: '模型 / 提供方',
913
+ choices: [
914
+ { name: '切换当前会话提供方', value: 'session' },
915
+ { name: '打开设置,修改默认提供方', value: 'settings' },
916
+ { name: '取消', value: '__cancel__' }
917
+ ],
918
+ default: 'session'
919
+ });
920
+
921
+ if (choice === '__cancel__') {
922
+ return;
923
+ }
924
+
925
+ if (choice === 'session') {
926
+ await switchCurrentSessionProvider();
927
+ return;
928
+ }
929
+
930
+ await showSettingsMenu({ ui, config });
931
+ }
932
+
933
+ function getCurrentMemoryRows() {
934
+ const memory = sessionManager.current?.memory || {};
935
+ const rows = [];
936
+
937
+ if (memory.summary) {
938
+ rows.push({ label: 'Summary', value: memory.summary });
939
+ }
940
+
941
+ if (Array.isArray(memory.pinned) && memory.pinned.length > 0) {
942
+ memory.pinned.slice(0, 6).forEach((item, index) => {
943
+ rows.push({
944
+ label: `Pin ${index + 1}`,
945
+ value: item?.text || ''
946
+ });
947
+ });
948
+ }
949
+
950
+ if (Array.isArray(memory.plan) && memory.plan.length > 0) {
951
+ rows.push({
952
+ label: 'Plan',
953
+ value: memory.plan.join(' -> ')
954
+ });
955
+ }
956
+
957
+ rows.push({
958
+ label: 'Last User',
959
+ value: memory.lastUserMessage || '(空)'
960
+ });
961
+ rows.push({
962
+ label: 'Last AI',
963
+ value: memory.lastAssistantMessage || '(空)'
964
+ });
965
+
966
+ return rows;
967
+ }
968
+
969
+ async function showMemoryPanel() {
970
+ ui.clearPinnedHeader();
971
+ ui.exitWorkspace();
972
+ while (true) {
973
+ const memory = sessionManager.current?.memory || {};
974
+ const pinned = Array.isArray(memory.pinned) ? memory.pinned : [];
975
+ const plan = Array.isArray(memory.plan) ? memory.plan : [];
976
+
977
+ ui.clear();
978
+ ui.printTitle('会话记忆');
979
+ ui.printNewline();
980
+ ui.printKeyValueTable('概览', [
981
+ { label: 'Summary', value: memory.summary || '(空)' },
982
+ { label: 'Pins', value: String(pinned.length) },
983
+ { label: 'Plan', value: plan.length > 0 ? `${plan.length} 条` : '(空)' },
984
+ { label: 'Last User', value: memory.lastUserMessage || '(空)' },
985
+ { label: 'Last AI', value: memory.lastAssistantMessage || '(空)' }
986
+ ]);
987
+ ui.printNewline();
988
+ ui.printInputHints([
989
+ 'P 查看固定记忆',
990
+ 'D 查看完整详情',
991
+ 'Q/Esc 返回'
992
+ ]);
993
+
994
+ const key = await waitForKeypress();
995
+
996
+ if (key === 'p') {
997
+ ui.clear();
998
+ ui.printTitle('固定记忆');
999
+ ui.printNewline();
1000
+ if (!pinned.length) {
1001
+ ui.printInfo('(暂无固定记忆)');
1002
+ } else {
1003
+ ui.printKeyValueTable('Pins', pinned.slice(0, 12).map((item, index) => ({
1004
+ label: `Pin ${index + 1}`,
1005
+ value: item?.text || '(空)'
1006
+ })));
1007
+ }
1008
+ ui.printNewline();
1009
+ ui.printInfo('按任意键返回...');
1010
+ await waitForKeypress();
1011
+ continue;
1012
+ }
1013
+
1014
+ if (key === 'd') {
1015
+ ui.clear();
1016
+ ui.printTitle('会话记忆详情');
1017
+ ui.printNewline();
1018
+ ui.printKeyValueTable('Memory', getCurrentMemoryRows());
1019
+ ui.printNewline();
1020
+ ui.printInfo('按任意键返回...');
1021
+ await waitForKeypress();
1022
+ continue;
1023
+ }
1024
+
1025
+ if (key === 'q' || key === 'escape' || key === 'enter') {
1026
+ return;
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+ async function pinCurrentMemory() {
1032
+ const note = await askInput('输入要固定的记忆: ', false);
1033
+ if (!note.trim()) {
1034
+ ui.printMuted('已取消');
1035
+ return;
1036
+ }
1037
+
1038
+ await sessionManager.pinMemory(note);
1039
+ ui.printSuccess('已固定到会话记忆');
1040
+ }
1041
+
1042
+ function printRuntimeStatus({ pinned = false, hints = [] } = {}) {
1043
+ const taskCountPromise = taskStore.list();
1044
+ return taskCountPromise.then((tasks) => {
1045
+ const channel =
1046
+ browserManager.isChannelReady?.('agent')
1047
+ ? 'agent'
1048
+ : browserManager.isChannelReady?.('chat')
1049
+ ? 'chat'
1050
+ : '-';
1051
+ const state = {
1052
+ provider: chatManager?.provider?.name || getProvider().name,
1053
+ mode: agentMode ? 'Agent' : 'Chat',
1054
+ browser: config.browser?.headless ? 'Headless' : 'Visible',
1055
+ channel,
1056
+ tasks: tasks.filter((task) => ['queued', 'running', 'stopping'].includes(task.status)).length,
1057
+ session: sessionManager.current?.title
1058
+ ? sessionManager.current.title.slice(0, 18)
1059
+ : '-'
1060
+ };
1061
+
1062
+ if (pinned) {
1063
+ return ui.renderChatChrome(state, hints);
1064
+ }
1065
+
1066
+ ui.printStatusBar(state);
1067
+ return false;
1068
+ });
1069
+ }
1070
+
1071
+ function getProviderRetryConfig() {
1072
+ return {
1073
+ sendRetryCount: Math.max(0, Number(config.providersRuntime?.sendRetryCount) || 0),
1074
+ responseRetryCount: Math.max(0, Number(config.providersRuntime?.responseRetryCount) || 0),
1075
+ retryBackoffMs: Math.max(0, Number(config.providersRuntime?.retryBackoffMs) || 0)
1076
+ };
1077
+ }
1078
+
1079
+ async function waitBeforeProviderRetry(attempt, label = 'provider') {
1080
+ const { retryBackoffMs } = getProviderRetryConfig();
1081
+ const delayMs = retryBackoffMs * Math.max(1, attempt);
1082
+ if (delayMs > 0) {
1083
+ ui.printMuted(`${label} 重试前等待 ${Math.round(delayMs / 100) / 10}s...`);
1084
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1085
+ }
1086
+ }
1087
+
1088
+ async function handleTaskHandoff(taskId, runner = ensureTaskRunner()) {
1089
+ const task = await taskStore.load(taskId);
1090
+ if (!task) {
1091
+ ui.printError('未找到待接管任务');
1092
+ await new Promise((resolve) => setTimeout(resolve, 700));
1093
+ return false;
1094
+ }
1095
+
1096
+ const provider = getProvider(task.provider);
1097
+ if (!provider) {
1098
+ ui.printError(`未找到任务提供方: ${task.provider || '(空)'}`);
1099
+ await new Promise((resolve) => setTimeout(resolve, 900));
1100
+ return false;
1101
+ }
1102
+
1103
+ if (!requiresBrowserForProvider(provider)) {
1104
+ ui.printInfo('该任务不依赖网页浏览器,可直接重试运行。');
1105
+ await runner.retryTask(taskId, { run: true });
1106
+ await new Promise((resolve) => setTimeout(resolve, 700));
1107
+ return true;
1108
+ }
1109
+
1110
+ if (config.browser?.allowVisibleBrowser === false) {
1111
+ ui.printError('当前配置禁止可见浏览器,无法进入人工接管。请先到 /config 开启“允许可见浏览器”。');
1112
+ await new Promise((resolve) => setTimeout(resolve, 1200));
1113
+ return false;
1114
+ }
1115
+
1116
+ const targetUrl = task.handoffUrl || provider.url;
1117
+ const shouldRestoreHeadless = config.browser?.headless !== false;
1118
+
1119
+ try {
1120
+ if (browserManager.isReady()) {
1121
+ await browserManager.relaunch({
1122
+ headless: false,
1123
+ url: targetUrl
1124
+ });
1125
+ } else {
1126
+ await browserManager.init({
1127
+ headless: false
1128
+ });
1129
+ if (targetUrl) {
1130
+ await browserManager.goto(targetUrl);
1131
+ }
1132
+ }
1133
+ } catch (error) {
1134
+ ui.printError(`打开接管页面失败: ${error.message}`);
1135
+ await new Promise((resolve) => setTimeout(resolve, 1200));
1136
+ return false;
1137
+ }
1138
+
1139
+ await taskStore.appendHistory(taskId, {
1140
+ type: 'status',
1141
+ label: '人工接管中',
1142
+ detail: targetUrl || provider.url || '已打开接管页面'
1143
+ });
1144
+
1145
+ ui.clear();
1146
+ ui.printTitle(`任务接管 · ${task.title}`);
1147
+ ui.printNewline();
1148
+ ui.printKeyValueTable('接管信息', [
1149
+ { label: '任务状态', value: task.status || 'handoff' },
1150
+ { label: '提供方', value: provider.name },
1151
+ { label: '接管页面', value: targetUrl || '(未知)' },
1152
+ { label: '当前说明', value: task.lastResult || task.lastError || '需要人工接管' }
1153
+ ]);
1154
+ ui.printNewline();
1155
+ ui.printInfo('已切到可见浏览器,请在页面中完成登录、验证码、授权或人工确认。');
1156
+ ui.printInfo('完成后输入 done 继续,输入 cancel 返回任务面板。');
1157
+
1158
+ while (true) {
1159
+ const answer = await askInput('接管完成后输入 done/cancel: ', false);
1160
+ const normalized = String(answer || '').trim().toLowerCase();
1161
+
1162
+ if (normalized === 'cancel' || normalized === 'c' || normalized === 'q') {
1163
+ ui.printMuted('已取消本次人工接管');
1164
+ await taskStore.appendHistory(taskId, {
1165
+ type: 'status',
1166
+ label: '取消接管',
1167
+ detail: '用户取消人工接管'
1168
+ });
1169
+ await new Promise((resolve) => setTimeout(resolve, 700));
1170
+ return false;
1171
+ }
1172
+
1173
+ if (!['done', 'ok', 'yes', 'y', '好了', '完成'].includes(normalized)) {
1174
+ ui.printMuted('请输入 done 或 cancel。');
1175
+ continue;
1176
+ }
1177
+
1178
+ const resumeUrl = browserManager.getCurrentUrl() || targetUrl;
1179
+
1180
+ if (shouldRestoreHeadless) {
1181
+ try {
1182
+ await browserManager.relaunch({
1183
+ headless: true,
1184
+ url: resumeUrl
1185
+ });
1186
+ ui.printSuccess('已恢复默认浏览器模式');
1187
+ } catch (error) {
1188
+ ui.printWarning(`恢复默认浏览器模式失败,将继续保留可见浏览器: ${error.message}`);
1189
+ }
1190
+ }
1191
+
1192
+ await taskStore.appendHistory(taskId, {
1193
+ type: 'status',
1194
+ label: '接管完成',
1195
+ detail: '人工接管结束,准备恢复任务执行'
1196
+ });
1197
+ await runner.retryTask(taskId, { run: true });
1198
+ ui.printSuccess('任务已恢复执行');
1199
+ await new Promise((resolve) => setTimeout(resolve, 800));
1200
+ return true;
1201
+ }
1202
+ }
1203
+
1204
+ function printAgentActionPanel(parsed, step, maxSteps) {
1205
+ ui.printActionPanel('Agent 行动面板', [
1206
+ { label: '目标', value: sessionManager.current?.title || '当前任务' },
1207
+ { label: '步骤', value: `${step}/${maxSteps}` },
1208
+ {
1209
+ label: '协作',
1210
+ value: isMultiAgentEnabled() ? '多 Agent' : '单 Agent'
1211
+ },
1212
+ {
1213
+ label: '角色',
1214
+ value: parsed.agentLabel || resolveAgentLabel(parsed.command, parsed.agent)
1215
+ },
1216
+ { label: '动作', value: parsed.command?.name || '(无)' },
1217
+ { label: '说明', value: parsed.displayText || parsed.text || '(无)' },
1218
+ {
1219
+ label: '权限',
1220
+ value: `Shell ${config.agent?.allowShell ? '开' : '关'} / 文件 ${config.agent?.allowFileWrite ? '开' : '关'} / 自动执行 ${config.agent?.autoExecuteCommands ? '开' : '关'}`
1221
+ }
1222
+ ]);
1223
+ }
1224
+
1225
+ function printAgentActionSummary(envelope, result = null) {
1226
+ if (!envelope?.command && !result) {
1227
+ return;
1228
+ }
1229
+
1230
+ const rows = [];
1231
+ if (envelope?.command?.name) {
1232
+ rows.push({ label: '动作', value: envelope.command.name });
1233
+ }
1234
+ if (envelope?.agentLabel) {
1235
+ rows.push({ label: '角色', value: envelope.agentLabel });
1236
+ }
1237
+ if (envelope?.displayText || envelope?.text) {
1238
+ rows.push({ label: '说明', value: envelope.displayText || envelope.text });
1239
+ }
1240
+ if (result?.message) {
1241
+ rows.push({ label: '结果', value: result.message });
1242
+ }
1243
+ if (result?.handoff) {
1244
+ rows.push({ label: '状态', value: '需要人工接管' });
1245
+ }
1246
+ if (rows.length > 0) {
1247
+ ui.printKeyValueTable('Agent 回执', rows);
1248
+ }
1249
+ }
1250
+
1251
+ function getHistoryEnvelopePresentation(envelope) {
1252
+ const title = envelope.agentLabel
1253
+ ? `AI / ${envelope.agentLabel}`
1254
+ : 'AI';
1255
+ const badges = [];
1256
+
1257
+ if (envelope.kind === 'command') {
1258
+ badges.push('动作');
1259
+ if (envelope.command?.name) {
1260
+ badges.push(envelope.command.name);
1261
+ }
1262
+ } else if (envelope.kind === 'handoff') {
1263
+ badges.push('接管');
1264
+ } else if (envelope.type === 'error') {
1265
+ badges.push('错误');
1266
+ } else {
1267
+ badges.push('回复');
1268
+ }
1269
+
1270
+ return {
1271
+ title,
1272
+ badges
1273
+ };
1274
+ }
1275
+
1276
+ async function ensureBrowserReady() {
1277
+ if (browserManager.isReady()) {
1278
+ return true;
1279
+ }
1280
+
1281
+ try {
1282
+ ui.clear();
1283
+ ui.printWarning('正在启动浏览器...');
1284
+ await browserManager.init();
1285
+ return true;
1286
+ } catch (error) {
1287
+ ui.printError(`浏览器启动失败: ${error.message}`);
1288
+ return false;
1289
+ }
1290
+ }
1291
+
1292
+ async function navigateWithRecovery(url, {
1293
+ title = '页面连接失败',
1294
+ maxAttempts = 2
1295
+ } = {}) {
1296
+ let attempt = 0;
1297
+
1298
+ while (attempt < maxAttempts) {
1299
+ attempt++;
1300
+
1301
+ try {
1302
+ await browserManager.goto(url);
1303
+ return true;
1304
+ } catch (error) {
1305
+ const recoverable = isRecoverableNavigationError(error);
1306
+
1307
+ if (recoverable && attempt < maxAttempts) {
1308
+ ui.printWarning(`连接失败,正在自动重试 (${attempt}/${maxAttempts})...`);
1309
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1310
+ continue;
1311
+ }
1312
+
1313
+ ui.clear();
1314
+ ui.printTitle(title);
1315
+ ui.printNewline();
1316
+ const panel = buildNavigationRecoveryRows({
1317
+ title,
1318
+ url,
1319
+ attempt,
1320
+ maxAttempts,
1321
+ error
1322
+ });
1323
+ ui.printActionPanel(panel.title, panel.rows);
1324
+ ui.printNewline();
1325
+
1326
+ const action = await select({
1327
+ message: '选择恢复操作',
1328
+ choices: [
1329
+ { name: '重试连接', value: 'retry' },
1330
+ { name: '切换可见浏览器后重试', value: 'visible' },
1331
+ { name: '返回上一步', value: 'back' }
1332
+ ]
1333
+ });
1334
+
1335
+ if (action === 'back') {
1336
+ return false;
1337
+ }
1338
+
1339
+ if (action === 'visible') {
1340
+ if (config.browser?.allowVisibleBrowser === false) {
1341
+ ui.printInfo('当前配置禁止切换到可见浏览器。可在 /config 中开启“允许可见浏览器”。');
1342
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1343
+ continue;
1344
+ }
1345
+ try {
1346
+ await browserManager.relaunch({
1347
+ headless: false,
1348
+ url
1349
+ });
1350
+ ui.printSuccess('已切换到可见浏览器并重新打开页面');
1351
+ await new Promise((resolve) => setTimeout(resolve, 800));
1352
+ return true;
1353
+ } catch (visibleError) {
1354
+ ui.printError(`可见浏览器重试失败: ${visibleError.message}`);
1355
+ await new Promise((resolve) => setTimeout(resolve, 1000));
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ return false;
1362
+ }
1363
+
1364
+ async function deleteCurrentSession() {
1365
+ const current = sessionManager.current;
1366
+ if (!current) {
1367
+ ui.printInfo('当前没有活跃会话。');
1368
+ return false;
1369
+ }
1370
+
1371
+ const approved = await confirm({
1372
+ message: `确认删除当前会话“${current.title || '(无标题)'}”?`,
1373
+ default: false
1374
+ });
1375
+
1376
+ if (!approved) {
1377
+ ui.printMuted('已取消删除');
1378
+ return false;
1379
+ }
1380
+
1381
+ const deleted = await sessionManager.delete(current.id);
1382
+ if (!deleted) {
1383
+ ui.printError('删除当前会话失败');
1384
+ return false;
1385
+ }
1386
+
1387
+ hasActiveSession = false;
1388
+ resetAgentPrimer();
1389
+ ui.printSuccess('当前会话已删除');
1390
+ return true;
1391
+ }
1392
+
1393
+ async function renameCurrentSession() {
1394
+ if (!sessionManager.current) {
1395
+ ui.printInfo('当前没有活跃会话。');
1396
+ return false;
1397
+ }
1398
+
1399
+ const nextTitle = await askInput('新的会话标题: ', false);
1400
+ if (!nextTitle.trim()) {
1401
+ ui.printMuted('已取消');
1402
+ return false;
1403
+ }
1404
+
1405
+ const renamed = await sessionManager.rename(sessionManager.current.id, nextTitle);
1406
+ if (!renamed) {
1407
+ ui.printError('重命名会话失败');
1408
+ return false;
1409
+ }
1410
+
1411
+ ui.printSuccess(`会话标题已更新为 ${nextTitle}`);
1412
+ return true;
1413
+ }
1414
+
1415
+ async function showTaskPanel() {
1416
+ ui.clearPinnedHeader();
1417
+ ui.exitWorkspace();
1418
+ const runner = ensureTaskRunner();
1419
+ let selectedIndex = 0;
1420
+ const pageSize = 10;
1421
+
1422
+ while (true) {
1423
+ const tasks = await taskStore.list();
1424
+ const runtime = await runner.getRuntimeSnapshot();
1425
+ selectedIndex = Math.min(Math.max(selectedIndex, 0), Math.max(0, tasks.length - 1));
1426
+ const currentPage = Math.floor(selectedIndex / pageSize) + 1;
1427
+ const totalPages = Math.max(1, Math.ceil(Math.max(tasks.length, 1) / pageSize));
1428
+ const start = (currentPage - 1) * pageSize;
1429
+ const pageTasks = tasks.slice(start, start + pageSize);
1430
+ const localIndex = Math.max(0, selectedIndex - start);
1431
+
1432
+ ui.clear();
1433
+ ui.printTitle('任务面板');
1434
+ ui.printNewline();
1435
+ ui.printTaskRuntime(runtime);
1436
+ ui.printNewline();
1437
+ ui.printInputHints([
1438
+ 'W/S 或 ↑↓ 选择',
1439
+ 'A/D 或 ←→ 翻页',
1440
+ 'N 新建',
1441
+ 'Enter/R 启动',
1442
+ 'H 接管/恢复',
1443
+ 'P 预览',
1444
+ 'T 停止',
1445
+ 'X 删除',
1446
+ 'Q/Esc 返回'
1447
+ ]);
1448
+ ui.printNewline();
1449
+
1450
+ if (!tasks.length) {
1451
+ ui.printInfo('暂无任务。按 N 新建任务,按 Q 返回。');
1452
+ const emptyKey = await waitForKeypress();
1453
+
1454
+ if (emptyKey === 'q' || emptyKey === 'escape') {
1455
+ return;
1456
+ }
1457
+
1458
+ if (emptyKey !== 'n') {
1459
+ continue;
1460
+ }
1461
+ } else {
1462
+ ui.printSelectableList(
1463
+ `任务列表 · 第 ${currentPage}/${totalPages} 页,共 ${tasks.length} 条`,
1464
+ pageTasks.map((task) => `${task.title} | ${task.status} | ${task.provider || '-'} | ${task.mode || 'once'}`),
1465
+ localIndex
1466
+ );
1467
+ ui.printNewline();
1468
+ ui.printInfo('按 P 预览任务详情,按 N 进入创建向导');
1469
+ ui.printNewline();
1470
+ }
1471
+
1472
+ if (!tasks.length) {
1473
+ const created = await runTaskCreationWizard(runner);
1474
+ if (created?.task?.id) {
1475
+ const refreshed = await taskStore.list();
1476
+ const nextIndex = refreshed.findIndex((item) => item.id === created.task.id);
1477
+ selectedIndex = nextIndex >= 0 ? nextIndex : Math.max(0, refreshed.length - 1);
1478
+ }
1479
+ continue;
1480
+ }
1481
+
1482
+ const key = await waitForKeypress();
1483
+
1484
+ if (key === 'up' || key === 'w') {
1485
+ selectedIndex = Math.max(0, selectedIndex - 1);
1486
+ continue;
1487
+ }
1488
+
1489
+ if (key === 'down' || key === 's') {
1490
+ selectedIndex = Math.min(tasks.length - 1, selectedIndex + 1);
1491
+ continue;
1492
+ }
1493
+
1494
+ if (key === 'left' || key === 'a') {
1495
+ selectedIndex = Math.max(0, selectedIndex - pageSize);
1496
+ continue;
1497
+ }
1498
+
1499
+ if (key === 'right' || key === 'd') {
1500
+ selectedIndex = Math.min(tasks.length - 1, selectedIndex + pageSize);
1501
+ continue;
1502
+ }
1503
+
1504
+ if (key === 'n') {
1505
+ const created = await runTaskCreationWizard(runner);
1506
+ if (created?.task?.id) {
1507
+ const refreshed = await taskStore.list();
1508
+ const nextIndex = refreshed.findIndex((item) => item.id === created.task.id);
1509
+ selectedIndex = nextIndex >= 0 ? nextIndex : selectedIndex;
1510
+ }
1511
+ continue;
1512
+ }
1513
+
1514
+ const taskId = tasks[selectedIndex].id;
1515
+
1516
+ if (key === 'p') {
1517
+ const currentTask = await taskStore.load(taskId);
1518
+ ui.clear();
1519
+ ui.printTitle('任务预览');
1520
+ ui.printNewline();
1521
+ ui.printTaskDetails(currentTask);
1522
+ ui.printNewline();
1523
+ ui.printInfo('按任意键返回列表...');
1524
+ await waitForKeypress();
1525
+ continue;
1526
+ }
1527
+
1528
+ if (key === 'enter' || key === 'r') {
1529
+ if (tasks[selectedIndex].status === 'handoff') {
1530
+ await handleTaskHandoff(taskId, runner);
1531
+ continue;
1532
+ }
1533
+ await runner.queueTask(taskId);
1534
+ runner.runTask(taskId).catch(() => {});
1535
+ ui.printSuccess('任务已加入执行队列');
1536
+ await new Promise((resolve) => setTimeout(resolve, 700));
1537
+ continue;
1538
+ }
1539
+
1540
+ if (key === 'h') {
1541
+ if (tasks[selectedIndex].status !== 'handoff') {
1542
+ ui.printInfo('当前任务不是待接管状态。');
1543
+ await new Promise((resolve) => setTimeout(resolve, 700));
1544
+ continue;
1545
+ }
1546
+ await handleTaskHandoff(taskId, runner);
1547
+ continue;
1548
+ }
1549
+
1550
+ if (key === 't') {
1551
+ await runner.stopTask(taskId);
1552
+ ui.printSuccess('已请求停止任务');
1553
+ await new Promise((resolve) => setTimeout(resolve, 700));
1554
+ continue;
1555
+ }
1556
+
1557
+ if (key === 'x') {
1558
+ const approved = await confirm({
1559
+ message: `确认删除任务“${tasks[selectedIndex].title}”?`,
1560
+ default: false
1561
+ });
1562
+ if (approved) {
1563
+ await taskStore.delete(taskId);
1564
+ ui.printSuccess('任务已删除');
1565
+ await new Promise((resolve) => setTimeout(resolve, 700));
1566
+ selectedIndex = Math.max(0, Math.min(selectedIndex, tasks.length - 2));
1567
+ }
1568
+ continue;
1569
+ }
1570
+
1571
+ if (key === 'q' || key === 'escape') {
1572
+ return;
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ async function waitForChatResponse() {
1578
+ let response;
1579
+ let streamed = false;
1580
+ let streamBuffer = '';
1581
+ let streamMode = 'pending';
1582
+ let prefixPrinted = false;
1583
+ let suppressStructuredStream = false;
1584
+ let renderedDuringStream = false;
1585
+
1586
+ const flushStreamBuffer = (forceMarkdown = false) => {
1587
+ if (!streamBuffer) {
1588
+ return;
1589
+ }
1590
+
1591
+ if (forceMarkdown || streamMode === 'markdown') {
1592
+ ui.stopSpinner();
1593
+ ui.printNewline();
1594
+ ui.printMarkdown(streamBuffer);
1595
+ } else {
1596
+ if (!prefixPrinted) {
1597
+ ui.stopSpinner();
1598
+ ui.printAIPrefix();
1599
+ prefixPrinted = true;
1600
+ }
1601
+ ui.printInlineMarkdown(streamBuffer, { streaming: true });
1602
+ }
1603
+
1604
+ streamed = true;
1605
+ renderedDuringStream = true;
1606
+ streamBuffer = '';
1607
+ };
1608
+
1609
+ try {
1610
+ response = await chatManager.waitForResponse((chunk, first) => {
1611
+ if (agentMode) {
1612
+ return;
1613
+ }
1614
+
1615
+ streamBuffer += chunk;
1616
+ const preview = normalizeResponseEnvelope(streamBuffer, { agentMode: false });
1617
+
1618
+ if (preview.mode === 'structured' || preview.incompleteStructured) {
1619
+ suppressStructuredStream = true;
1620
+ return;
1621
+ }
1622
+
1623
+ if (streamMode === 'pending') {
1624
+ if (
1625
+ preview.streamMarkdownPreferred ||
1626
+ preview.renderAs === 'markdown'
1627
+ ) {
1628
+ streamMode = 'markdown';
1629
+ flushStreamBuffer(true);
1630
+ return;
1631
+ }
1632
+
1633
+ const compact = (preview.displayText || '').trim();
1634
+ if (first && compact.length < 24 && !/[.!?。!?\n]$/.test(compact)) {
1635
+ return;
1636
+ }
1637
+
1638
+ streamMode = 'inline';
1639
+ }
1640
+
1641
+ if (streamMode === 'markdown') {
1642
+ flushStreamBuffer(true);
1643
+ return;
1644
+ }
1645
+
1646
+ flushStreamBuffer(false);
1647
+ });
1648
+ } catch (error) {
1649
+ const handled = await maybeHandleIntervention('wait', error);
1650
+ if (!handled) {
1651
+ if (error?.message === '响应超时') {
1652
+ ui.stopSpinner();
1653
+ ui.printWarning('本轮等待模型回复超时,页面可能卡住、站点变慢,或当前 provider 没有正常出字。');
1654
+ try {
1655
+ const snapshot = await chatManager.inspectSite();
1656
+ printInspectResult(snapshot);
1657
+ } catch {
1658
+ // ignore inspect errors on timeout fallback
1659
+ }
1660
+ const switched = await maybeFallbackProvider('timeout');
1661
+ if (switched) {
1662
+ error.silentReported = true;
1663
+ throw new Error('PROVIDER_FALLBACK_RETRY');
1664
+ }
1665
+ error.silentReported = true;
1666
+ }
1667
+ throw error;
1668
+ }
1669
+
1670
+ response = await chatManager.waitForResponse((chunk, first) => {
1671
+ if (agentMode) {
1672
+ return;
1673
+ }
1674
+
1675
+ streamBuffer += chunk;
1676
+ const preview = normalizeResponseEnvelope(streamBuffer, { agentMode: false });
1677
+
1678
+ if (preview.mode === 'structured' || preview.incompleteStructured) {
1679
+ suppressStructuredStream = true;
1680
+ return;
1681
+ }
1682
+
1683
+ if (streamMode === 'pending') {
1684
+ if (
1685
+ preview.streamMarkdownPreferred ||
1686
+ preview.renderAs === 'markdown'
1687
+ ) {
1688
+ streamMode = 'markdown';
1689
+ flushStreamBuffer(true);
1690
+ return;
1691
+ }
1692
+
1693
+ const compact = (preview.displayText || '').trim();
1694
+ if (first && compact.length < 24 && !/[.!?。!?\n]$/.test(compact)) {
1695
+ return;
1696
+ }
1697
+
1698
+ streamMode = 'inline';
1699
+ }
1700
+
1701
+ if (streamMode === 'markdown') {
1702
+ flushStreamBuffer(true);
1703
+ return;
1704
+ }
1705
+
1706
+ flushStreamBuffer(false);
1707
+ });
1708
+ }
1709
+
1710
+ if (streamBuffer) {
1711
+ if (!suppressStructuredStream) {
1712
+ if (streamMode === 'markdown') {
1713
+ flushStreamBuffer(true);
1714
+ } else {
1715
+ flushStreamBuffer(false);
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ return {
1721
+ response,
1722
+ streamed,
1723
+ streamMode,
1724
+ renderedDuringStream,
1725
+ suppressStructuredStream
1726
+ };
1727
+ }
1728
+
1729
+ async function switchProviderWithContext(targetProvider, {
1730
+ reason = '当前 provider 不稳定'
1731
+ } = {}) {
1732
+ const previousSession = sessionManager.current;
1733
+ if (!targetProvider) {
1734
+ return false;
1735
+ }
1736
+
1737
+ ui.printWarning(`${reason},正在切换到 ${targetProvider.name}...`);
1738
+
1739
+ if (requiresBrowserForProvider(targetProvider)) {
1740
+ const opened = await navigateWithRecovery(targetProvider.url, {
1741
+ title: `切换到 ${targetProvider.name} 失败`
1742
+ });
1743
+ if (!opened) {
1744
+ ui.printMuted('自动切换 provider 失败');
1745
+ return false;
1746
+ }
1747
+ await new Promise((resolve) => setTimeout(resolve, 1200));
1748
+ }
1749
+
1750
+ rebuildChatManager(targetProvider);
1751
+ resetAgentPrimer();
1752
+ await sessionManager.switchProvider(targetProvider, {
1753
+ reason: `fallback:${reason}`
1754
+ });
1755
+ sessionManager.setUrl(getProviderSessionUrl(targetProvider, browserManager));
1756
+
1757
+ if (previousSession?.messages?.length) {
1758
+ ui.printMuted('正在恢复上下文到新的 provider...');
1759
+ const primer = buildHistoryPrimer(
1760
+ previousSession.messages,
1761
+ getSessionCompressionOptions()
1762
+ );
1763
+
1764
+ const success = await chatManager.sendMessage(primer);
1765
+ if (success) {
1766
+ try {
1767
+ await new Promise((resolve) => setTimeout(resolve, 800));
1768
+ await chatManager.waitForResponse(() => {}, {
1769
+ timeoutMs: 4000,
1770
+ allowEmptyOnTimeout: true
1771
+ });
1772
+ } catch {
1773
+ // ignore fallback primer ack timeout
1774
+ }
1775
+ }
1776
+ }
1777
+
1778
+ ui.printSuccess(`已切换到 ${targetProvider.name}`);
1779
+ printChatOnboarding();
1780
+ ui.printNewline();
1781
+ lastAgentOrReplySummary = `切换到 ${targetProvider.name}`;
1782
+ return true;
1783
+ }
1784
+
1785
+ async function maybeFallbackProvider(trigger = 'timeout') {
1786
+ if (!config.providersRuntime?.autoFallback || !chatManager?.provider) {
1787
+ return false;
1788
+ }
1789
+
1790
+ const mode = config.providersRuntime?.includeApiFallback ? 'web-first' : 'web-only';
1791
+ const fallback = getFallbackProvider({
1792
+ currentProviderId: chatManager.provider.id,
1793
+ mode
1794
+ });
1795
+
1796
+ if (!fallback) {
1797
+ return false;
1798
+ }
1799
+
1800
+ const reason =
1801
+ trigger === 'timeout'
1802
+ ? '当前 provider 响应超时'
1803
+ : trigger === 'send'
1804
+ ? '当前 provider 发送失败'
1805
+ : '当前 provider 状态异常';
1806
+
1807
+ return switchProviderWithContext(fallback, { reason });
1808
+ }
1809
+
1810
+ async function submitConversationMessage(content, {
1811
+ allowRetry = true,
1812
+ skipLog = false,
1813
+ sendAttempt = 0,
1814
+ responseAttempt = 0
1815
+ } = {}) {
1816
+ if (
1817
+ !isApiProvider(chatManager.provider) &&
1818
+ config.browser?.shareAuthState &&
1819
+ browserManager.isChannelReady?.('agent') &&
1820
+ browserManager.isChannelReady?.('chat') &&
1821
+ typeof browserManager.syncStorageState === 'function'
1822
+ ) {
1823
+ await browserManager.syncStorageState('agent', 'chat').catch(() => {});
1824
+ }
1825
+
1826
+ if (!skipLog) {
1827
+ lastSubmittedQuestion = content;
1828
+ inputHistory.push(content);
1829
+ if (inputHistory.length > 50) {
1830
+ inputHistory.shift();
1831
+ }
1832
+ await logger.log('YOU', content);
1833
+ await sessionManager.addMessage('user', content);
1834
+ }
1835
+
1836
+ const localIntent = detectLocalIntent(content);
1837
+ if (localIntent.matched) {
1838
+ return handleLocalIntent(localIntent);
1839
+ }
1840
+
1841
+ await primeAgentMode();
1842
+
1843
+ ui.printAIPrefix();
1844
+ ui.startSpinner();
1845
+
1846
+ const success = await chatManager.sendMessage(toProviderMessage(content));
1847
+
1848
+ if (!success) {
1849
+ ui.stopSpinner();
1850
+ const handled = await maybeHandleIntervention(
1851
+ 'send',
1852
+ new Error('发送消息失败,可能需要登录、验证或人工确认')
1853
+ );
1854
+ if (handled && allowRetry) {
1855
+ return retryQuestion(content);
1856
+ }
1857
+ const { sendRetryCount } = getProviderRetryConfig();
1858
+ if (sendAttempt < sendRetryCount) {
1859
+ ui.printWarning(`当前 provider 发送失败,正在第 ${sendAttempt + 1}/${sendRetryCount} 次重试...`);
1860
+ await waitBeforeProviderRetry(sendAttempt + 1, '发送');
1861
+ return submitConversationMessage(content, {
1862
+ allowRetry,
1863
+ skipLog: true,
1864
+ sendAttempt: sendAttempt + 1,
1865
+ responseAttempt
1866
+ });
1867
+ }
1868
+ if (allowRetry) {
1869
+ const switched = await maybeFallbackProvider('send');
1870
+ if (switched) {
1871
+ return retryQuestion(content, { skipLog: true });
1872
+ }
1873
+ }
1874
+ ui.printError('发送消息失败,请重试');
1875
+ return loopQuestion();
1876
+ }
1877
+
1878
+ await new Promise(resolve => setTimeout(resolve, 500));
1879
+ sessionManager.setUrl(getProviderSessionUrl(chatManager.provider, browserManager));
1880
+
1881
+ let responseResult;
1882
+
1883
+ try {
1884
+ responseResult = await waitForChatResponse();
1885
+ } catch (error) {
1886
+ ui.stopSpinner();
1887
+ const { responseRetryCount } = getProviderRetryConfig();
1888
+ if (error?.message === '响应超时' && responseAttempt < responseRetryCount) {
1889
+ ui.printWarning(`当前 provider 回复超时,正在第 ${responseAttempt + 1}/${responseRetryCount} 次重试...`);
1890
+ await waitBeforeProviderRetry(responseAttempt + 1, '回复');
1891
+ return submitConversationMessage(content, {
1892
+ allowRetry,
1893
+ skipLog: true,
1894
+ sendAttempt,
1895
+ responseAttempt: responseAttempt + 1
1896
+ });
1897
+ }
1898
+ throw error;
1899
+ }
1900
+
1901
+ if (isInterruptedTurnResponse(responseResult?.response) && responseAttempt < 1) {
1902
+ ui.printMuted('检测到网页回复疑似被中断,正在等待稳定后补抓一次...');
1903
+ await new Promise((resolve) => setTimeout(resolve, 1500));
1904
+ try {
1905
+ responseResult = await waitForChatResponse();
1906
+ } catch (error) {
1907
+ ui.stopSpinner();
1908
+ if (!error?.silentReported) {
1909
+ ui.printWarning('补抓回复失败,将先展示当前已拿到的内容。');
1910
+ }
1911
+ }
1912
+ }
1913
+
1914
+ ui.stopSpinner();
1915
+ const {
1916
+ response,
1917
+ streamed,
1918
+ streamMode,
1919
+ renderedDuringStream,
1920
+ suppressStructuredStream
1921
+ } = responseResult;
1922
+ await handleModelResponse(response, {
1923
+ streamed,
1924
+ streamMode,
1925
+ renderedDuringStream,
1926
+ suppressStructuredStream
1927
+ });
1928
+ ui.ensureTrailingNewline();
1929
+ return loopQuestion();
1930
+ }
1931
+
1932
+ async function handleLocalIntent(intent) {
1933
+ if (intent?.control) {
1934
+ const action = intent.control.action;
1935
+
1936
+ if (action === 'switch_provider') {
1937
+ const targetProvider = getProvider(intent.control.providerId);
1938
+ const currentProviderId = chatManager?.provider?.id || getProvider().id;
1939
+
1940
+ if (targetProvider.id === currentProviderId) {
1941
+ ui.printInfo(`当前已经是 ${targetProvider.name}。`);
1942
+ return loopQuestion();
1943
+ }
1944
+
1945
+ const switched = await switchProviderWithContext(targetProvider, {
1946
+ reason: '根据自然语言指令切换当前会话 provider'
1947
+ });
1948
+ if (switched) {
1949
+ chatChromeDirty = true;
1950
+ }
1951
+ return loopQuestion();
1952
+ }
1953
+
1954
+ if (action === 'show_provider_status') {
1955
+ ui.printNewline();
1956
+ ui.printKeyValueTable('当前会话状态', [
1957
+ { label: 'Provider', value: chatManager?.provider?.name || getProvider().name },
1958
+ { label: 'Provider ID', value: chatManager?.provider?.id || getProvider().id },
1959
+ { label: 'Mode', value: agentMode ? 'Agent' : 'Chat' },
1960
+ { label: 'Browser', value: config.browser?.headless ? 'Headless' : 'Visible' },
1961
+ { label: 'Session', value: sessionManager.current?.title || '(无会话)' },
1962
+ { label: 'Messages', value: String(sessionManager.current?.messages?.length || 0) }
1963
+ ]);
1964
+ ui.printNewline();
1965
+ return loopQuestion();
1966
+ }
1967
+
1968
+ if (action === 'show_app_state') {
1969
+ return handleLocalIntent({
1970
+ matched: true,
1971
+ reason: 'show-app-state',
1972
+ command: {
1973
+ name: 'get_app_state',
1974
+ args: {}
1975
+ }
1976
+ });
1977
+ }
1978
+
1979
+ if (action === 'show_config_summary') {
1980
+ ui.printNewline();
1981
+ ui.printKeyValueTable('配置摘要', [
1982
+ { label: 'Default Provider', value: getProvider().name },
1983
+ { label: 'Default Provider ID', value: getProvider().id },
1984
+ { label: 'Browser', value: config.browser?.headless ? 'Headless' : 'Visible' },
1985
+ { label: 'Allow Visible', value: String(config.browser?.allowVisibleBrowser !== false) },
1986
+ { label: 'Share Auth', value: String(Boolean(config.browser?.shareAuthState)) },
1987
+ { label: 'Agent Enabled', value: String(Boolean(config.agent?.enabled)) },
1988
+ { label: 'Auto Execute', value: String(Boolean(config.agent?.autoExecuteCommands)) },
1989
+ { label: 'Allow Shell', value: String(Boolean(config.agent?.allowShell)) },
1990
+ { label: 'Allow File Write', value: String(Boolean(config.agent?.allowFileWrite)) },
1991
+ { label: 'Multi Agent', value: String(Boolean(config.agent?.multiAgent)) }
1992
+ ]);
1993
+ ui.printNewline();
1994
+ return loopQuestion();
1995
+ }
1996
+
1997
+ if (action === 'open_task_panel') {
1998
+ await showTaskPanel();
1999
+ chatChromeDirty = true;
2000
+ return loopQuestion();
2001
+ }
2002
+
2003
+ if (action === 'open_memory_panel') {
2004
+ await showMemoryPanel();
2005
+ chatChromeDirty = true;
2006
+ return loopQuestion();
2007
+ }
2008
+
2009
+ if (action === 'open_sessions_panel') {
2010
+ const sessionMeta = await browseSessions({
2011
+ sessionManager,
2012
+ ui
2013
+ });
2014
+ if (sessionMeta) {
2015
+ const resumed = await resumeSession(sessionMeta);
2016
+ if (resumed) {
2017
+ hasActiveSession = true;
2018
+ }
2019
+ } else {
2020
+ chatChromeDirty = true;
2021
+ }
2022
+ return loopQuestion();
2023
+ }
2024
+
2025
+ if (action === 'open_settings') {
2026
+ const beforeProviderId = getProvider().id;
2027
+ await showSettingsMenu({ ui, config });
2028
+ const afterProviderId = getProvider().id;
2029
+ if (afterProviderId !== beforeProviderId) {
2030
+ ui.printInfo(`默认提供方已改为 ${getProvider().name}。当前会话仍使用 ${chatManager?.provider?.name || getProvider().name}。`);
2031
+ ui.printInfo('如需把当前会话一起切过去,请输入 /switch-provider。');
2032
+ }
2033
+ printChatOnboarding();
2034
+ ui.printNewline();
2035
+ return loopQuestion();
2036
+ }
2037
+
2038
+ if (action === 'open_command_palette') {
2039
+ const command = await showCommandPalette();
2040
+ if (command === '__cancel__') {
2041
+ return loopQuestion();
2042
+ }
2043
+ return loopQuestion(command);
2044
+ }
2045
+ }
2046
+
2047
+ ui.printAIPrefix();
2048
+ ui.startSpinner();
2049
+
2050
+ try {
2051
+ if (
2052
+ !config.agent?.autoExecuteCommands &&
2053
+ typeof agentExecutor?.requiresManualApproval === 'function' &&
2054
+ agentExecutor.requiresManualApproval(intent.command)
2055
+ ) {
2056
+ ui.stopSpinner();
2057
+ const approved = await agentExecutor.confirm(intent.command);
2058
+ if (!approved) {
2059
+ ui.printMuted('已取消执行该命令');
2060
+ return loopQuestion();
2061
+ }
2062
+ ui.startSpinner();
2063
+ }
2064
+
2065
+ const result = await agentExecutor.execute(intent.command);
2066
+ ui.stopSpinner();
2067
+
2068
+ if (!result.ok) {
2069
+ ui.printError(result.error);
2070
+ await logger.log('AI ', result.error);
2071
+ await sessionManager.addMessage('assistant', result.error);
2072
+ return loopQuestion();
2073
+ }
2074
+
2075
+ const responseText = formatLocalIntentResult(intent, result);
2076
+ ui.printNewline();
2077
+ ui.printMarkdown(responseText);
2078
+ await logger.log('AI ', responseText);
2079
+ await sessionManager.addMessage('assistant', responseText);
2080
+ return loopQuestion();
2081
+ } catch (error) {
2082
+ ui.stopSpinner();
2083
+ ui.printError(error.message);
2084
+ await logger.log('AI ', error.message);
2085
+ await sessionManager.addMessage('assistant', error.message);
2086
+ return loopQuestion();
2087
+ }
2088
+ }
2089
+
2090
+ function printInspectResult(snapshot) {
2091
+ ui.printNewline();
2092
+ ui.printTitle(`站点检查 - ${snapshot.provider}`);
2093
+ ui.printNewline();
2094
+ ui.printInfo(`输入框: ${snapshot.inputSelector || '(未命中)'}`);
2095
+ ui.printInfo(`发送按钮: ${snapshot.sendSelector || '(未命中,将回退 Enter)'}`);
2096
+ ui.printInfo(`读取节点: ${snapshot.readSelector || '(未命中)'}`);
2097
+ ui.printInfo(`完成模式: ${snapshot.completionMode}`);
2098
+ ui.printInfo(`pending: ${String(snapshot.result.pending)}`);
2099
+ ui.printInfo(`done: ${String(snapshot.result.done)}`);
2100
+ ui.printInfo(`error: ${snapshot.result.error || '(无)'}`);
2101
+ ui.printInfo(`preview: ${snapshot.result.preview || '(空)'}`);
2102
+ if (snapshot.intervention?.blocked) {
2103
+ ui.printInfo(`人工介入: ${snapshot.intervention.reason}`);
2104
+ ui.printInfo(`页面: ${snapshot.intervention.url || '(未知)'}`);
2105
+ if (config.browser?.allowVisibleBrowser === false) {
2106
+ ui.printInfo('当前配置已禁止可见浏览器,程序不会自动弹出窗口。');
2107
+ ui.printInfo('如需手动接管,请先在 /config 中开启“允许可见浏览器”。');
2108
+ }
2109
+ }
2110
+ if (snapshot.observation?.observation?.counts) {
2111
+ const counts = snapshot.observation.observation.counts;
2112
+ ui.printInfo(
2113
+ `观测: req ${counts.requests} / resp ${counts.responses} / console ${counts.console} / error ${counts.pageErrors}`
2114
+ );
2115
+ }
2116
+ const latestResponse = snapshot.observation?.observation?.responses?.at?.(-1);
2117
+ if (latestResponse?.url) {
2118
+ ui.printInfo(
2119
+ `最近响应: ${latestResponse.status || '-'} ${latestResponse.method || ''} ${latestResponse.url}`
2120
+ );
2121
+ }
2122
+ const latestError = snapshot.observation?.observation?.pageErrors?.at?.(-1);
2123
+ if (latestError?.message) {
2124
+ ui.printInfo(`最近页面错误: ${latestError.message}`);
2125
+ }
2126
+ ui.printNewline();
2127
+ }
2128
+
2129
+ async function maybeHandleIntervention(stage, error) {
2130
+ if (!chatManager || !conversationAgent) {
2131
+ return false;
2132
+ }
2133
+
2134
+ const result = await conversationAgent.handleIntervention({
2135
+ chatManager,
2136
+ stage,
2137
+ error
2138
+ });
2139
+
2140
+ if (!result.handled) {
2141
+ return false;
2142
+ }
2143
+
2144
+ rebuildChatManager(chatManager.provider);
2145
+ sessionManager.setUrl(browserManager.getCurrentUrl());
2146
+ return true;
2147
+ }
2148
+
2149
+ async function primeAgentMode() {
2150
+ if (!agentMode || !sessionManager.current) {
2151
+ return;
2152
+ }
2153
+
2154
+ if (agentPrimedSessionId === sessionManager.current.id) {
2155
+ return;
2156
+ }
2157
+
2158
+ ui.printWarning('正在初始化 Agent 协议...');
2159
+ const primer = buildAgentPrimer(
2160
+ getAgentSystemPrompt({
2161
+ providerId: chatManager?.provider?.id || '',
2162
+ multiAgentEnabled: isMultiAgentEnabled(),
2163
+ allowShell: Boolean(config.agent?.allowShell),
2164
+ allowFileWrite: Boolean(config.agent?.allowFileWrite)
2165
+ }),
2166
+ sessionManager.current.messages || [],
2167
+ getSessionCompressionOptions()
2168
+ );
2169
+ const success = await chatManager.sendMessage(primer);
2170
+
2171
+ if (!success) {
2172
+ ui.printMuted('Agent 预热未完成,将直接按本轮协议继续。');
2173
+ agentPrimedSessionId = sessionManager.current.id;
2174
+ return;
2175
+ }
2176
+
2177
+ agentPrimedSessionId = sessionManager.current.id;
2178
+
2179
+ try {
2180
+ await new Promise(resolve => setTimeout(resolve, 800));
2181
+ await chatManager.waitForResponse(() => {}, {
2182
+ timeoutMs: 4000,
2183
+ allowEmptyOnTimeout: true
2184
+ });
2185
+ } catch {
2186
+ ui.printMuted('Agent 预热确认较慢,继续直接发送当前任务。');
2187
+ }
2188
+ }
2189
+
2190
+ function printAgentResult(parsed) {
2191
+ const text = parsed.consoleText ?? parsed.displayText ?? parsed.text ?? '';
2192
+
2193
+ if (!text) {
2194
+ return;
2195
+ }
2196
+
2197
+ if ((parsed.consoleTextRenderAs || parsed.renderAs) === 'markdown') {
2198
+ ui.printAIPrefix();
2199
+ ui.printNewline();
2200
+ ui.printMarkdown(text);
2201
+ return;
2202
+ }
2203
+
2204
+ ui.printAIPrefix();
2205
+ ui.printInlineMarkdown(text);
2206
+ ui.printNewline();
2207
+ }
2208
+
2209
+ function printStandardResponse(envelope) {
2210
+ const text = envelope.displayText ?? envelope.text ?? '';
2211
+ if (!text) {
2212
+ ui.printNewline();
2213
+ return;
2214
+ }
2215
+ ui.printNewline();
2216
+ if (envelope.renderAs === 'markdown') {
2217
+ ui.printMarkdown(text);
2218
+ } else if (envelope.inlinePreferred) {
2219
+ ui.printAIPrefix();
2220
+ ui.printInlineMarkdown(text);
2221
+ ui.printNewline();
2222
+ } else {
2223
+ ui.printMarkdown(text);
2224
+ }
2225
+ }
2226
+
2227
+ async function handleModelResponse(response, context = {}) {
2228
+ await logger.log('AI ', response);
2229
+ await sessionManager.addMessage('assistant', response);
2230
+
2231
+ const envelope = normalizeResponseEnvelope(response, { agentMode });
2232
+ const display = (envelope.displayText ?? envelope.text ?? '').trim();
2233
+ if (display) {
2234
+ const lines = display.split('\n').map((line) => line.trim()).filter(Boolean);
2235
+ const summary = lines[0] || '';
2236
+ const plan =
2237
+ lines
2238
+ .filter((line) => /^[-*]\s+/.test(line) || /^\d+\.\s+/.test(line))
2239
+ .map((line) => line.replace(/^[-*]\s+/, '').replace(/^\d+\.\s+/, '').trim())
2240
+ .slice(0, 6);
2241
+ await sessionManager.updateMemory({
2242
+ summary: summary.slice(0, 240),
2243
+ plan
2244
+ });
2245
+ lastAgentOrReplySummary = summary.slice(0, 160);
2246
+ }
2247
+
2248
+ if (agentMode) {
2249
+ printAgentResult(envelope);
2250
+ await maybeExecuteAgentCommand(envelope, context);
2251
+ } else {
2252
+ if (context.streamed && !context.suppressStructuredStream && context.renderedDuringStream) {
2253
+ if (context.streamMode === 'inline' || envelope.inlinePreferred) {
2254
+ ui.ensureTrailingNewline();
2255
+ } else {
2256
+ ui.printNewline();
2257
+ }
2258
+ } else if (context.streamed) {
2259
+ printStandardResponse(envelope);
2260
+ } else if (envelope.renderAs === 'markdown') {
2261
+ ui.printNewline();
2262
+ ui.printMarkdown(envelope.displayText ?? envelope.text);
2263
+ } else if (envelope.inlinePreferred) {
2264
+ ui.printAIPrefix();
2265
+ ui.printInlineMarkdown(envelope.displayText ?? envelope.text);
2266
+ ui.printNewline();
2267
+ } else {
2268
+ ui.printNewline();
2269
+ ui.printMarkdown(envelope.displayText ?? envelope.text);
2270
+ }
2271
+ }
2272
+
2273
+ if (!agentMode) {
2274
+ ui.ensureTrailingNewline();
2275
+ }
2276
+ }
2277
+
2278
+ function toProviderMessage(content) {
2279
+ if (!agentMode) {
2280
+ return content;
2281
+ }
2282
+
2283
+ return wrapAgentTurnMessage(content, {
2284
+ providerId: chatManager?.provider?.id || ''
2285
+ });
2286
+ }
2287
+
2288
+ async function requestAgentFollowupResponse(content, _context = {}) {
2289
+ if (
2290
+ !isApiProvider(chatManager.provider) &&
2291
+ config.browser?.shareAuthState &&
2292
+ browserManager.isChannelReady?.('agent') &&
2293
+ browserManager.isChannelReady?.('chat') &&
2294
+ typeof browserManager.syncStorageState === 'function'
2295
+ ) {
2296
+ await browserManager.syncStorageState('agent', 'chat').catch(() => {});
2297
+ }
2298
+
2299
+ ui.printWarning('Agent 正在执行后续消息...');
2300
+ ui.printAgentStatus({
2301
+ stage: '继续规划',
2302
+ summary: '正在向模型请求下一轮结构化动作',
2303
+ detail: '主控会结合上一条命令回执继续判断是否观察、点击、输入或结束任务',
2304
+ agentLabel: getAgentRoleLabel('orchestrator')
2305
+ });
2306
+ ui.printTimeline('Agent 时间线', [
2307
+ createTimelineEvent('发送给模型', '继续当前任务并等待下一轮结构化回复', {
2308
+ agentRole: 'orchestrator'
2309
+ })
2310
+ ]);
2311
+ const sent = await chatManager.sendMessage(toProviderMessage(content));
2312
+
2313
+ if (!sent) {
2314
+ throw new Error('Agent 后续消息发送失败');
2315
+ }
2316
+
2317
+ await new Promise(resolve => setTimeout(resolve, 500));
2318
+ sessionManager.setUrl(getProviderSessionUrl(chatManager.provider, browserManager));
2319
+
2320
+ ui.printAgentStatus({
2321
+ stage: '等待回复',
2322
+ summary: '模型正在生成下一步',
2323
+ detail: '如果网页模型返回结构化命令,将自动进入下一步执行',
2324
+ agentLabel: getAgentRoleLabel('orchestrator')
2325
+ });
2326
+ ui.printTimeline('Agent 时间线', [
2327
+ createTimelineEvent('等待回复', '模型正在返回新的观察或下一步命令', {
2328
+ agentRole: 'orchestrator'
2329
+ })
2330
+ ]);
2331
+ const response = await chatManager.waitForResponse(() => {});
2332
+
2333
+ const retryAttempt = Number(_context?.retryAttempt) || 0;
2334
+ if (isInterruptedTurnResponse(response) && retryAttempt < 1) {
2335
+ ui.printMuted('检测到网页模型把上一轮回答中断了,正在等待稳定后重试一次...');
2336
+ await new Promise((resolve) => setTimeout(resolve, 1500));
2337
+ return requestAgentFollowupResponse(content, {
2338
+ ..._context,
2339
+ retryAttempt: retryAttempt + 1
2340
+ });
2341
+ }
2342
+
2343
+ return response;
2344
+ }
2345
+
2346
+ async function sendAgentCommandMessage(content, context = {}) {
2347
+ const response = await requestAgentFollowupResponse(content, context);
2348
+ await handleModelResponse(response, context);
2349
+ return response;
2350
+ }
2351
+
2352
+ async function maybeExecuteAgentCommand(parsed, context = {}) {
2353
+ if (parsed.kind === 'handoff') {
2354
+ ui.printWarning('模型建议人工接管,可输入 /agent');
2355
+ return;
2356
+ }
2357
+
2358
+ if (parsed.kind !== 'command' || !parsed.command) {
2359
+ return;
2360
+ }
2361
+
2362
+ const maxSteps = Math.max(1, Number(config.agent?.maxAutoSteps) || 1);
2363
+ const outcome = await runAgentLoop({
2364
+ initialEnvelope: parsed,
2365
+ initialStep: context.followupStep ?? 0,
2366
+ maxSteps,
2367
+ agentExecutor,
2368
+ requestFollowupResponse: requestAgentFollowupResponse,
2369
+ onBeforeCommand: async ({ envelope, step, maxSteps }) => {
2370
+ ui.printAgentStep({
2371
+ step,
2372
+ maxSteps,
2373
+ command: envelope.command,
2374
+ phase: 'execute',
2375
+ agentLabel: envelope.agentLabel || resolveAgentLabel(envelope.command, envelope.agent),
2376
+ multiAgent: isMultiAgentEnabled()
2377
+ });
2378
+ printAgentActionPanel(envelope, step, maxSteps);
2379
+ ui.printTimeline('Agent 时间线', [
2380
+ createTimelineEvent(`第 ${step} 步`, envelope.displayText || envelope.text || envelope.command.name, {
2381
+ agentRole: resolveAgentRole(envelope.command, envelope.agent)
2382
+ }),
2383
+ createTimelineEvent('执行命令', envelope.command.name, {
2384
+ agentRole: resolveAgentRole(envelope.command, envelope.agent)
2385
+ })
2386
+ ]);
2387
+
2388
+ if (config.agent?.autoExecuteCommands) {
2389
+ return { stop: false };
2390
+ }
2391
+
2392
+ const approved = await agentExecutor.confirm(envelope.command);
2393
+ if (!approved) {
2394
+ ui.printMuted('已取消执行该命令');
2395
+ return { stop: true, status: 'cancelled' };
2396
+ }
2397
+
2398
+ return { stop: false };
2399
+ },
2400
+ onAfterCommand: async ({ envelope, result, step, maxSteps }) => {
2401
+ if (!result.ok) {
2402
+ if (result.recoverable) {
2403
+ ui.printWarning(`动作未成功,正在尝试调整策略: ${result.error}`);
2404
+ ui.printAgentStatus({
2405
+ stage: '恢复中',
2406
+ summary: '上一条命令失败,但判定为可恢复',
2407
+ detail: result.error,
2408
+ command: envelope.command,
2409
+ agentLabel: envelope.agentLabel || resolveAgentLabel(envelope.command, envelope.agent)
2410
+ });
2411
+ } else {
2412
+ ui.printError(result.error);
2413
+ }
2414
+ return;
2415
+ }
2416
+
2417
+ if (result.message) {
2418
+ ui.printSuccess(result.message);
2419
+ }
2420
+
2421
+ printAgentActionSummary(envelope, result);
2422
+
2423
+ if (result.output && !result.silentOutput) {
2424
+ ui.printNewline();
2425
+ ui.printMarkdown(result.output);
2426
+ }
2427
+
2428
+ if (result.handoff) {
2429
+ ui.printWarning('需要你手动接手时,可输入 /agent');
2430
+ return;
2431
+ }
2432
+
2433
+ if (agentExecutor.shouldContinue(result) && step < maxSteps) {
2434
+ ui.printAgentStep({
2435
+ step,
2436
+ maxSteps,
2437
+ command: {
2438
+ name: result.commandName,
2439
+ args: {}
2440
+ },
2441
+ phase: 'observe',
2442
+ agentLabel: getAgentRoleLabel(result.agent || inferAgentRoleFromCommand({
2443
+ name: result.commandName
2444
+ })),
2445
+ multiAgent: isMultiAgentEnabled()
2446
+ });
2447
+ ui.printTimeline('Agent 时间线', [
2448
+ createTimelineEvent(`第 ${step} 步`, `读取 ${result.commandName || '上一动作'} 的结果`, {
2449
+ agentRole: resolveAgentRole({ name: result.commandName }, result.agent)
2450
+ }),
2451
+ createTimelineEvent(`第 ${step + 1} 步`, '基于结果继续向模型请求下一步', {
2452
+ agentRole: 'orchestrator'
2453
+ })
2454
+ ]);
2455
+ }
2456
+ },
2457
+ onFollowupResponse: async ({ rawResponse, envelope }) => {
2458
+ await logger.log('AI ', rawResponse);
2459
+ await sessionManager.addMessage('assistant', rawResponse);
2460
+ printAgentResult(envelope);
2461
+ }
2462
+ });
2463
+
2464
+ if (outcome.status === 'handoff' && outcome.envelope?.kind === 'handoff') {
2465
+ ui.printWarning('模型建议人工接管,可输入 /agent');
2466
+ }
2467
+
2468
+ if (outcome.status === 'limit') {
2469
+ ui.printMuted(`已达到 Agent 自动续跑上限 (${maxSteps} 步)`);
2470
+ }
2471
+ }
2472
+
2473
+ async function loopQuestion(overrideQuestion = null) {
2474
+ if (isShuttingDown) return;
2475
+
2476
+ try {
2477
+ ui.ensureTrailingNewline();
2478
+ const rawQuestion = overrideQuestion ?? await askQuestion();
2479
+ const question = rawQuestion.trim();
2480
+ const isSlashCommand = question.startsWith('/');
2481
+
2482
+ // 检测快捷命令
2483
+ if (question === '/menu' || question === '/back') {
2484
+ ui.printNewline();
2485
+ ui.printInfo('返回主菜单...');
2486
+ await new Promise(resolve => setTimeout(resolve, 800));
2487
+ return 'MENU';
2488
+ }
2489
+
2490
+ if (question === '/quit' || question === '/exit') {
2491
+ process.kill(process.pid, 'SIGINT');
2492
+ return;
2493
+ }
2494
+
2495
+ if (question === '/doc') {
2496
+ showQuickDoc();
2497
+ ui.printInfo('按回车返回聊天...');
2498
+ await askInput('', true);
2499
+ chatChromeDirty = true;
2500
+ return await loopQuestion();
2501
+ }
2502
+
2503
+ if (question === '/help') {
2504
+ ui.printNewline();
2505
+ ui.printChatHelp(agentMode);
2506
+ ui.printNewline();
2507
+ chatChromeDirty = true;
2508
+ return await loopQuestion();
2509
+ }
2510
+
2511
+ if (question === '/status') {
2512
+ await showRuntimeOverview();
2513
+ chatChromeDirty = true;
2514
+ return await loopQuestion();
2515
+ }
2516
+
2517
+ if (question === '/' || question === '/cmd') {
2518
+ const command = await showCommandPalette();
2519
+ if (command === '__cancel__') {
2520
+ return await loopQuestion();
2521
+ }
2522
+ return await loopQuestion(command);
2523
+ }
2524
+
2525
+ if (question === '/h') {
2526
+ return await loopQuestion('/help');
2527
+ }
2528
+
2529
+ if (question === '/m') {
2530
+ return await loopQuestion('/menu');
2531
+ }
2532
+
2533
+ if (question === '/s') {
2534
+ return await loopQuestion('/status');
2535
+ }
2536
+
2537
+ if (question === '/t') {
2538
+ return await loopQuestion('/task');
2539
+ }
2540
+
2541
+ if (question === '/paste') {
2542
+ const pasted = await askMultiline('进入多行粘贴模式');
2543
+ if (!pasted) {
2544
+ ui.printMuted('已取消');
2545
+ return await loopQuestion();
2546
+ }
2547
+
2548
+ ui.printUserMessage(pasted);
2549
+ return await submitConversationMessage(pasted);
2550
+ }
2551
+
2552
+ if (question === '/memory') {
2553
+ await showMemoryPanel();
2554
+ chatChromeDirty = true;
2555
+ return await loopQuestion();
2556
+ }
2557
+
2558
+ if (question === '/pin') {
2559
+ await pinCurrentMemory();
2560
+ chatChromeDirty = true;
2561
+ return await loopQuestion();
2562
+ }
2563
+
2564
+ if (question === '/retry-last') {
2565
+ if (!lastSubmittedQuestion) {
2566
+ ui.printInfo('当前还没有可重试的上一条消息。');
2567
+ return await loopQuestion();
2568
+ }
2569
+ ui.printWarning('正在重试上一条消息...');
2570
+ return await submitConversationMessage(lastSubmittedQuestion, {
2571
+ allowRetry: false,
2572
+ skipLog: true
2573
+ });
2574
+ }
2575
+
2576
+ if (question === '/plan') {
2577
+ const plan = sessionManager.current?.memory?.plan || [];
2578
+ ui.printNewline();
2579
+ if (!plan.length) {
2580
+ ui.printInfo('当前还没有提炼出的轻量计划。');
2581
+ } else {
2582
+ ui.printKeyValueTable('当前计划', plan.map((item, index) => ({
2583
+ label: `Step ${index + 1}`,
2584
+ value: item
2585
+ })));
2586
+ }
2587
+ ui.printNewline();
2588
+ chatChromeDirty = true;
2589
+ return await loopQuestion();
2590
+ }
2591
+
2592
+ if (question === '/config') {
2593
+ const beforeProviderId = getProvider().id;
2594
+ await showSettingsMenu({ ui, config });
2595
+ const afterProviderId = getProvider().id;
2596
+ if (afterProviderId !== beforeProviderId) {
2597
+ ui.printInfo(`默认提供方已改为 ${getProvider().name}。当前会话仍使用 ${chatManager?.provider?.name || getProvider().name}。`);
2598
+ ui.printInfo('如需把当前会话一起切过去,请输入 /switch-provider。');
2599
+ }
2600
+ printChatOnboarding();
2601
+ ui.printNewline();
2602
+ return await loopQuestion();
2603
+ }
2604
+
2605
+ if (question === '/task') {
2606
+ await showTaskPanel();
2607
+ chatChromeDirty = true;
2608
+ return await loopQuestion();
2609
+ }
2610
+
2611
+ if (question === '/sessions') {
2612
+ const sessionMeta = await browseSessions({
2613
+ sessionManager,
2614
+ ui
2615
+ });
2616
+ if (sessionMeta) {
2617
+ const resumed = await resumeSession(sessionMeta);
2618
+ if (resumed) {
2619
+ hasActiveSession = true;
2620
+ }
2621
+ } else {
2622
+ chatChromeDirty = true;
2623
+ }
2624
+ return await loopQuestion();
2625
+ }
2626
+
2627
+ if (question === '/resume') {
2628
+ const latest = await resumeLatestSession(sessionManager);
2629
+ if (!latest) {
2630
+ ui.printInfo('没有可恢复的历史会话。');
2631
+ return await loopQuestion();
2632
+ }
2633
+
2634
+ const resumed = await resumeSession(latest);
2635
+ if (resumed) {
2636
+ hasActiveSession = true;
2637
+ } else {
2638
+ chatChromeDirty = true;
2639
+ }
2640
+ return await loopQuestion();
2641
+ }
2642
+
2643
+ if (question === '/rename-session') {
2644
+ await renameCurrentSession();
2645
+ chatChromeDirty = true;
2646
+ return await loopQuestion();
2647
+ }
2648
+
2649
+ if (question === '/delete-session') {
2650
+ const deleted = await deleteCurrentSession();
2651
+ if (deleted) {
2652
+ return 'MENU';
2653
+ }
2654
+ return await loopQuestion();
2655
+ }
2656
+
2657
+ if (question === '/inspect') {
2658
+ ui.printNewline();
2659
+ ui.printWarning('正在检查当前页面适配状态...');
2660
+ const snapshot = await chatManager.inspectSite();
2661
+ printInspectResult(snapshot);
2662
+ chatChromeDirty = true;
2663
+ return await loopQuestion();
2664
+ }
2665
+
2666
+ if (question === '/agent') {
2667
+ ui.printNewline();
2668
+ ui.printWarning('正在检查是否需要人工接管...');
2669
+ const handled = await maybeHandleIntervention('manual', null);
2670
+ if (!handled) {
2671
+ ui.printInfo('当前没有明确的登录或验证阻塞。');
2672
+ ui.printNewline();
2673
+ }
2674
+ chatChromeDirty = true;
2675
+ return await loopQuestion();
2676
+ }
2677
+
2678
+ if (question === '/mode') {
2679
+ agentMode = !agentMode;
2680
+ if (!agentMode) {
2681
+ resetAgentPrimer();
2682
+ }
2683
+ ui.printNewline();
2684
+ ui.printSuccess(`已切换到${agentMode ? ' Agent' : '普通'}模式`);
2685
+ printChatOnboarding();
2686
+ if (agentMode) {
2687
+ printAgentHelp();
2688
+ }
2689
+ return await loopQuestion();
2690
+ }
2691
+
2692
+ if (question === '/switch-provider') {
2693
+ await switchCurrentSessionProvider();
2694
+ chatChromeDirty = true;
2695
+ return await loopQuestion();
2696
+ }
2697
+
2698
+ if (question === '/model') {
2699
+ await openModelSwitcher();
2700
+ printChatOnboarding();
2701
+ ui.printNewline();
2702
+ return await loopQuestion();
2703
+ }
2704
+
2705
+ if (isSlashCommand) {
2706
+ ui.printWarning(`未识别的命令: ${question}`);
2707
+ ui.printMuted('输入 /help 查看可用命令。');
2708
+ return await loopQuestion();
2709
+ }
2710
+
2711
+ ui.printUserMessage(question);
2712
+ return await submitConversationMessage(question);
2713
+
2714
+ } catch (error) {
2715
+ if (error?.message === 'PROVIDER_FALLBACK_RETRY') {
2716
+ return await loopQuestion();
2717
+ }
2718
+
2719
+ if (error.name === 'AbortError' || error.code === 'ERR_USE_AFTER_CLOSE') {
2720
+ if (!isShuttingDown) {
2721
+ process.kill(process.pid, 'SIGINT');
2722
+ }
2723
+ return;
2724
+ }
2725
+
2726
+ ui.stopSpinner();
2727
+ if (!error?.silentReported) {
2728
+ ui.printError(`发生错误: ${error.message}`);
2729
+ }
2730
+
2731
+ await new Promise(resolve => setTimeout(resolve, 1000));
2732
+ return await loopQuestion();
2733
+ }
2734
+ }
2735
+
2736
+ async function retryQuestion(question) {
2737
+ ui.printWarning('人工接管已完成,正在重试当前消息...');
2738
+ return submitConversationMessage(question, { allowRetry: false });
2739
+ }
2740
+
2741
+ async function main() {
2742
+ try {
2743
+ await logger.init();
2744
+ await sessionManager.init();
2745
+ await taskStore.init();
2746
+ const maintenance = await runRuntimeMaintenance();
2747
+ ensureTaskRunner().startLoop();
2748
+
2749
+ setupGracefulShutdown();
2750
+
2751
+ if (maintenance.tempRemoved > 0) {
2752
+ ui.printMuted(`启动清理: 已移除 ${maintenance.tempRemoved} 个临时文件`);
2753
+ }
2754
+ if (maintenance.sessionsArchived > 0 || maintenance.tasksArchived > 0) {
2755
+ ui.printMuted(
2756
+ `启动归档: 会话 ${maintenance.sessionsArchived} 个, 任务 ${maintenance.tasksArchived} 个`
2757
+ );
2758
+ }
2759
+ if (maintenance.profileWarning) {
2760
+ ui.printWarning(
2761
+ `浏览器资料目录约 ${maintenance.profileWarning.sizeMb} MB,已超过提醒阈值 ${maintenance.profileWarning.thresholdMb} MB`
2762
+ );
2763
+ }
2764
+
2765
+ while (true) {
2766
+ const choice = await showStartupMenu();
2767
+
2768
+ if (choice === 'q' || choice === 'Q') {
2769
+ ui.clear();
2770
+ ui.printSuccess('再见!');
2771
+ process.exit(0);
2772
+ }
2773
+
2774
+ if (choice === 'd' || choice === 'D') {
2775
+ showQuickDoc();
2776
+ ui.printInfo('按回车返回主菜单...');
2777
+ await askInput('', true);
2778
+ continue;
2779
+ }
2780
+
2781
+ if (choice === 's' || choice === 'S') {
2782
+ await showSettingsMenu({ ui, config });
2783
+ continue;
2784
+ }
2785
+
2786
+ if (choice === 'c' || choice === 'C') {
2787
+ if (hasActiveSession) {
2788
+ ui.clear();
2789
+ ui.printSuccess('继续聊天...');
2790
+ ui.printNewline();
2791
+ const result = await loopQuestion();
2792
+ if (result === 'MENU') {
2793
+ continue;
2794
+ }
2795
+ } else {
2796
+ ui.printError('没有活跃的会话');
2797
+ await new Promise(resolve => setTimeout(resolve, 1500));
2798
+ continue;
2799
+ }
2800
+ }
2801
+
2802
+ if (choice === '1') {
2803
+ agentMode = Boolean(config.agent?.enabled);
2804
+ resetAgentPrimer();
2805
+ const browserReady = await ensureBrowserReady();
2806
+ if (!browserReady) {
2807
+ continue;
2808
+ }
2809
+
2810
+ const provider = getProvider();
2811
+ const started = await startNewChat(provider);
2812
+ if (!started) {
2813
+ continue;
2814
+ }
2815
+ hasActiveSession = true;
2816
+ if (agentMode) {
2817
+ printAgentHelp();
2818
+ }
2819
+
2820
+ const result = await loopQuestion();
2821
+ if (result === 'MENU') {
2822
+ continue;
2823
+ }
2824
+ break;
2825
+ }
2826
+
2827
+ if (choice === '2') {
2828
+ agentMode = Boolean(config.agent?.enabled);
2829
+ resetAgentPrimer();
2830
+ const sessionMeta = await browseSessions({
2831
+ sessionManager,
2832
+ ui
2833
+ });
2834
+ if (sessionMeta) {
2835
+ const browserReady = await ensureBrowserReady();
2836
+ if (!browserReady) {
2837
+ continue;
2838
+ }
2839
+
2840
+ const resumed = await resumeSession(sessionMeta);
2841
+ if (resumed) {
2842
+ hasActiveSession = true;
2843
+ const result = await loopQuestion();
2844
+ if (result === 'MENU') {
2845
+ continue;
2846
+ }
2847
+ break;
2848
+ }
2849
+ }
2850
+ continue;
2851
+ }
2852
+
2853
+ ui.printError('无效的选择,请从菜单中选择可用项。');
2854
+ ui.printInfo(hasActiveSession ? '可选项: c, 1, 2, s, d, q' : '可选项: 1, 2, s, d, q');
2855
+ await new Promise(resolve => setTimeout(resolve, 1500));
2856
+ }
2857
+
2858
+ } catch (error) {
2859
+ ui.printError(`初始化失败: ${error.message}`);
2860
+ console.error(error.stack);
2861
+ process.exit(1);
2862
+ }
2863
+ }
2864
+
2865
+ main();