@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.7
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/CHANGELOG.md +40 -0
- package/README.md +29 -3
- package/README.zh.md +29 -3
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +1 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +247 -120
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/codex/TraePlanEntry.js +70 -0
- package/dist/codex/TraePlanEntry.js.map +1 -0
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +92 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +57 -7
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +620 -214
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +33 -1
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +22 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalCommandTracker.js +74 -0
- package/dist/terminal/TerminalCommandTracker.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +95 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +676 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +11 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexEventMapper.ts +1 -0
- package/src/codex/CodexRuntime.ts +244 -128
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/TraePlanEntry.ts +63 -0
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +109 -8
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +56 -8
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +629 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +35 -2
- package/src/supervision/SafeRestartScheduler.ts +25 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalCommandTracker.ts +74 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +93 -0
- package/src/terminal/TerminalServer.ts +551 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
appendSteerMessage as appendSteerMessageToState,
|
|
5
5
|
createTurnViewState,
|
|
6
6
|
compactTurnView,
|
|
7
|
+
continueTurnView,
|
|
7
8
|
reduceTurnEvent,
|
|
8
9
|
} from "../presentation/TurnStateReducer.js";
|
|
9
10
|
import { splitMarkdown } from "../presentation/splitMarkdown.js";
|
|
@@ -74,6 +75,7 @@ interface TurnEntry {
|
|
|
74
75
|
elapsedTimer?: ReturnType<typeof setInterval>;
|
|
75
76
|
finalizing?: Promise<void>;
|
|
76
77
|
lastFinalizedStatus?: TurnViewState["status"];
|
|
78
|
+
continuing?: boolean;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export class FeishuTurnPresenter {
|
|
@@ -158,6 +160,13 @@ export class FeishuTurnPresenter {
|
|
|
158
160
|
const existing = this.pendingEntries.get(sessionId);
|
|
159
161
|
if (existing) {
|
|
160
162
|
await existing.initializing;
|
|
163
|
+
if (existing.continuing) {
|
|
164
|
+
existing.state = { ...existing.state, status: "running", error: undefined, completedAt: undefined, durationMs: undefined };
|
|
165
|
+
this.persist(existing.state.turnId, sessionId, existing.state, existing.contextKey);
|
|
166
|
+
existing.scheduler?.update(existing.state, "critical");
|
|
167
|
+
this.startElapsedUpdates(existing);
|
|
168
|
+
return existing.state.turnId;
|
|
169
|
+
}
|
|
161
170
|
const images = localImagePaths === undefined ? existing.state.promptImagePaths : [...new Set(localImagePaths)];
|
|
162
171
|
if ((prompt && existing.state.prompt !== prompt) || JSON.stringify(images) !== JSON.stringify(existing.state.promptImagePaths)) {
|
|
163
172
|
existing.state = { ...existing.state, ...(prompt ? { prompt } : {}), ...(images ? { promptImagePaths: images } : {}) };
|
|
@@ -204,15 +213,15 @@ export class FeishuTurnPresenter {
|
|
|
204
213
|
): Promise<boolean> {
|
|
205
214
|
const entry = this.pendingEntries.get(sessionId);
|
|
206
215
|
if (!entry) return false;
|
|
207
|
-
this.pendingEntries.delete(sessionId);
|
|
208
|
-
this.stopElapsedUpdates(entry);
|
|
216
|
+
if (!entry.continuing) this.pendingEntries.delete(sessionId);
|
|
217
|
+
if (!entry.continuing) this.stopElapsedUpdates(entry);
|
|
209
218
|
const completedAt = Date.now();
|
|
210
219
|
entry.state = {
|
|
211
220
|
...entry.state,
|
|
212
|
-
status: "failed",
|
|
221
|
+
status: entry.continuing ? "running" : "failed",
|
|
213
222
|
error: message,
|
|
214
|
-
completedAt,
|
|
215
|
-
durationMs: Math.max(0, completedAt - entry.state.startedAt),
|
|
223
|
+
completedAt: entry.continuing ? undefined : completedAt,
|
|
224
|
+
durationMs: entry.continuing ? undefined : Math.max(0, completedAt - entry.state.startedAt),
|
|
216
225
|
};
|
|
217
226
|
this.persist(entry.state.turnId, sessionId, entry.state, entry.contextKey);
|
|
218
227
|
await entry.initializing;
|
|
@@ -298,7 +307,7 @@ export class FeishuTurnPresenter {
|
|
|
298
307
|
status: "running" as const,
|
|
299
308
|
};
|
|
300
309
|
this.store.previews?.seed(initial);
|
|
301
|
-
this.store.previews?.user(turnId, activityId, text, localImagePaths);
|
|
310
|
+
this.store.previews?.user(initial.previewTurnId ?? turnId, activityId, text, localImagePaths);
|
|
302
311
|
const state = this.cardState(appendSteerMessageToState(initial, activityId, text, localImagePaths));
|
|
303
312
|
entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
|
|
304
313
|
this.entries.set(turnId, entry);
|
|
@@ -309,7 +318,7 @@ export class FeishuTurnPresenter {
|
|
|
309
318
|
}
|
|
310
319
|
|
|
311
320
|
await entry.initializing;
|
|
312
|
-
this.store.previews?.user(turnId, activityId, text, localImagePaths);
|
|
321
|
+
this.store.previews?.user(entry.state.previewTurnId ?? turnId, activityId, text, localImagePaths);
|
|
313
322
|
entry.state = this.cardState(appendSteerMessageToState(entry.state, activityId, text, localImagePaths));
|
|
314
323
|
this.persist(turnId, sessionId, entry.state, entry.contextKey);
|
|
315
324
|
if (!entry.historySnapshot) entry.scheduler?.update(entry.state, "critical");
|
|
@@ -317,10 +326,50 @@ export class FeishuTurnPresenter {
|
|
|
317
326
|
|
|
318
327
|
async onEvent(event: AgentEvent): Promise<void> {
|
|
319
328
|
let existing = this.entries.get(event.turnId);
|
|
329
|
+
if (!existing) {
|
|
330
|
+
const saved = this.store.getTurnSnapshot(event.turnId);
|
|
331
|
+
if (isTurnViewState(saved) && saved.continuedToTurnId) return;
|
|
332
|
+
}
|
|
320
333
|
let eventApplied = false;
|
|
334
|
+
if (event.type === "turn_completed" && event.approvedPlan) {
|
|
335
|
+
const entry = existing ?? this.createEntry({ type: "progress", sessionId: event.sessionId,
|
|
336
|
+
turnId: event.turnId, activityId: "plan-handoff", text: "方案已确认,继续执行" });
|
|
337
|
+
if (!entry || entry.continuing) return;
|
|
338
|
+
entry.continuing = true;
|
|
339
|
+
entry.historySnapshot = undefined;
|
|
340
|
+
this.store.previews?.seed(entry.state);
|
|
341
|
+
entry.state = { ...entry.state, status: "running", approval: undefined };
|
|
342
|
+
this.pendingEntries.set(event.sessionId, entry);
|
|
343
|
+
this.persist(event.turnId, event.sessionId, entry.state, entry.contextKey);
|
|
344
|
+
await entry.initializing;
|
|
345
|
+
await entry.scheduler?.flush(entry.state);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
321
348
|
if (!existing && event.type === "turn_started") {
|
|
322
349
|
const pending = this.pendingEntries.get(event.sessionId);
|
|
323
|
-
if (pending) {
|
|
350
|
+
if (pending?.continuing) {
|
|
351
|
+
const previous = pending.state;
|
|
352
|
+
const previewTurnId = previous.previewTurnId ?? previous.turnId;
|
|
353
|
+
this.flushPersistence();
|
|
354
|
+
const state = continueTurnView(previous, event.turnId, event.startedAt);
|
|
355
|
+
this.store.previews?.handoff(previewTurnId, event.turnId, event.startedAt);
|
|
356
|
+
this.store.saveTurnSnapshot(previous.turnId, event.sessionId, {
|
|
357
|
+
...previous, status: "completed", completedAt: event.startedAt,
|
|
358
|
+
durationMs: Math.max(0, event.startedAt - previous.startedAt),
|
|
359
|
+
finalResponse: "", continuedToTurnId: event.turnId, previewTurnId,
|
|
360
|
+
}, pending.contextKey);
|
|
361
|
+
if (pending.messageId) this.store.saveTurnDelivery(event.turnId, { progressMessageId: pending.messageId });
|
|
362
|
+
this.entries.delete(previous.turnId);
|
|
363
|
+
this.pendingEntries.delete(event.sessionId);
|
|
364
|
+
pending.state = state;
|
|
365
|
+
pending.continuing = false;
|
|
366
|
+
pending.lastFinalizedStatus = undefined;
|
|
367
|
+
pending.historySnapshot = undefined;
|
|
368
|
+
this.entries.set(event.turnId, pending);
|
|
369
|
+
this.persist(event.turnId, event.sessionId, state, pending.contextKey);
|
|
370
|
+
existing = pending;
|
|
371
|
+
eventApplied = true;
|
|
372
|
+
} else if (pending) {
|
|
324
373
|
const pendingTurnId = pending.state.turnId;
|
|
325
374
|
const state = reduceTurnEvent(
|
|
326
375
|
createTurnViewState(
|
|
@@ -362,8 +411,9 @@ export class FeishuTurnPresenter {
|
|
|
362
411
|
if (!entry) return;
|
|
363
412
|
|
|
364
413
|
if (existing && !eventApplied) {
|
|
414
|
+
event = presentationEvent(event, entry.state);
|
|
365
415
|
this.store.previews?.seed(entry.state);
|
|
366
|
-
this.store.previews?.event(event);
|
|
416
|
+
this.store.previews?.event(event, entry.state.previewTurnId);
|
|
367
417
|
const previousStatus = entry.state.status;
|
|
368
418
|
entry.state = this.cardState(reduceTurnEvent(entry.state, event));
|
|
369
419
|
if (isTerminalViewStatus(previousStatus) && !event.type.startsWith("turn_")) entry.state.status = previousStatus;
|
|
@@ -374,6 +424,8 @@ export class FeishuTurnPresenter {
|
|
|
374
424
|
if (!entry.scheduler) return;
|
|
375
425
|
|
|
376
426
|
if (isTerminalEvent(event)) {
|
|
427
|
+
if (this.pendingEntries.get(event.sessionId) === entry) this.pendingEntries.delete(event.sessionId);
|
|
428
|
+
entry.continuing = false;
|
|
377
429
|
this.stopElapsedUpdates(entry);
|
|
378
430
|
if (entry.finalizing) await entry.finalizing;
|
|
379
431
|
const terminalState = entry.state;
|
|
@@ -394,7 +446,8 @@ export class FeishuTurnPresenter {
|
|
|
394
446
|
}
|
|
395
447
|
|
|
396
448
|
async showDetails(contextKey: string, turnId: string): Promise<void> {
|
|
397
|
-
const
|
|
449
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
450
|
+
const snapshot = this.store.previews?.load(isTurnViewState(saved) ? saved.previewTurnId ?? turnId : turnId) ?? saved;
|
|
398
451
|
if (!isTurnViewState(snapshot)) {
|
|
399
452
|
await this.outbound.sendText(contextKey, "未找到这次执行的详情。可能已被清理。");
|
|
400
453
|
return;
|
|
@@ -408,6 +461,7 @@ export class FeishuTurnPresenter {
|
|
|
408
461
|
page: number | "latest",
|
|
409
462
|
messageId?: string,
|
|
410
463
|
): Promise<void> {
|
|
464
|
+
turnId = this.latestPresentationTurn(turnId, contextKey, messageId);
|
|
411
465
|
const entry = this.entries.get(turnId);
|
|
412
466
|
if (entry) await entry.initializing;
|
|
413
467
|
const ownsProgressCard = entry
|
|
@@ -430,7 +484,7 @@ export class FeishuTurnPresenter {
|
|
|
430
484
|
}
|
|
431
485
|
|
|
432
486
|
if (!entry.historySnapshot) {
|
|
433
|
-
entry.historySnapshot = this.store.previews?.load(turnId) ?? entry.state;
|
|
487
|
+
entry.historySnapshot = this.store.previews?.load(entry.state.previewTurnId ?? turnId) ?? entry.state;
|
|
434
488
|
await entry.scheduler?.flush();
|
|
435
489
|
}
|
|
436
490
|
const card = this.renderActivityHistory(entry.historySnapshot, page);
|
|
@@ -439,7 +493,8 @@ export class FeishuTurnPresenter {
|
|
|
439
493
|
return;
|
|
440
494
|
}
|
|
441
495
|
|
|
442
|
-
const
|
|
496
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
497
|
+
const snapshot = this.store.previews?.load(isTurnViewState(saved) ? saved.previewTurnId ?? turnId : turnId) ?? saved;
|
|
443
498
|
if (!isTurnViewState(snapshot)) throw new Error("未找到这次执行的活动历史。");
|
|
444
499
|
const card = page === "latest"
|
|
445
500
|
? this.renderTurn(snapshot)
|
|
@@ -477,9 +532,9 @@ export class FeishuTurnPresenter {
|
|
|
477
532
|
const compact = compactTurnView(state);
|
|
478
533
|
this.dirty.set(turnId, { state: compact, contextKey });
|
|
479
534
|
if (!event || isTerminalViewStatus(state.status) || state.status === "waiting_for_approval") {
|
|
480
|
-
this.store.previews.flush(turnId, true);
|
|
535
|
+
this.store.previews.flush(state.previewTurnId ?? turnId, true);
|
|
481
536
|
this.flushPersistence();
|
|
482
|
-
if (isTerminalViewStatus(state.status)) this.store.previews.release(turnId);
|
|
537
|
+
if (isTerminalViewStatus(state.status)) this.store.previews.release(state.previewTurnId ?? turnId);
|
|
483
538
|
} else if (!this.persistTimer) {
|
|
484
539
|
this.persistTimer = setTimeout(() => {
|
|
485
540
|
try { this.flushPersistence(); } catch (error) { this.options.onError?.(error); }
|
|
@@ -519,7 +574,8 @@ export class FeishuTurnPresenter {
|
|
|
519
574
|
this.sessionModelProviders.get(event.sessionId),
|
|
520
575
|
);
|
|
521
576
|
this.store.previews?.seed(initial);
|
|
522
|
-
|
|
577
|
+
event = presentationEvent(event, initial);
|
|
578
|
+
this.store.previews?.event(event, initial.previewTurnId);
|
|
523
579
|
const state = this.cardState(reduceTurnEvent(initial, event));
|
|
524
580
|
const entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
|
|
525
581
|
this.entries.set(event.turnId, entry);
|
|
@@ -596,7 +652,21 @@ export class FeishuTurnPresenter {
|
|
|
596
652
|
private turnPreviewUrl(state: TurnViewState): string | undefined {
|
|
597
653
|
return state.turnId.startsWith("pending_")
|
|
598
654
|
? undefined
|
|
599
|
-
: this.options.turnPreviewUrl?.(state.turnId);
|
|
655
|
+
: this.options.turnPreviewUrl?.(state.previewTurnId ?? state.turnId);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private latestPresentationTurn(turnId: string, contextKey: string, messageId?: string): string {
|
|
659
|
+
const seen = new Set<string>();
|
|
660
|
+
while (!seen.has(turnId)) {
|
|
661
|
+
seen.add(turnId);
|
|
662
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
663
|
+
if (!isTurnViewState(saved) || !saved.continuedToTurnId) break;
|
|
664
|
+
const next = saved.continuedToTurnId;
|
|
665
|
+
if (this.store.getTurnContextKey?.(next) !== contextKey
|
|
666
|
+
|| (messageId && this.store.getTurnDelivery(next)?.progressMessageId !== messageId)) break;
|
|
667
|
+
turnId = next;
|
|
668
|
+
}
|
|
669
|
+
return turnId;
|
|
600
670
|
}
|
|
601
671
|
|
|
602
672
|
private startElapsedUpdates(entry: TurnEntry): void {
|
|
@@ -717,6 +787,28 @@ export class FeishuTurnPresenter {
|
|
|
717
787
|
}
|
|
718
788
|
}
|
|
719
789
|
|
|
790
|
+
function presentationEvent(event: AgentEvent, state: TurnViewState): AgentEvent {
|
|
791
|
+
if (event.type === "turn_started" && state.segmentStartedAt !== undefined) return { ...event, startedAt: state.startedAt };
|
|
792
|
+
if (event.type === "turn_completed" && event.durationMs !== undefined && state.segmentStartedAt !== undefined) {
|
|
793
|
+
return { ...event, durationMs: Math.max(0, state.segmentStartedAt - state.startedAt) + event.durationMs };
|
|
794
|
+
}
|
|
795
|
+
if (state.continuedFromTurnId) {
|
|
796
|
+
// Tool/item IDs are only required to be unique within one runtime Turn.
|
|
797
|
+
const scoped = (id: string) => event.turnId + ":" + id;
|
|
798
|
+
if (event.type === "tool_started" || event.type === "tool_updated") return { ...event, tool: { ...event.tool, id: scoped(event.tool.id) } };
|
|
799
|
+
if (event.type === "tool_output_delta") return { ...event, toolId: scoped(event.toolId) };
|
|
800
|
+
if (event.type === "reasoning_delta" || event.type === "reasoning_completed") return { ...event, itemId: scoped(event.itemId) };
|
|
801
|
+
if (event.type === "progress") return { ...event,
|
|
802
|
+
activityId: (event.activityId?.startsWith("commentary:") ? "commentary:" : "") + scoped(event.activityId ?? "progress"),
|
|
803
|
+
...(event.reasoning ? { reasoning: { ...event.reasoning, itemId: scoped(event.reasoning.itemId) } } : {}) };
|
|
804
|
+
if (event.type === "agent_text_delta" && event.replacesActivityId) return {
|
|
805
|
+
...event, replacesActivityId: "commentary:" + scoped(event.replacesActivityId),
|
|
806
|
+
};
|
|
807
|
+
if (event.type === "context_compaction" && event.compactionId) return { ...event, compactionId: scoped(event.compactionId) };
|
|
808
|
+
}
|
|
809
|
+
return event;
|
|
810
|
+
}
|
|
811
|
+
|
|
720
812
|
function normalizeTurnCardMarkdown(
|
|
721
813
|
state: TurnViewState,
|
|
722
814
|
localFileUrl: LocalFileUrlResolver | undefined,
|
package/src/feishu/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { TerminalServer } from "./terminal/TerminalServer.js";
|
|
5
|
+
import { sendTerminalAuthorization } from "./terminal/TerminalCards.js";
|
|
6
|
+
import { ShellCommandJobManager } from "./shell/ShellCommandJobManager.js";
|
|
4
7
|
import { LocalControlServer } from "./cli/LocalControlServer.js";
|
|
5
8
|
import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
|
|
6
9
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
@@ -79,6 +82,8 @@ const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.
|
|
|
79
82
|
const logger = createLogger(config);
|
|
80
83
|
const store = new StateStore(config.storage.sqlitePath, (error) => logger.error({ error }, "Failed to persist Turn preview journal."));
|
|
81
84
|
let localFileViewer: LocalFileViewerServer | undefined;
|
|
85
|
+
let terminalServer: TerminalServer | undefined;
|
|
86
|
+
const shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"));
|
|
82
87
|
let loadTurnDetails: ((turnId: string) => Promise<unknown>) | undefined;
|
|
83
88
|
if (config.fileViewer.enabled) {
|
|
84
89
|
const viewer = new LocalFileViewerServer({
|
|
@@ -140,7 +145,14 @@ if (feishuOutbound) {
|
|
|
140
145
|
workspaceKind: defaultAgent?.kind === "app-server" ? "projectless" : "project",
|
|
141
146
|
defaultUserOpenId: config.feishu.userOpenId,
|
|
142
147
|
}, metadataHydrator);
|
|
143
|
-
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger
|
|
148
|
+
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger, {
|
|
149
|
+
ownerId: () => config.feishu.userOpenId,
|
|
150
|
+
isCurrentSchedule: (scheduleId) => !shuttingDown && !restartRequested && safeRestart.isCurrentSchedule(scheduleId),
|
|
151
|
+
stopTerminal: (terminalId, userId) => {
|
|
152
|
+
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
153
|
+
return terminalServer.stopActiveTerminal(terminalId, userId);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
const consoleEnabled = consoleOnly || config.console.enabled || transport === "console";
|
|
@@ -160,7 +172,7 @@ let restartRequested = false;
|
|
|
160
172
|
let pendingSelfUpdatePlanPath: string | undefined;
|
|
161
173
|
let preparingSelfUpdate = false;
|
|
162
174
|
const safeRestart = new SafeRestartScheduler({
|
|
163
|
-
readActivity: () =>
|
|
175
|
+
readActivity: () => serverActivity(),
|
|
164
176
|
onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
|
|
165
177
|
? initiateSelfUpdate(reason, notificationTargets)
|
|
166
178
|
: initiateRestart(reason, notificationTargets),
|
|
@@ -226,8 +238,43 @@ async function prepareAndScheduleUpdate(version: string, target: RestartNotifica
|
|
|
226
238
|
}
|
|
227
239
|
}
|
|
228
240
|
|
|
241
|
+
if (feishuOutbound && localFileViewer && config.terminal?.enabled !== false) {
|
|
242
|
+
const server = new TerminalServer({
|
|
243
|
+
stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "terminal"),
|
|
244
|
+
ownerId: () => config.feishu.userOpenId,
|
|
245
|
+
canStart: () => !shuttingDown && !restartRequested && !safeRestart.scheduled && !safeRestart.restarting,
|
|
246
|
+
readJob: (jobId) => shellCommandJobs.readJob(jobId),
|
|
247
|
+
requestApproval: (authorization) => sendTerminalAuthorization(feishuOutbound, authorization),
|
|
248
|
+
updateCard: (messageId, view) => feishuOutbound.updateInteractiveCard(messageId, new CardRenderer().renderTerminalAuthorization(view)),
|
|
249
|
+
logger,
|
|
250
|
+
});
|
|
251
|
+
try {
|
|
252
|
+
const publicBaseUrl = await server.start(localFileViewer);
|
|
253
|
+
terminalServer = server;
|
|
254
|
+
logger.info({ publicBaseUrl }, "Interactive terminal service started (owner authorization required).");
|
|
255
|
+
} catch (error) {
|
|
256
|
+
await server.close();
|
|
257
|
+
logger.warn({ error }, "Terminal service unavailable; command cards remain usable.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function serverActivity() {
|
|
262
|
+
const activity = store.getServerActivityState();
|
|
263
|
+
const terminalBlockers = terminalServer?.listActiveTerminals() ?? [];
|
|
264
|
+
return { ...activity, activeTerminals: terminalBlockers.length, terminalBlockers };
|
|
265
|
+
}
|
|
266
|
+
|
|
229
267
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
230
268
|
supervised,
|
|
269
|
+
terminalUrl: (jobId) => terminalServer?.createUrl(jobId),
|
|
270
|
+
authorizeTerminal: (action) => {
|
|
271
|
+
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
272
|
+
return terminalServer.resolveApproval(action);
|
|
273
|
+
},
|
|
274
|
+
stopSafeRestartTerminal: (action) => {
|
|
275
|
+
if (!safeRestartNotifier) throw new Error("安全重启通知未启用。");
|
|
276
|
+
return safeRestartNotifier.stopTerminal(action);
|
|
277
|
+
},
|
|
231
278
|
showUpdates: (contextKey, userId) => manualUpdates.show(contextKey, userId),
|
|
232
279
|
selectUpdate: (action, replyTarget) => manualUpdates.select(action, replyTarget),
|
|
233
280
|
restart: requestRestart,
|
|
@@ -264,7 +311,7 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
264
311
|
const changed = writeAgentExecutionDefaults(activeConfigPath, agentName, defaults);
|
|
265
312
|
logger.info({ agentName, changed }, "Stored Agent execution defaults.");
|
|
266
313
|
},
|
|
267
|
-
});
|
|
314
|
+
}, undefined, undefined, shellCommandJobs);
|
|
268
315
|
loadTurnDetails = (turnId) => controller.loadTurnDetails(turnId);
|
|
269
316
|
|
|
270
317
|
if (feishuOutbound) {
|
|
@@ -327,7 +374,7 @@ function recoverPendingSelfUpdate(): void {
|
|
|
327
374
|
pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
|
|
328
375
|
notificationTarget,
|
|
329
376
|
{
|
|
330
|
-
restartImmediatelyIfIdle: isServerIdle(
|
|
377
|
+
restartImmediatelyIfIdle: isServerIdle(serverActivity()),
|
|
331
378
|
},
|
|
332
379
|
);
|
|
333
380
|
logger.warn(
|
|
@@ -468,7 +515,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
468
515
|
}),
|
|
469
516
|
safeRestartScheduled: safeRestart.scheduled,
|
|
470
517
|
safeRestartReason: safeRestart.pendingReason,
|
|
471
|
-
activity:
|
|
518
|
+
activity: serverActivity(),
|
|
472
519
|
},
|
|
473
520
|
};
|
|
474
521
|
case "server_restart": {
|
|
@@ -497,7 +544,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
497
544
|
}
|
|
498
545
|
assertSelfUpdatePlanPath(request.planPath, agentBotHome());
|
|
499
546
|
const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
|
|
500
|
-
const restartImmediatelyIfIdle = isServerIdle(
|
|
547
|
+
const restartImmediatelyIfIdle = isServerIdle(serverActivity());
|
|
501
548
|
pendingSelfUpdatePlanPath = request.planPath;
|
|
502
549
|
safeRestart.schedule(request.reason, notificationTarget, { restartImmediatelyIfIdle });
|
|
503
550
|
return {
|
|
@@ -636,6 +683,7 @@ async function shutdown(
|
|
|
636
683
|
outbound.flushAll().catch((error: unknown) => logger.warn({ error }, "Failed to flush presenters.")),
|
|
637
684
|
delay(5_000),
|
|
638
685
|
]);
|
|
686
|
+
await terminalServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close terminals."));
|
|
639
687
|
runtimes.close();
|
|
640
688
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
641
689
|
await localFileViewer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local file viewer."));
|
|
@@ -708,8 +756,8 @@ function inferServerRestartTarget(notificationSessionId?: string): RestartNotifi
|
|
|
708
756
|
return privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
709
757
|
}
|
|
710
758
|
|
|
711
|
-
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number }): boolean {
|
|
712
|
-
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0;
|
|
759
|
+
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number; activeTerminals?: number }): boolean {
|
|
760
|
+
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0 && !activity.activeTerminals;
|
|
713
761
|
}
|
|
714
762
|
|
|
715
763
|
function restartNotificationTargetForSession(
|
|
@@ -3,6 +3,7 @@ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import http, { type IncomingMessage, type ServerResponse } from "node:http";
|
|
6
|
+
import type { Duplex } from "node:stream";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import hljs from "highlight.js/lib/common";
|
|
8
9
|
import dos from "highlight.js/lib/languages/dos";
|
|
@@ -283,11 +284,18 @@ export interface LocalFileViewerAddress {
|
|
|
283
284
|
publicConnectionKind?: NetworkConnectionKind;
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
export interface LocalFileViewerService {
|
|
288
|
+
handleRequest(request: IncomingMessage, response: ServerResponse): void;
|
|
289
|
+
handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void;
|
|
290
|
+
close(): Promise<void>;
|
|
291
|
+
}
|
|
292
|
+
|
|
286
293
|
export class LocalFileViewerServer {
|
|
287
294
|
private server?: http.Server;
|
|
288
295
|
private secret?: Buffer;
|
|
289
296
|
private address?: LocalFileViewerAddress;
|
|
290
297
|
private basePath = "";
|
|
298
|
+
private readonly services = new Map<string, LocalFileViewerService>();
|
|
291
299
|
|
|
292
300
|
constructor(private readonly options: LocalFileViewerServerOptions) {}
|
|
293
301
|
|
|
@@ -315,6 +323,11 @@ export class LocalFileViewerServer {
|
|
|
315
323
|
});
|
|
316
324
|
});
|
|
317
325
|
server.on("clientError", (_error, socket) => socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"));
|
|
326
|
+
server.on("upgrade", (request, socket, head) => {
|
|
327
|
+
const service = this.serviceFor(request);
|
|
328
|
+
if (service) service.handleUpgrade(request, socket, head);
|
|
329
|
+
else socket.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
330
|
+
});
|
|
318
331
|
|
|
319
332
|
let selectedPort = preferredPort;
|
|
320
333
|
try {
|
|
@@ -361,12 +374,36 @@ export class LocalFileViewerServer {
|
|
|
361
374
|
}
|
|
362
375
|
|
|
363
376
|
async close(): Promise<void> {
|
|
377
|
+
await Promise.all([...this.services.values()].map((service) => service.close()));
|
|
378
|
+
this.services.clear();
|
|
364
379
|
const server = this.server;
|
|
365
380
|
this.server = undefined;
|
|
366
381
|
this.address = undefined;
|
|
367
382
|
if (server) await closeServer(server);
|
|
368
383
|
}
|
|
369
384
|
|
|
385
|
+
mountService(name: string, service: LocalFileViewerService): { publicBaseUrl: string; unmount(): void } {
|
|
386
|
+
if (!this.address || !this.server) throw new Error("Preview server is not running.");
|
|
387
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(name) || ["preview", "turn-preview", "assets", "view", "raw"].includes(name)) {
|
|
388
|
+
throw new Error("Invalid Preview service route.");
|
|
389
|
+
}
|
|
390
|
+
const basePath = `${this.basePath}/${name}`;
|
|
391
|
+
if (this.services.has(basePath)) throw new Error("Preview service route is already mounted.");
|
|
392
|
+
this.services.set(basePath, service);
|
|
393
|
+
const url = new URL(this.address.publicBaseUrl);
|
|
394
|
+
url.pathname = basePath;
|
|
395
|
+
return { publicBaseUrl: url.toString(), unmount: () => {
|
|
396
|
+
if (this.services.get(basePath) === service) this.services.delete(basePath);
|
|
397
|
+
} };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private serviceFor(request: IncomingMessage): LocalFileViewerService | undefined {
|
|
401
|
+
try {
|
|
402
|
+
const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
|
|
403
|
+
return [...this.services].find(([base]) => pathname === base || pathname.startsWith(`${base}/`))?.[1];
|
|
404
|
+
} catch { return undefined; }
|
|
405
|
+
}
|
|
406
|
+
|
|
370
407
|
createFileUrl(filePath: string, reference?: string): string | undefined {
|
|
371
408
|
if (!this.address || !this.secret) return undefined;
|
|
372
409
|
const absolutePath = normalizeAbsolutePath(filePath);
|
|
@@ -397,6 +434,8 @@ export class LocalFileViewerServer {
|
|
|
397
434
|
}
|
|
398
435
|
|
|
399
436
|
private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
437
|
+
const service = this.serviceFor(request);
|
|
438
|
+
if (service) { service.handleRequest(request, response); return; }
|
|
400
439
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
401
440
|
response.setHeader("Allow", "GET, HEAD");
|
|
402
441
|
this.sendHtml(response, 405, errorPage("不支持的请求", "该服务只接受只读请求。"), request.method === "HEAD");
|
|
@@ -433,14 +472,16 @@ export class LocalFileViewerServer {
|
|
|
433
472
|
return;
|
|
434
473
|
}
|
|
435
474
|
const journal = this.options.previewJournal;
|
|
436
|
-
const
|
|
475
|
+
const stored = journal?.has(turnId) ? undefined : this.options.getTurnSnapshot?.(turnId);
|
|
476
|
+
const journalId = isTurnPreviewState(stored) ? stored.previewTurnId ?? turnId : turnId;
|
|
477
|
+
const recorded = journal?.has(journalId);
|
|
437
478
|
if (recorded && requestUrl.searchParams.get("events") === "1") {
|
|
438
|
-
this.serveJournalEvents(request, response,
|
|
479
|
+
this.serveJournalEvents(request, response, journalId, journal!);
|
|
439
480
|
return;
|
|
440
481
|
}
|
|
441
482
|
const detailKey = requestUrl.searchParams.get("detail");
|
|
442
483
|
let snapshot = recorded
|
|
443
|
-
? journal!.load(
|
|
484
|
+
? journal!.load(journalId, detailKey ?? false)
|
|
444
485
|
: this.options.getTurnSnapshot?.(turnId);
|
|
445
486
|
if (!recorded && this.options.loadTurnSnapshot && request.method !== "HEAD"
|
|
446
487
|
&& requestUrl.searchParams.get("events") !== "1" && !requestUrl.searchParams.has("detail")) {
|
|
@@ -467,7 +508,7 @@ export class LocalFileViewerServer {
|
|
|
467
508
|
const after = requestUrl.searchParams.get("detailAfter");
|
|
468
509
|
const outputAppend = recorded && after && tool?.kind === "tool" && tool.tool.kind === "command" && !tool.tool.error
|
|
469
510
|
&& !/repl/i.test(tool.tool.title) && snapshot.previewCursor !== undefined
|
|
470
|
-
? journal!.outputAppend(
|
|
511
|
+
? journal!.outputAppend(journalId, tool.id, Number(after), snapshot.previewCursor) : undefined;
|
|
471
512
|
const detail = outputAppend !== undefined && tool?.kind === "tool"
|
|
472
513
|
&& (snapshot.fullToolOutputs?.[tool.id]?.length ?? 0) > outputAppend.length
|
|
473
514
|
? { key, revision: String(tool.tool.previewRevision), content: "", outputAppend,
|
|
@@ -965,6 +1006,9 @@ export class LocalFileViewerServer {
|
|
|
965
1006
|
if (renderHtml) {
|
|
966
1007
|
// A response-level sandbox also isolates this document when opened outside the viewer iframe.
|
|
967
1008
|
response.setHeader("Content-Security-Policy", "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; font-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
|
|
1009
|
+
} else {
|
|
1010
|
+
// Raw HTML/SVG/XML must not gain the shared origin's storage or execution privileges.
|
|
1011
|
+
response.setHeader("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data: blob:; media-src data: blob:; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
|
|
968
1012
|
}
|
|
969
1013
|
response.setHeader("Accept-Ranges", "bytes");
|
|
970
1014
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { approvalActivityId } from "../presentation/TurnStateReducer.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { enableDiagramFences, DIAGRAM_PREVIEW_CLIENT_SCRIPT, DIAGRAM_PREVIEW_CSS } from "./DiagramPreview.js";
|
|
4
5
|
import MarkdownIt from "markdown-it";
|
|
@@ -693,7 +694,10 @@ function renderApprovalNotice(request: ApprovalRequest, renderText: (value: stri
|
|
|
693
694
|
: `<div class="markdown">${renderText(reason)}</div>`;
|
|
694
695
|
const details = commandApproval
|
|
695
696
|
? `<details class="approval-details" data-activity-id="approval:${escapeAttribute(request.id)}"><summary>${language === "zh" ? "查看完整命令" : "View full command"}</summary><pre data-scroll-id="approval:${escapeAttribute(request.id)}:command">${escapeHtml(request.command!)}</pre></details>` : "";
|
|
696
|
-
|
|
697
|
+
const instruction = request.automatic
|
|
698
|
+
? language === "zh" ? "自动执行模式:正在自动同意。" : "Auto mode: approving automatically."
|
|
699
|
+
: language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.";
|
|
700
|
+
return `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(title)}</h2>${description}${details}<p>${instruction}</p></section>`;
|
|
697
701
|
}
|
|
698
702
|
|
|
699
703
|
function renderFinalAnswer(markdown: string, turnId: string, renderText: (value: string) => string, language: TurnPreviewLanguage): string {
|
|
@@ -726,7 +730,8 @@ function renderTimeline(
|
|
|
726
730
|
projectCwd?: string,
|
|
727
731
|
deferDetails = false,
|
|
728
732
|
): string {
|
|
729
|
-
const activities = state.
|
|
733
|
+
const activities = state.approval
|
|
734
|
+
? state.activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : state.activities ?? [];
|
|
730
735
|
const positions = new Map(activities.map((activity, index) => [activity.id, index]));
|
|
731
736
|
const reasoning = new Map<number, TurnReasoningItem[]>();
|
|
732
737
|
for (const item of turnReasoningItems(state)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentEvent, RemoteTurnDetails, ToolState } from "../runtime/types.js";
|
|
1
|
+
import type { AgentEvent, ApprovalDecision, ApprovalRequest, RemoteTurnDetails, ToolState } from "../runtime/types.js";
|
|
2
2
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
3
3
|
import type { MessageReplyTarget } from "../feishu/types.js";
|
|
4
4
|
import { formatStorageSize } from "../utils/formatStorageSize.js";
|
|
@@ -113,6 +113,13 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent, complet
|
|
|
113
113
|
if (event.type === "turn_completed") next.finalResponse = event.finalResponse;
|
|
114
114
|
if (event.type === "agent_text_delta") next.assistantText = state.assistantText + event.text;
|
|
115
115
|
if (event.type === "plan_updated") next.plan = event.steps;
|
|
116
|
+
if (event.type === "approval_requested" || event.type === "approval_resolved") {
|
|
117
|
+
const request = event.type === "approval_requested" ? event.request
|
|
118
|
+
: state.approval?.id === event.requestId ? state.approval : undefined;
|
|
119
|
+
if (request) next.activities = next.activities.map((a) => a.id === approvalActivityId(event.turnId, request.id) && a.kind === "assistant"
|
|
120
|
+
? { ...a, text: approvalHistoryText(request, event.type === "approval_resolved" ? event.decision : undefined,
|
|
121
|
+
event.type === "approval_resolved" && event.automatic) } : a);
|
|
122
|
+
}
|
|
116
123
|
if (event.type === "progress") {
|
|
117
124
|
const id = event.activityId ?? "progress";
|
|
118
125
|
const old = state.activities.find((a) => a.id === id);
|
|
@@ -305,7 +312,7 @@ function reduceEvent(state: TurnViewState, event: AgentEvent): TurnViewState {
|
|
|
305
312
|
return {
|
|
306
313
|
...state,
|
|
307
314
|
...tracking,
|
|
308
|
-
status: state.approval ? "waiting_for_approval" : "tool_running",
|
|
315
|
+
status: state.approval && !state.approval.automatic ? "waiting_for_approval" : "tool_running",
|
|
309
316
|
activeTool: bounded,
|
|
310
317
|
activities: activityUpdate.activities,
|
|
311
318
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -317,10 +324,21 @@ function reduceEvent(state: TurnViewState, event: AgentEvent): TurnViewState {
|
|
|
317
324
|
case "tool_output_delta":
|
|
318
325
|
return reduceToolOutputDelta(state, event.toolId, event.delta);
|
|
319
326
|
case "approval_requested":
|
|
320
|
-
return { ...state, status: "waiting_for_approval", approval: event.request
|
|
327
|
+
return { ...state, status: event.request.automatic ? state.activeTool ? "tool_running" : "running" : "waiting_for_approval", approval: event.request,
|
|
328
|
+
activities: [...state.activities.filter((a) => a.id !== approvalActivityId(event.turnId, event.request.id)), {
|
|
329
|
+
kind: "assistant", id: approvalActivityId(event.turnId, event.request.id), text: bound(approvalHistoryText(event.request)),
|
|
330
|
+
}],
|
|
331
|
+
};
|
|
321
332
|
case "approval_resolved":
|
|
322
333
|
if (state.approval?.id !== event.requestId) return state;
|
|
323
|
-
return {
|
|
334
|
+
return {
|
|
335
|
+
...state, status: state.activeTool ? "tool_running" : "running", approval: undefined,
|
|
336
|
+
activities: upsertActivity(state.activities,
|
|
337
|
+
state.activities.findIndex((a) => a.id === approvalActivityId(event.turnId, event.requestId)), {
|
|
338
|
+
kind: "assistant", id: approvalActivityId(event.turnId, event.requestId),
|
|
339
|
+
text: bound(approvalHistoryText(state.approval, event.decision, event.automatic)),
|
|
340
|
+
}).activities,
|
|
341
|
+
};
|
|
324
342
|
case "turn_completed": {
|
|
325
343
|
const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
|
|
326
344
|
return {
|
|
@@ -363,6 +381,27 @@ function reduceEvent(state: TurnViewState, event: AgentEvent): TurnViewState {
|
|
|
363
381
|
}
|
|
364
382
|
}
|
|
365
383
|
|
|
384
|
+
export function approvalActivityId(turnId: string, requestId: string): string {
|
|
385
|
+
return "approval:" + turnId + ":" + requestId;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function continueTurnView(state: TurnViewState, turnId: string, startedAt: number): TurnViewState {
|
|
389
|
+
return { ...state, turnId, previewTurnId: state.previewTurnId ?? state.turnId,
|
|
390
|
+
continuedFromTurnId: state.turnId, continuedToTurnId: undefined, segmentStartedAt: startedAt,
|
|
391
|
+
status: "running", completedAt: undefined, durationMs: undefined, finalResponse: undefined,
|
|
392
|
+
error: undefined, approval: undefined, activeTool: undefined, assistantText: "", progressText: undefined };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function approvalHistoryText(request: ApprovalRequest, decision?: ApprovalDecision, automatic = false): string {
|
|
396
|
+
const labels = { accept: "允许一次", acceptForSession: "本会话允许", decline: "拒绝", cancel: "取消请求" };
|
|
397
|
+
const choice = decision ? request.options.find((option) => option.id === decision)?.label ?? labels[decision] : undefined;
|
|
398
|
+
const heading = choice ? automatic ? "自动同意(auto)" : "用户选择:" + choice
|
|
399
|
+
: request.automatic ? "正在自动同意(auto)" : "等待选择";
|
|
400
|
+
return ["**审批请求 · " + heading + "**", request.title, request.reason,
|
|
401
|
+
request.command ? "~~~\n" + request.command + "\n~~~" : undefined,
|
|
402
|
+
].filter(Boolean).join("\n\n");
|
|
403
|
+
}
|
|
404
|
+
|
|
366
405
|
export function appendSteerMessage(state: TurnViewState, id: string, text: string, localImagePaths?: string[]): TurnViewState {
|
|
367
406
|
const normalized = text.trim();
|
|
368
407
|
if (!normalized && !localImagePaths?.length) return state;
|
|
@@ -442,7 +481,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
442
481
|
...tracking,
|
|
443
482
|
fullToolOutputs,
|
|
444
483
|
fullToolErrors,
|
|
445
|
-
status: state.approval ? "waiting_for_approval" : "tool_running",
|
|
484
|
+
status: state.approval && !state.approval.automatic ? "waiting_for_approval" : "tool_running",
|
|
446
485
|
activeTool: bounded,
|
|
447
486
|
activities: activityUpdate.activities,
|
|
448
487
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -458,7 +497,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
458
497
|
...tracking,
|
|
459
498
|
fullToolOutputs,
|
|
460
499
|
fullToolErrors,
|
|
461
|
-
status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
500
|
+
status: state.approval && !state.approval.automatic ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
462
501
|
activeTool,
|
|
463
502
|
activities: activityUpdate.activities,
|
|
464
503
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -473,7 +512,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
473
512
|
...tracking,
|
|
474
513
|
fullToolOutputs,
|
|
475
514
|
fullToolErrors,
|
|
476
|
-
status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
515
|
+
status: state.approval && !state.approval.automatic ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
477
516
|
activeTool,
|
|
478
517
|
activities: activityUpdate.activities,
|
|
479
518
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|