@keyou007/agent-bot 0.1.24-alpha.5 → 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.
Files changed (96) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +27 -3
  3. package/README.zh.md +27 -3
  4. package/SECURITY.md +12 -0
  5. package/config.example.yaml +5 -0
  6. package/dist/cli/controlProtocol.js.map +1 -1
  7. package/dist/cli/serverStatus.js +1 -0
  8. package/dist/cli/serverStatus.js.map +1 -1
  9. package/dist/cli.js +6 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/codex/CodexRuntime.js +202 -115
  12. package/dist/codex/CodexRuntime.js.map +1 -1
  13. package/dist/codex/ProviderSwitch.js +12 -13
  14. package/dist/codex/ProviderSwitch.js.map +1 -1
  15. package/dist/config/AgentExecutionDefaultsStore.js +4 -0
  16. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  17. package/dist/config/loadConfig.js +4 -0
  18. package/dist/config/loadConfig.js.map +1 -1
  19. package/dist/config/schema.js +3 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/feishu/CardRenderer.js +66 -8
  22. package/dist/feishu/CardRenderer.js.map +1 -1
  23. package/dist/feishu/FeishuConnector.js +1 -0
  24. package/dist/feishu/FeishuConnector.js.map +1 -1
  25. package/dist/feishu/FeishuTurnPresenter.js +121 -17
  26. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  27. package/dist/index.js +43 -6
  28. package/dist/index.js.map +1 -1
  29. package/dist/local-files/LocalFileViewerServer.js +51 -4
  30. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  31. package/dist/local-files/TurnPreviewPage.js +7 -2
  32. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  33. package/dist/presentation/TurnStateReducer.js +41 -6
  34. package/dist/presentation/TurnStateReducer.js.map +1 -1
  35. package/dist/proxy/ProxySessionController.js +615 -214
  36. package/dist/proxy/ProxySessionController.js.map +1 -1
  37. package/dist/proxy/SessionOperationQueue.js +14 -0
  38. package/dist/proxy/SessionOperationQueue.js.map +1 -0
  39. package/dist/runtime/AcpRuntimeAdapter.js +4 -2
  40. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  41. package/dist/state/StateStore.js +22 -0
  42. package/dist/state/StateStore.js.map +1 -1
  43. package/dist/state/TurnPreviewJournal.js +19 -7
  44. package/dist/state/TurnPreviewJournal.js.map +1 -1
  45. package/dist/state/migrations.js +6 -0
  46. package/dist/state/migrations.js.map +1 -1
  47. package/dist/supervision/SafeRestartNotifier.js +1 -0
  48. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  49. package/dist/supervision/SafeRestartScheduler.js +19 -6
  50. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  51. package/dist/terminal/TerminalCards.js +13 -0
  52. package/dist/terminal/TerminalCards.js.map +1 -0
  53. package/dist/terminal/TerminalLinks.js +76 -0
  54. package/dist/terminal/TerminalLinks.js.map +1 -0
  55. package/dist/terminal/TerminalPage.js +212 -0
  56. package/dist/terminal/TerminalPage.js.map +1 -0
  57. package/dist/terminal/TerminalProcess.js +71 -0
  58. package/dist/terminal/TerminalProcess.js.map +1 -0
  59. package/dist/terminal/TerminalServer.js +639 -0
  60. package/dist/terminal/TerminalServer.js.map +1 -0
  61. package/docs/technical-reference.md +48 -0
  62. package/docs/technical-reference.zh.md +48 -0
  63. package/npm-shrinkwrap.json +55 -5
  64. package/package.json +11 -4
  65. package/skills/agent-bot/SKILL.md +9 -2
  66. package/src/cli/controlProtocol.ts +2 -1
  67. package/src/cli/serverStatus.ts +1 -0
  68. package/src/cli.ts +6 -1
  69. package/src/codex/CodexRuntime.ts +200 -123
  70. package/src/codex/ProviderSwitch.ts +11 -12
  71. package/src/config/AgentExecutionDefaultsStore.ts +2 -0
  72. package/src/config/loadConfig.ts +4 -0
  73. package/src/config/schema.ts +5 -1
  74. package/src/feishu/CardRenderer.ts +81 -8
  75. package/src/feishu/FeishuConnector.ts +1 -0
  76. package/src/feishu/FeishuTurnPresenter.ts +108 -16
  77. package/src/feishu/types.ts +1 -0
  78. package/src/index.ts +44 -7
  79. package/src/local-files/LocalFileViewerServer.ts +48 -4
  80. package/src/local-files/TurnPreviewPage.ts +7 -2
  81. package/src/presentation/TurnStateReducer.ts +46 -7
  82. package/src/presentation/turnViewTypes.ts +5 -0
  83. package/src/proxy/ProxySessionController.ts +625 -211
  84. package/src/proxy/SessionOperationQueue.ts +13 -0
  85. package/src/runtime/AcpRuntimeAdapter.ts +5 -3
  86. package/src/runtime/types.ts +8 -3
  87. package/src/state/StateStore.ts +38 -1
  88. package/src/state/TurnPreviewJournal.ts +19 -7
  89. package/src/state/migrations.ts +6 -0
  90. package/src/supervision/SafeRestartNotifier.ts +1 -0
  91. package/src/supervision/SafeRestartScheduler.ts +18 -6
  92. package/src/terminal/TerminalCards.ts +18 -0
  93. package/src/terminal/TerminalLinks.ts +61 -0
  94. package/src/terminal/TerminalPage.ts +213 -0
  95. package/src/terminal/TerminalProcess.ts +71 -0
  96. package/src/terminal/TerminalServer.ts +513 -0
@@ -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 recorded = journal?.has(turnId);
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, turnId, journal!);
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(turnId, detailKey ?? false)
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(turnId, tool.id, Number(after), snapshot.previewCursor) : undefined;
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
- return `<section class="notice" data-preview-key="approval"><h2>${escapeHtml(title)}</h2>${description}${details}<p>${language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu."}</p></section>`;
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.activities ?? [];
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 { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
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,
@@ -36,6 +36,11 @@ export interface TurnViewState {
36
36
  previewJournal?: boolean;
37
37
  /** Byte cursor for the on-demand preview, never needed for recovery. */
38
38
  previewCursor?: number;
39
+ /** Shared presentation journal across a plan/implementation handoff. */
40
+ previewTurnId?: string;
41
+ continuedFromTurnId?: string;
42
+ continuedToTurnId?: string;
43
+ segmentStartedAt?: number;
39
44
  sessionId: string;
40
45
  turnId: string;
41
46
  agentLabel?: string;