@keyou007/agent-bot 0.1.24-alpha.4 → 0.1.24-alpha.6
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 +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- 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/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +238 -127
- 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/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.js.map +1 -1
- 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 +146 -72
- 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 +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- 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 +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -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/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 +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- 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 +159 -72
- 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 +44 -7
- package/src/local-files/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -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 +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -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({
|
|
@@ -160,7 +165,7 @@ let restartRequested = false;
|
|
|
160
165
|
let pendingSelfUpdatePlanPath: string | undefined;
|
|
161
166
|
let preparingSelfUpdate = false;
|
|
162
167
|
const safeRestart = new SafeRestartScheduler({
|
|
163
|
-
readActivity: () =>
|
|
168
|
+
readActivity: () => serverActivity(),
|
|
164
169
|
onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
|
|
165
170
|
? initiateSelfUpdate(reason, notificationTargets)
|
|
166
171
|
: initiateRestart(reason, notificationTargets),
|
|
@@ -226,8 +231,39 @@ async function prepareAndScheduleUpdate(version: string, target: RestartNotifica
|
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
if (feishuOutbound && localFileViewer && config.terminal?.enabled !== false) {
|
|
235
|
+
const server = new TerminalServer({
|
|
236
|
+
stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "terminal"),
|
|
237
|
+
ownerId: () => config.feishu.userOpenId,
|
|
238
|
+
canStart: () => !shuttingDown && !restartRequested && !safeRestart.scheduled && !safeRestart.restarting,
|
|
239
|
+
readJob: (jobId) => shellCommandJobs.readJob(jobId),
|
|
240
|
+
requestApproval: (authorization) => sendTerminalAuthorization(feishuOutbound, authorization),
|
|
241
|
+
updateCard: (messageId, view) => feishuOutbound.updateInteractiveCard(messageId, new CardRenderer().renderTerminalAuthorization(view)),
|
|
242
|
+
logger,
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const publicBaseUrl = await server.start(localFileViewer);
|
|
246
|
+
terminalServer = server;
|
|
247
|
+
logger.info({ publicBaseUrl }, "Interactive terminal service started (owner authorization required).");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await server.close();
|
|
250
|
+
logger.warn({ error }, "Terminal service unavailable; command cards remain usable.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function serverActivity() {
|
|
255
|
+
const activity = store.getServerActivityState();
|
|
256
|
+
const activeTerminals = terminalServer?.activeCount ?? 0;
|
|
257
|
+
return { ...activity, activeTerminals };
|
|
258
|
+
}
|
|
259
|
+
|
|
229
260
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
230
261
|
supervised,
|
|
262
|
+
terminalUrl: (jobId) => terminalServer?.createUrl(jobId),
|
|
263
|
+
authorizeTerminal: (action) => {
|
|
264
|
+
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
265
|
+
return terminalServer.resolveApproval(action);
|
|
266
|
+
},
|
|
231
267
|
showUpdates: (contextKey, userId) => manualUpdates.show(contextKey, userId),
|
|
232
268
|
selectUpdate: (action, replyTarget) => manualUpdates.select(action, replyTarget),
|
|
233
269
|
restart: requestRestart,
|
|
@@ -264,7 +300,7 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
264
300
|
const changed = writeAgentExecutionDefaults(activeConfigPath, agentName, defaults);
|
|
265
301
|
logger.info({ agentName, changed }, "Stored Agent execution defaults.");
|
|
266
302
|
},
|
|
267
|
-
});
|
|
303
|
+
}, undefined, undefined, shellCommandJobs);
|
|
268
304
|
loadTurnDetails = (turnId) => controller.loadTurnDetails(turnId);
|
|
269
305
|
|
|
270
306
|
if (feishuOutbound) {
|
|
@@ -327,7 +363,7 @@ function recoverPendingSelfUpdate(): void {
|
|
|
327
363
|
pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
|
|
328
364
|
notificationTarget,
|
|
329
365
|
{
|
|
330
|
-
restartImmediatelyIfIdle: isServerIdle(
|
|
366
|
+
restartImmediatelyIfIdle: isServerIdle(serverActivity()),
|
|
331
367
|
},
|
|
332
368
|
);
|
|
333
369
|
logger.warn(
|
|
@@ -468,7 +504,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
468
504
|
}),
|
|
469
505
|
safeRestartScheduled: safeRestart.scheduled,
|
|
470
506
|
safeRestartReason: safeRestart.pendingReason,
|
|
471
|
-
activity:
|
|
507
|
+
activity: serverActivity(),
|
|
472
508
|
},
|
|
473
509
|
};
|
|
474
510
|
case "server_restart": {
|
|
@@ -497,7 +533,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
497
533
|
}
|
|
498
534
|
assertSelfUpdatePlanPath(request.planPath, agentBotHome());
|
|
499
535
|
const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
|
|
500
|
-
const restartImmediatelyIfIdle = isServerIdle(
|
|
536
|
+
const restartImmediatelyIfIdle = isServerIdle(serverActivity());
|
|
501
537
|
pendingSelfUpdatePlanPath = request.planPath;
|
|
502
538
|
safeRestart.schedule(request.reason, notificationTarget, { restartImmediatelyIfIdle });
|
|
503
539
|
return {
|
|
@@ -636,6 +672,7 @@ async function shutdown(
|
|
|
636
672
|
outbound.flushAll().catch((error: unknown) => logger.warn({ error }, "Failed to flush presenters.")),
|
|
637
673
|
delay(5_000),
|
|
638
674
|
]);
|
|
675
|
+
await terminalServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close terminals."));
|
|
639
676
|
runtimes.close();
|
|
640
677
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
641
678
|
await localFileViewer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local file viewer."));
|
|
@@ -708,8 +745,8 @@ function inferServerRestartTarget(notificationSessionId?: string): RestartNotifi
|
|
|
708
745
|
return privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
709
746
|
}
|
|
710
747
|
|
|
711
|
-
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number }): boolean {
|
|
712
|
-
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0;
|
|
748
|
+
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number; activeTerminals?: number }): boolean {
|
|
749
|
+
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0 && !activity.activeTerminals;
|
|
713
750
|
}
|
|
714
751
|
|
|
715
752
|
function restartNotificationTargetForSession(
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const MAX_ROWS = 500;
|
|
2
|
+
const MAX_COLUMNS = 50;
|
|
3
|
+
const MAX_CELL_CHARACTERS = 4_000;
|
|
4
|
+
|
|
5
|
+
export interface CsvPreview {
|
|
6
|
+
rows: string[][];
|
|
7
|
+
notices: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function csvSourcePreview(source: string): { text: string; truncated: boolean } {
|
|
11
|
+
const text = source.replace(/\r\n?/gu, "\n");
|
|
12
|
+
let offset = 0;
|
|
13
|
+
for (let line = 0; line < 2_000; line += 1) {
|
|
14
|
+
const newline = text.indexOf("\n", offset);
|
|
15
|
+
if (newline < 0 || newline === text.length - 1) return { text, truncated: false };
|
|
16
|
+
offset = newline + 1;
|
|
17
|
+
}
|
|
18
|
+
return { text: text.slice(0, offset - 1), truncated: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Parse records rather than physical lines: quoted CSV fields may contain newlines.
|
|
22
|
+
export function parseCsvPreview(source: string, sourceTruncated = false): CsvPreview {
|
|
23
|
+
const text = source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
24
|
+
const rows: string[][] = [];
|
|
25
|
+
const notices = new Set<string>();
|
|
26
|
+
let row: string[] = [];
|
|
27
|
+
let field = "";
|
|
28
|
+
let fieldCharacters = 0;
|
|
29
|
+
let state: "start" | "plain" | "quoted" | "closed" = "start";
|
|
30
|
+
let recordStarted = false;
|
|
31
|
+
const append = (character: string): void => {
|
|
32
|
+
if (row.length >= MAX_COLUMNS) return;
|
|
33
|
+
if (fieldCharacters < MAX_CELL_CHARACTERS) { field += character; fieldCharacters += 1; }
|
|
34
|
+
else notices.add(`过长单元格仅显示前 ${MAX_CELL_CHARACTERS} 个字符。`);
|
|
35
|
+
};
|
|
36
|
+
const endField = (): void => {
|
|
37
|
+
if (row.length < MAX_COLUMNS) row.push(field);
|
|
38
|
+
else notices.add(`表格仅显示前 ${MAX_COLUMNS} 列。`);
|
|
39
|
+
field = "";
|
|
40
|
+
fieldCharacters = 0;
|
|
41
|
+
state = "start";
|
|
42
|
+
};
|
|
43
|
+
if (sourceTruncated) notices.add("文件较大,仅读取开头 2 MiB;末尾不完整的记录不显示。可下载完整文件。");
|
|
44
|
+
let invalid = false;
|
|
45
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
46
|
+
const character = String.fromCodePoint(text.codePointAt(index)!);
|
|
47
|
+
if (character.length === 2) index += 1;
|
|
48
|
+
recordStarted = true;
|
|
49
|
+
if (state === "quoted") {
|
|
50
|
+
if (character === '"') state = "closed";
|
|
51
|
+
else append(character);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (character === '"') {
|
|
55
|
+
if (state === "start") state = "quoted";
|
|
56
|
+
else if (state === "closed") { append(character); state = "quoted"; }
|
|
57
|
+
else { invalid = true; break; }
|
|
58
|
+
} else if (character === ",") {
|
|
59
|
+
endField();
|
|
60
|
+
} else if (character === "\r" || character === "\n") {
|
|
61
|
+
if (character === "\r" && text[index + 1] === "\n") index += 1;
|
|
62
|
+
endField();
|
|
63
|
+
rows.push(row);
|
|
64
|
+
row = [];
|
|
65
|
+
recordStarted = false;
|
|
66
|
+
if (rows.length === MAX_ROWS && index + 1 < text.length) {
|
|
67
|
+
notices.add(`表格仅显示前 ${MAX_ROWS} 行。`);
|
|
68
|
+
return { rows, notices: [...notices] };
|
|
69
|
+
}
|
|
70
|
+
} else if (state === "closed") {
|
|
71
|
+
invalid = true;
|
|
72
|
+
break;
|
|
73
|
+
} else {
|
|
74
|
+
state = "plain";
|
|
75
|
+
append(character);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (invalid || (!sourceTruncated && state === "quoted")) {
|
|
79
|
+
notices.add(`第 ${rows.length + 1} 条记录的引号格式不完整或不合法,已停止解析。请切换代码查看原文。`);
|
|
80
|
+
} else if (recordStarted && !sourceTruncated) {
|
|
81
|
+
endField();
|
|
82
|
+
rows.push(row);
|
|
83
|
+
}
|
|
84
|
+
return { rows, notices: [...notices] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderCsvPreview(source: string, sourceTruncated = false): string {
|
|
88
|
+
const { rows, notices } = parseCsvPreview(source, sourceTruncated);
|
|
89
|
+
const columns = Math.max(0, ...rows.map((row) => row.length));
|
|
90
|
+
const noticesHtml = notices.map((notice) => `<div class="notice">${escapeHtml(notice)}</div>`).join("");
|
|
91
|
+
if (!rows.length) {
|
|
92
|
+
return `<section data-view-panel="rendered">${noticesHtml}<div class="csv-summary">${notices.length ? "没有可显示的完整记录。" : "CSV 文件为空。"}</div></section>`;
|
|
93
|
+
}
|
|
94
|
+
const headings = Array.from({ length: columns }, (_, index) => {
|
|
95
|
+
const label = index < 26 ? String.fromCharCode(65 + index) : `A${String.fromCharCode(65 + index - 26)}`;
|
|
96
|
+
return `<th scope="col">${label}</th>`;
|
|
97
|
+
}).join("");
|
|
98
|
+
const body = rows.map((row, index) => {
|
|
99
|
+
const cells = row.map((value) => `<td><div class="csv-cell">${escapeHtml(value)}</div></td>`).join("");
|
|
100
|
+
const missing = columns - row.length;
|
|
101
|
+
return `<tr><th scope="row">${index + 1}</th>${cells}${missing ? `<td colspan="${missing}" class="csv-missing"></td>` : ""}</tr>`;
|
|
102
|
+
}).join("");
|
|
103
|
+
return `<section data-view-panel="rendered">${noticesHtml}<div class="csv-summary">已显示 ${rows.length} 行 · 最多 ${columns} 列 · 首行保留为数据</div><div class="csv-table-scroll" role="region" aria-label="CSV 表格" tabindex="0"><table class="csv-table"><thead><tr><th scope="col" aria-label="行号">#</th>${headings}</tr></thead><tbody>${body}</tbody></table></div></section>`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function escapeHtml(value: string): string {
|
|
107
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const CSV_PREVIEW_CSS = `
|
|
111
|
+
.csv-summary { padding: 10px 12px; color: #646a73; font-size: 12px; }
|
|
112
|
+
.csv-table-scroll { isolation: isolate; max-width: 100%; max-height: calc(100dvh - var(--viewer-header-offset) - 40px); overflow: auto; background: #fff; }
|
|
113
|
+
.csv-table-scroll:focus-visible { outline: 2px solid #1456f0; outline-offset: -2px; }
|
|
114
|
+
.csv-table { width: max-content; min-width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; line-height: 1.5; }
|
|
115
|
+
.csv-table th, .csv-table td { padding: 7px 12px; text-align: left; vertical-align: top; border-right: 1px solid #dfe3e8; border-bottom: 1px solid #dfe3e8; }
|
|
116
|
+
.csv-table th { background: #f0f3f8; font-weight: 600; }
|
|
117
|
+
.csv-table thead th { position: sticky; top: 0; z-index: 1; }
|
|
118
|
+
.csv-table tr > th:first-child { position: sticky; left: 0; text-align: right; min-width: 3.5em; }
|
|
119
|
+
.csv-table thead th:first-child { z-index: 2; }
|
|
120
|
+
.csv-table tbody tr:nth-child(even) td { background: #f9fafc; }
|
|
121
|
+
.csv-table tbody tr:hover td { background: #edf3ff; }
|
|
122
|
+
.csv-cell { min-width: 4em; max-width: 32rem; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
123
|
+
@media (max-width: 640px) { .csv-cell { max-width: 20rem; } }
|
|
124
|
+
@media (prefers-color-scheme: dark) {
|
|
125
|
+
.csv-summary { color: #a6a9ad; }
|
|
126
|
+
.csv-table-scroll { background: #202124; }
|
|
127
|
+
.csv-table th, .csv-table td { border-color: #55585c; }
|
|
128
|
+
.csv-table th { background: #292a2d; }
|
|
129
|
+
.csv-table tbody tr:nth-child(even) td { background: #252629; }
|
|
130
|
+
.csv-table tbody tr:hover td { background: #292f3d; }
|
|
131
|
+
}
|
|
132
|
+
`;
|