@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -1
  3. package/README.zh.md +3 -1
  4. package/config.example.yaml +12 -0
  5. package/dist/config/loadConfig.js +10 -0
  6. package/dist/config/loadConfig.js.map +1 -1
  7. package/dist/config/schema.js +6 -0
  8. package/dist/config/schema.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +6 -16
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuMarkdown.js +42 -13
  12. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  13. package/dist/feishu/FeishuTurnPresenter.js +33 -10
  14. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  15. package/dist/feishu/LocalImageMarkdown.js +16 -2
  16. package/dist/feishu/LocalImageMarkdown.js.map +1 -1
  17. package/dist/index.js +33 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/local-files/LocalFileViewerServer.js +1064 -0
  20. package/dist/local-files/LocalFileViewerServer.js.map +1 -0
  21. package/dist/local-files/NetworkAddressSelector.js +97 -0
  22. package/dist/local-files/NetworkAddressSelector.js.map +1 -0
  23. package/dist/proxy/ProxySessionController.js +81 -34
  24. package/dist/proxy/ProxySessionController.js.map +1 -1
  25. package/dist/startup/StartupNotifier.js +7 -4
  26. package/dist/startup/StartupNotifier.js.map +1 -1
  27. package/dist/supervision/SafeRestartScheduler.js +15 -2
  28. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  29. package/docs/technical-reference.md +10 -0
  30. package/docs/technical-reference.zh.md +10 -0
  31. package/npm-shrinkwrap.json +12 -2
  32. package/package.json +2 -1
  33. package/skills/agent-bot/SKILL.md +1 -1
  34. package/src/config/loadConfig.ts +10 -0
  35. package/src/config/schema.ts +9 -0
  36. package/src/feishu/CardRenderer.ts +6 -22
  37. package/src/feishu/FeishuMarkdown.ts +51 -12
  38. package/src/feishu/FeishuTurnPresenter.ts +41 -11
  39. package/src/feishu/LocalImageMarkdown.ts +23 -2
  40. package/src/index.ts +33 -4
  41. package/src/local-files/LocalFileViewerServer.ts +1142 -0
  42. package/src/local-files/NetworkAddressSelector.ts +111 -0
  43. package/src/proxy/ProxySessionController.ts +75 -24
  44. package/src/startup/StartupNotifier.ts +9 -4
  45. 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.renderer.renderTurn(state));
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.renderer.renderTurnDetails(snapshot));
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.renderer.renderTurn(entry.state));
359
+ await this.outbound.updateInteractiveCard(targetMessageId, this.renderTurn(entry.state));
359
360
  } else {
360
- await this.outbound.sendInteractiveCard(contextKey, this.renderer.renderTurn(entry.state));
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.renderer.renderActivityHistory(entry.historySnapshot, page);
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.renderer.renderTurn(snapshot)
379
- : this.renderer.renderActivityHistory(snapshot, page);
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.renderer.renderTurn(sentState);
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.renderer.renderTurn(state),
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.slice(cursor, matchIndex));
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 = inferControlRestartTarget(pending.plan.notificationSessionId);
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 = inferControlRestartTarget(request.notificationSessionId);
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: "Agent Bot update scheduled after active tasks finish and the server becomes idle.",
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 {