@keyou007/agent-bot 0.1.17-alpha.1 → 0.1.17
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 +12 -0
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/config.example.yaml +12 -0
- package/dist/config/loadConfig.js +10 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +6 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +42 -13
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +33 -10
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/LocalImageMarkdown.js +16 -2
- package/dist/feishu/LocalImageMarkdown.js.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +1064 -0
- package/dist/local-files/LocalFileViewerServer.js.map +1 -0
- package/dist/local-files/NetworkAddressSelector.js +97 -0
- package/dist/local-files/NetworkAddressSelector.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +81 -34
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +7 -4
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +15 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/docs/technical-reference.md +10 -0
- package/docs/technical-reference.zh.md +10 -0
- package/npm-shrinkwrap.json +12 -2
- package/package.json +2 -1
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/config/loadConfig.ts +10 -0
- package/src/config/schema.ts +9 -0
- package/src/feishu/CardRenderer.ts +6 -22
- package/src/feishu/FeishuMarkdown.ts +51 -12
- package/src/feishu/FeishuTurnPresenter.ts +41 -11
- package/src/feishu/LocalImageMarkdown.ts +23 -2
- package/src/index.ts +33 -4
- package/src/local-files/LocalFileViewerServer.ts +1142 -0
- package/src/local-files/NetworkAddressSelector.ts +111 -0
- package/src/proxy/ProxySessionController.ts +75 -24
- package/src/startup/StartupNotifier.ts +9 -4
- package/src/supervision/SafeRestartScheduler.ts +23 -2
|
@@ -8,7 +8,7 @@ import { splitMarkdown } from "../presentation/splitMarkdown.js";
|
|
|
8
8
|
import type { TurnViewState } from "../presentation/turnViewTypes.js";
|
|
9
9
|
import { CardRenderer } from "./CardRenderer.js";
|
|
10
10
|
import { CardUpdateScheduler } from "./CardUpdateScheduler.js";
|
|
11
|
-
import { normalizeFeishuMarkdown } from "./FeishuMarkdown.js";
|
|
11
|
+
import { normalizeFeishuMarkdown, type LocalFileUrlResolver } from "./FeishuMarkdown.js";
|
|
12
12
|
import type { FeishuOutbound, MessageReplyTarget } from "./types.js";
|
|
13
13
|
import {
|
|
14
14
|
FINAL_RESPONSE_BRANDING_BLOCK,
|
|
@@ -50,6 +50,7 @@ export interface FeishuTurnPresenterOptions {
|
|
|
50
50
|
finalChunkLength?: number;
|
|
51
51
|
onError?: (error: unknown) => void;
|
|
52
52
|
finalRetryBackoffMs?: number[];
|
|
53
|
+
localFileUrl?: LocalFileUrlResolver;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const MAX_FINAL_TABLES_PER_CARD = 5;
|
|
@@ -215,7 +216,7 @@ export class FeishuTurnPresenter {
|
|
|
215
216
|
}
|
|
216
217
|
const delivery = this.store.getTurnDelivery(turnId);
|
|
217
218
|
if (delivery?.progressMessageId) {
|
|
218
|
-
await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.
|
|
219
|
+
await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderTurn(state));
|
|
219
220
|
}
|
|
220
221
|
}
|
|
221
222
|
|
|
@@ -332,7 +333,7 @@ export class FeishuTurnPresenter {
|
|
|
332
333
|
await this.outbound.sendText(contextKey, "未找到这次执行的详情。可能已被清理。");
|
|
333
334
|
return;
|
|
334
335
|
}
|
|
335
|
-
await this.outbound.sendInteractiveCard(contextKey, this.
|
|
336
|
+
await this.outbound.sendInteractiveCard(contextKey, this.renderTurnDetails(snapshot));
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
async showActivityPage(
|
|
@@ -355,9 +356,9 @@ export class FeishuTurnPresenter {
|
|
|
355
356
|
entry.scheduler.invalidateRenderedCard();
|
|
356
357
|
await entry.scheduler.flush(entry.state);
|
|
357
358
|
} else if (targetMessageId) {
|
|
358
|
-
await this.outbound.updateInteractiveCard(targetMessageId, this.
|
|
359
|
+
await this.outbound.updateInteractiveCard(targetMessageId, this.renderTurn(entry.state));
|
|
359
360
|
} else {
|
|
360
|
-
await this.outbound.sendInteractiveCard(contextKey, this.
|
|
361
|
+
await this.outbound.sendInteractiveCard(contextKey, this.renderTurn(entry.state));
|
|
361
362
|
}
|
|
362
363
|
return;
|
|
363
364
|
}
|
|
@@ -366,7 +367,7 @@ export class FeishuTurnPresenter {
|
|
|
366
367
|
entry.historySnapshot = entry.state;
|
|
367
368
|
await entry.scheduler?.flush();
|
|
368
369
|
}
|
|
369
|
-
const card = this.
|
|
370
|
+
const card = this.renderActivityHistory(entry.historySnapshot, page);
|
|
370
371
|
if (targetMessageId) await this.outbound.updateInteractiveCard(targetMessageId, card);
|
|
371
372
|
else await this.outbound.sendInteractiveCard(contextKey, card);
|
|
372
373
|
return;
|
|
@@ -375,8 +376,8 @@ export class FeishuTurnPresenter {
|
|
|
375
376
|
const snapshot = this.store.getTurnSnapshot(turnId);
|
|
376
377
|
if (!isTurnViewState(snapshot)) throw new Error("未找到这次执行的活动历史。");
|
|
377
378
|
const card = page === "latest"
|
|
378
|
-
? this.
|
|
379
|
-
: this.
|
|
379
|
+
? this.renderTurn(snapshot)
|
|
380
|
+
: this.renderActivityHistory(snapshot, page);
|
|
380
381
|
if (messageId) await this.outbound.updateInteractiveCard(messageId, card);
|
|
381
382
|
else await this.outbound.sendInteractiveCard(contextKey, card);
|
|
382
383
|
}
|
|
@@ -428,7 +429,7 @@ export class FeishuTurnPresenter {
|
|
|
428
429
|
|
|
429
430
|
private async initializeEntry(entry: TurnEntry): Promise<void> {
|
|
430
431
|
const sentState = entry.state;
|
|
431
|
-
const card = this.
|
|
432
|
+
const card = this.renderTurn(sentState);
|
|
432
433
|
const delivery = this.store.getTurnDelivery(sentState.turnId);
|
|
433
434
|
const persistedMessageId = delivery?.progressMessageId;
|
|
434
435
|
if (delivery?.finalDelivered && !persistedMessageId) return;
|
|
@@ -449,7 +450,7 @@ export class FeishuTurnPresenter {
|
|
|
449
450
|
if (!messageId) return;
|
|
450
451
|
this.store.saveTurnDelivery(entry.state.turnId, { progressMessageId: messageId });
|
|
451
452
|
entry.scheduler = new CardUpdateScheduler<TurnViewState>({
|
|
452
|
-
render: (state) => this.
|
|
453
|
+
render: (state) => this.renderTurn(state),
|
|
453
454
|
write: (card) => this.outbound.updateInteractiveCard(messageId, card),
|
|
454
455
|
normalIntervalMs: this.options.normalIntervalMs,
|
|
455
456
|
criticalGapMs: this.options.criticalGapMs,
|
|
@@ -461,6 +462,18 @@ export class FeishuTurnPresenter {
|
|
|
461
462
|
await this.syncThinkingCardReaction(entry);
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
private renderTurn(state: TurnViewState): Record<string, unknown> {
|
|
466
|
+
return this.renderer.renderTurn(normalizeTurnCardMarkdown(state, this.options.localFileUrl));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private renderTurnDetails(state: TurnViewState): Record<string, unknown> {
|
|
470
|
+
return this.renderer.renderTurnDetails(normalizeTurnCardMarkdown(state, this.options.localFileUrl));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private renderActivityHistory(state: TurnViewState, page: number): Record<string, unknown> {
|
|
474
|
+
return this.renderer.renderActivityHistory(normalizeTurnCardMarkdown(state, this.options.localFileUrl), page);
|
|
475
|
+
}
|
|
476
|
+
|
|
464
477
|
private startElapsedUpdates(entry: TurnEntry): void {
|
|
465
478
|
if (entry.elapsedTimer || isTerminalViewStatus(entry.state.status)) return;
|
|
466
479
|
const intervalMs = Math.max(1, this.options.elapsedUpdateIntervalMs ?? DEFAULT_ELAPSED_UPDATE_INTERVAL_MS);
|
|
@@ -539,7 +552,7 @@ export class FeishuTurnPresenter {
|
|
|
539
552
|
if (delivery?.finalDelivered || !state.finalResponse) return;
|
|
540
553
|
const finalChunkLength = this.options.finalChunkLength ?? 4_000;
|
|
541
554
|
const chunks = brandedFinalChunks(
|
|
542
|
-
normalizeFeishuMarkdown(state.finalResponse, state.projectCwd),
|
|
555
|
+
normalizeFeishuMarkdown(state.finalResponse, state.projectCwd, this.options.localFileUrl),
|
|
543
556
|
finalChunkLength,
|
|
544
557
|
);
|
|
545
558
|
const messageIds = [...(delivery?.finalMessageIds ?? [])];
|
|
@@ -579,6 +592,23 @@ export class FeishuTurnPresenter {
|
|
|
579
592
|
}
|
|
580
593
|
}
|
|
581
594
|
|
|
595
|
+
function normalizeTurnCardMarkdown(
|
|
596
|
+
state: TurnViewState,
|
|
597
|
+
localFileUrl: LocalFileUrlResolver | undefined,
|
|
598
|
+
): TurnViewState {
|
|
599
|
+
if (!localFileUrl) return state;
|
|
600
|
+
const normalize = (value: string): string => normalizeFeishuMarkdown(value, state.projectCwd, localFileUrl);
|
|
601
|
+
return {
|
|
602
|
+
...state,
|
|
603
|
+
assistantText: normalize(state.assistantText),
|
|
604
|
+
...(state.progressText !== undefined ? { progressText: normalize(state.progressText) } : {}),
|
|
605
|
+
plan: state.plan.map((step) => ({ ...step, text: normalize(step.text) })),
|
|
606
|
+
activities: state.activities.map((activity) => activity.kind === "tool"
|
|
607
|
+
? activity
|
|
608
|
+
: { ...activity, text: normalize(activity.text) }),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
582
612
|
function brandedFinalChunks(response: string, maxLength: number): string[] {
|
|
583
613
|
const chunks = splitMarkdown(response, maxLength, MAX_FINAL_TABLES_PER_CARD);
|
|
584
614
|
if (chunks.length <= 1 && response.length <= FINAL_RESPONSE_BRANDING_THRESHOLD) return chunks;
|
|
@@ -29,7 +29,7 @@ export async function renderMarkdownWithLocalImages(
|
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
appendMarkdown(elements, markdown
|
|
32
|
+
appendMarkdown(elements, markdownBeforeLocalImage(markdown, cursor, matchIndex, matchIndex + match[0].length));
|
|
33
33
|
try {
|
|
34
34
|
let upload = uploads.get(filePath);
|
|
35
35
|
if (!upload) {
|
|
@@ -86,7 +86,28 @@ function unsupportedImageFallback(label: string, rawTarget: string): string {
|
|
|
86
86
|
: `图片不可用:${label || "本地图片"}`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function markdownBeforeLocalImage(
|
|
90
|
+
markdown: string,
|
|
91
|
+
cursor: number,
|
|
92
|
+
matchStart: number,
|
|
93
|
+
matchEnd: number,
|
|
94
|
+
): string {
|
|
95
|
+
const lineStart = markdown.lastIndexOf("\n", matchStart - 1) + 1;
|
|
96
|
+
if (lineStart < cursor) return markdown.slice(cursor, matchStart);
|
|
97
|
+
|
|
98
|
+
const lineEndIndex = markdown.indexOf("\n", matchEnd);
|
|
99
|
+
const lineEnd = lineEndIndex === -1 ? markdown.length : lineEndIndex;
|
|
100
|
+
const beforeLink = markdown.slice(lineStart, matchStart);
|
|
101
|
+
const afterLink = markdown.slice(matchEnd, lineEnd);
|
|
102
|
+
const standaloneListMarker = /^[ \t]*(?:[-+*]|\d+[.)])[ \t]+$/u.test(beforeLink);
|
|
103
|
+
if (!standaloneListMarker || !/^[ \t]*\r?$/u.test(afterLink)) {
|
|
104
|
+
return markdown.slice(cursor, matchStart);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return markdown.slice(cursor, lineStart);
|
|
108
|
+
}
|
|
109
|
+
|
|
89
110
|
function appendMarkdown(elements: Array<Record<string, unknown>>, content: string): void {
|
|
90
|
-
if (!content) return;
|
|
111
|
+
if (!content.trim()) return;
|
|
91
112
|
elements.push({ tag: "markdown", content });
|
|
92
113
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
|
44
44
|
import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
|
|
45
45
|
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
46
46
|
import { privateRestartNotificationTarget } from "./supervision/restartNotificationTarget.js";
|
|
47
|
+
import { LocalFileViewerServer } from "./local-files/LocalFileViewerServer.js";
|
|
47
48
|
import {
|
|
48
49
|
nodeDiagnosticReportArguments,
|
|
49
50
|
prepareCrashReportDirectory,
|
|
@@ -72,6 +73,22 @@ const config = loadConfig();
|
|
|
72
73
|
const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
|
|
73
74
|
const logger = createLogger(config);
|
|
74
75
|
const store = new StateStore(config.storage.sqlitePath);
|
|
76
|
+
let localFileViewer: LocalFileViewerServer | undefined;
|
|
77
|
+
if (config.fileViewer.enabled) {
|
|
78
|
+
const viewer = new LocalFileViewerServer({
|
|
79
|
+
host: config.fileViewer.host,
|
|
80
|
+
port: config.fileViewer.port,
|
|
81
|
+
publicBaseUrl: config.fileViewer.publicBaseUrl,
|
|
82
|
+
stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "file-viewer"),
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const address = await viewer.start();
|
|
86
|
+
localFileViewer = viewer;
|
|
87
|
+
logger.info(address, "Local file viewer started.");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
logger.warn({ error }, "Local file viewer failed to start; local file links will remain plain text.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
75
92
|
|
|
76
93
|
const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
|
|
77
94
|
const activeConfigPath = configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath();
|
|
@@ -98,6 +115,7 @@ if (feishuOutbound) {
|
|
|
98
115
|
normalIntervalMs: 2_000,
|
|
99
116
|
criticalGapMs: 500,
|
|
100
117
|
onError: (error) => logger.warn({ error }, "Failed to update Agent progress card."),
|
|
118
|
+
localFileUrl: (filePath, reference) => localFileViewer?.createFileUrl(filePath, reference),
|
|
101
119
|
});
|
|
102
120
|
routes.push({ matches: (contextKey) => !contextKey.startsWith("console:"), outbound: feishuOutbound, presenter });
|
|
103
121
|
const defaultAgentName = config.defaults.agent!;
|
|
@@ -215,7 +233,7 @@ function recoverPendingSelfUpdate(): void {
|
|
|
215
233
|
if (!pending || pendingSelfUpdatePlanPath || restartRequested || safeRestart.scheduled) return;
|
|
216
234
|
let notificationTarget: RestartNotificationTarget | undefined;
|
|
217
235
|
try {
|
|
218
|
-
notificationTarget =
|
|
236
|
+
notificationTarget = inferServerRestartTarget(pending.plan.notificationSessionId);
|
|
219
237
|
} catch (error) {
|
|
220
238
|
logger.warn({ error }, "Could not restore the self-update notification target; update recovery will continue.");
|
|
221
239
|
}
|
|
@@ -223,6 +241,9 @@ function recoverPendingSelfUpdate(): void {
|
|
|
223
241
|
safeRestart.schedule(
|
|
224
242
|
pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
|
|
225
243
|
notificationTarget,
|
|
244
|
+
{
|
|
245
|
+
restartImmediatelyIfIdle: isServerIdle(store.getServerActivityState()),
|
|
246
|
+
},
|
|
226
247
|
);
|
|
227
248
|
logger.warn(
|
|
228
249
|
{ planPath: pending.planPath, toVersion: pending.plan.toVersion },
|
|
@@ -390,12 +411,15 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
390
411
|
return { ok: false, message: "A restart or update is already pending." };
|
|
391
412
|
}
|
|
392
413
|
assertSelfUpdatePlanPath(request.planPath, agentBotHome());
|
|
393
|
-
const notificationTarget =
|
|
414
|
+
const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
|
|
415
|
+
const restartImmediatelyIfIdle = isServerIdle(store.getServerActivityState());
|
|
394
416
|
pendingSelfUpdatePlanPath = request.planPath;
|
|
395
|
-
safeRestart.schedule(request.reason, notificationTarget);
|
|
417
|
+
safeRestart.schedule(request.reason, notificationTarget, { restartImmediatelyIfIdle });
|
|
396
418
|
return {
|
|
397
419
|
ok: true,
|
|
398
|
-
message:
|
|
420
|
+
message: restartImmediatelyIfIdle
|
|
421
|
+
? "Agent Bot update will start immediately because the server is idle."
|
|
422
|
+
: "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
|
|
399
423
|
};
|
|
400
424
|
}
|
|
401
425
|
case "server_stop":
|
|
@@ -517,6 +541,7 @@ async function shutdown(
|
|
|
517
541
|
]);
|
|
518
542
|
runtimes.close();
|
|
519
543
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
544
|
+
await localFileViewer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local file viewer."));
|
|
520
545
|
store.close();
|
|
521
546
|
if (restartReason) startReplacementSupervisor(restartReason, restartNotificationTargets);
|
|
522
547
|
process.exit(exitCode);
|
|
@@ -586,6 +611,10 @@ function inferServerRestartTarget(notificationSessionId?: string): RestartNotifi
|
|
|
586
611
|
return privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
587
612
|
}
|
|
588
613
|
|
|
614
|
+
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number }): boolean {
|
|
615
|
+
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
589
618
|
function restartNotificationTargetForSession(
|
|
590
619
|
session: { localSessionId: string; contextKey: string },
|
|
591
620
|
): RestartNotificationTarget {
|