@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +29 -3
  3. package/README.zh.md +29 -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/CodexEventMapper.js +1 -0
  12. package/dist/codex/CodexEventMapper.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +247 -120
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/codex/ProviderSwitch.js +12 -13
  16. package/dist/codex/ProviderSwitch.js.map +1 -1
  17. package/dist/codex/TraePlanEntry.js +70 -0
  18. package/dist/codex/TraePlanEntry.js.map +1 -0
  19. package/dist/config/AgentExecutionDefaultsStore.js +4 -0
  20. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
  21. package/dist/config/loadConfig.js +4 -0
  22. package/dist/config/loadConfig.js.map +1 -1
  23. package/dist/config/schema.js +3 -0
  24. package/dist/config/schema.js.map +1 -1
  25. package/dist/feishu/CardRenderer.js +92 -8
  26. package/dist/feishu/CardRenderer.js.map +1 -1
  27. package/dist/feishu/FeishuConnector.js +1 -0
  28. package/dist/feishu/FeishuConnector.js.map +1 -1
  29. package/dist/feishu/FeishuTurnPresenter.js +121 -17
  30. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  31. package/dist/index.js +57 -7
  32. package/dist/index.js.map +1 -1
  33. package/dist/local-files/LocalFileViewerServer.js +51 -4
  34. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  35. package/dist/local-files/TurnPreviewPage.js +7 -2
  36. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  37. package/dist/presentation/TurnStateReducer.js +41 -6
  38. package/dist/presentation/TurnStateReducer.js.map +1 -1
  39. package/dist/proxy/ProxySessionController.js +620 -214
  40. package/dist/proxy/ProxySessionController.js.map +1 -1
  41. package/dist/proxy/SessionOperationQueue.js +14 -0
  42. package/dist/proxy/SessionOperationQueue.js.map +1 -0
  43. package/dist/runtime/AcpRuntimeAdapter.js +4 -2
  44. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  45. package/dist/state/StateStore.js +22 -0
  46. package/dist/state/StateStore.js.map +1 -1
  47. package/dist/state/TurnPreviewJournal.js +19 -7
  48. package/dist/state/TurnPreviewJournal.js.map +1 -1
  49. package/dist/state/migrations.js +6 -0
  50. package/dist/state/migrations.js.map +1 -1
  51. package/dist/supervision/SafeRestartNotifier.js +33 -1
  52. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  53. package/dist/supervision/SafeRestartScheduler.js +22 -6
  54. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  55. package/dist/terminal/TerminalCards.js +13 -0
  56. package/dist/terminal/TerminalCards.js.map +1 -0
  57. package/dist/terminal/TerminalCommandTracker.js +74 -0
  58. package/dist/terminal/TerminalCommandTracker.js.map +1 -0
  59. package/dist/terminal/TerminalLinks.js +76 -0
  60. package/dist/terminal/TerminalLinks.js.map +1 -0
  61. package/dist/terminal/TerminalPage.js +212 -0
  62. package/dist/terminal/TerminalPage.js.map +1 -0
  63. package/dist/terminal/TerminalProcess.js +95 -0
  64. package/dist/terminal/TerminalProcess.js.map +1 -0
  65. package/dist/terminal/TerminalServer.js +676 -0
  66. package/dist/terminal/TerminalServer.js.map +1 -0
  67. package/docs/technical-reference.md +48 -0
  68. package/docs/technical-reference.zh.md +48 -0
  69. package/npm-shrinkwrap.json +55 -5
  70. package/package.json +11 -4
  71. package/skills/agent-bot/SKILL.md +11 -2
  72. package/src/cli/controlProtocol.ts +2 -1
  73. package/src/cli/serverStatus.ts +1 -0
  74. package/src/cli.ts +6 -1
  75. package/src/codex/CodexEventMapper.ts +1 -0
  76. package/src/codex/CodexRuntime.ts +244 -128
  77. package/src/codex/ProviderSwitch.ts +11 -12
  78. package/src/codex/TraePlanEntry.ts +63 -0
  79. package/src/config/AgentExecutionDefaultsStore.ts +2 -0
  80. package/src/config/loadConfig.ts +4 -0
  81. package/src/config/schema.ts +5 -1
  82. package/src/feishu/CardRenderer.ts +109 -8
  83. package/src/feishu/FeishuConnector.ts +1 -0
  84. package/src/feishu/FeishuTurnPresenter.ts +108 -16
  85. package/src/feishu/types.ts +1 -0
  86. package/src/index.ts +56 -8
  87. package/src/local-files/LocalFileViewerServer.ts +48 -4
  88. package/src/local-files/TurnPreviewPage.ts +7 -2
  89. package/src/presentation/TurnStateReducer.ts +46 -7
  90. package/src/presentation/turnViewTypes.ts +5 -0
  91. package/src/proxy/ProxySessionController.ts +629 -211
  92. package/src/proxy/SessionOperationQueue.ts +13 -0
  93. package/src/runtime/AcpRuntimeAdapter.ts +5 -3
  94. package/src/runtime/types.ts +8 -3
  95. package/src/state/StateStore.ts +38 -1
  96. package/src/state/TurnPreviewJournal.ts +19 -7
  97. package/src/state/migrations.ts +6 -0
  98. package/src/supervision/SafeRestartNotifier.ts +35 -2
  99. package/src/supervision/SafeRestartScheduler.ts +25 -6
  100. package/src/terminal/TerminalCards.ts +18 -0
  101. package/src/terminal/TerminalCommandTracker.ts +74 -0
  102. package/src/terminal/TerminalLinks.ts +61 -0
  103. package/src/terminal/TerminalPage.ts +213 -0
  104. package/src/terminal/TerminalProcess.ts +93 -0
  105. package/src/terminal/TerminalServer.ts +551 -0
@@ -1,24 +1,23 @@
1
- import { AppServerRequestError } from "./AppServerConnection.js";
2
1
  import type { RuntimeExecutionSettings } from "../runtime/types.js";
3
2
 
4
3
  export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
4
+ export const PROVIDER_SWITCH_EMPTY = "App Server 尚未保存此空会话,无法在保留任务 ID 的前提下切换 Provider。未改变会话或设置,也不会自动创建替代任务。";
5
5
 
6
- export function isMissingRolloutError(error: unknown): boolean {
7
- if (error instanceof AppServerRequestError && error.method !== "thread/resume") return false;
8
- const message = error instanceof AppServerRequestError ? error.serverMessage
9
- : error instanceof Error ? error.message : String(error);
10
- if (/paginated history lineage|missing source rollout/iu.test(message)) return false;
11
- return /no rollout found|rollout[^\n]*(?:not found|missing)/iu.test(message);
6
+ export class ProviderSwitchError extends Error {
7
+ constructor(message: string, readonly previousSettingsRestored: boolean, options?: ErrorOptions) {
8
+ super(message, options);
9
+ this.name = "ProviderSwitchError";
10
+ }
12
11
  }
13
12
 
14
13
  export function assertProviderSettingsApplied(
15
14
  actual: { modelProvider?: string; model?: string },
16
- requested: Pick<RuntimeExecutionSettings, "modelProvider" | "model">,
15
+ requested: Partial<Pick<RuntimeExecutionSettings, "modelProvider" | "model">>,
17
16
  ): void {
18
- if (actual.modelProvider !== requested.modelProvider) {
19
- throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。`);
17
+ if (requested.modelProvider !== undefined && actual.modelProvider !== requested.modelProvider) {
18
+ throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server 未应用设置;可在任务空闲后安全重启再重试,不会自动 Fork 或更改历史。`);
20
19
  }
21
- if (actual.model !== requested.model) {
20
+ if (requested.model !== undefined && actual.model !== requested.model) {
22
21
  throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
23
22
  }
24
23
  }
@@ -29,7 +28,7 @@ export function providerSwitchFailure(error: unknown): string {
29
28
  if (/model[^\n]*(?:unsupported|not supported|not found|unavailable|does not exist)|unknown model/iu.test(message)) {
30
29
  return `目标 Provider 不支持当前模型,请选择兼容的模型后重试。${message}`;
31
30
  }
32
- if (/unauthorized|forbidden|auth(?:entication|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
31
+ if (/unauthorized|forbidden|auth(?:enticat(?:e|ion)|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
33
32
  return `请检查目标 Provider 的配置和认证信息。${message}`;
34
33
  }
35
34
  return message;
@@ -0,0 +1,63 @@
1
+ import { open } from "node:fs/promises";
2
+ import { z } from "zod";
3
+
4
+ const MAX_TAIL_BYTES = 256 * 1024;
5
+ const mutation = z.object({
6
+ type: z.literal("history_mutation"),
7
+ payload: z.object({
8
+ version: z.literal(1),
9
+ turn_id: z.string(),
10
+ operation: z.string(),
11
+ items: z.array(z.object({
12
+ type: z.string(),
13
+ call_id: z.string().optional(),
14
+ name: z.string().optional(),
15
+ output: z.unknown().optional(),
16
+ })).optional(),
17
+ }),
18
+ });
19
+
20
+ // TraeX can finish EnterPlanMode without a mode/changeResolved notification.
21
+ // Read only a bounded tail, and require the exact call and its successful result.
22
+ export async function hasCompletedTraePlanEntry(
23
+ rolloutPath: string, turnId: string, toolCallId: string,
24
+ ): Promise<boolean> {
25
+ const file = await open(rolloutPath, "r");
26
+ try {
27
+ const { size } = await file.stat();
28
+ const start = Math.max(0, size - MAX_TAIL_BYTES);
29
+ const buffer = Buffer.alloc(size - start);
30
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, start);
31
+ const text = buffer.subarray(0, bytesRead).toString("utf8");
32
+ const lines = text.split("\n");
33
+ if (start > 0) lines.shift();
34
+ lines.pop(); // Ignore an uncommitted/partial final record.
35
+ let entered = false;
36
+ let matchedCall = false;
37
+ for (const line of lines) {
38
+ let value: unknown;
39
+ try { value = JSON.parse(line); } catch { continue; }
40
+ const parsed = mutation.safeParse(value);
41
+ if (!parsed.success || parsed.data.payload.turn_id !== turnId) continue;
42
+ const payload = parsed.data.payload;
43
+ if (payload.operation !== "append") {
44
+ matchedCall = false;
45
+ entered = false;
46
+ continue;
47
+ }
48
+ for (const item of payload.items ?? []) {
49
+ if (item.call_id !== toolCallId) continue;
50
+ if (item.type === "function_call") {
51
+ matchedCall = item.name === "EnterPlanMode";
52
+ entered = false;
53
+ } else if (item.type === "function_call_output") {
54
+ entered = matchedCall && typeof item.output === "string"
55
+ && item.output.startsWith("Entered plan mode.");
56
+ }
57
+ }
58
+ }
59
+ return entered;
60
+ } finally {
61
+ await file.close();
62
+ }
63
+ }
@@ -33,7 +33,9 @@ export function writeAgentExecutionDefaults(
33
33
  for (const key of DEFAULT_KEYS) {
34
34
  const value = defaults[key];
35
35
  if (value !== undefined) target.set(key, value);
36
+ else target.delete(key);
36
37
  }
38
+ if (target.items.length === 0) agent.delete("defaults");
37
39
 
38
40
  const updated = document.toString();
39
41
  if (updated === original) return false;
@@ -30,6 +30,10 @@ updates:
30
30
  # Daily stable update check, private 60-second cancellation window, then safe update.
31
31
  enabled: true
32
32
 
33
+ terminal:
34
+ enabled: true
35
+ # Shares fileViewer host, port and publicBaseUrl; keeps independent owner authorization.
36
+
33
37
  fileViewer:
34
38
  enabled: true
35
39
  # Use 0.0.0.0 to accept LAN connections and automatically prefer
@@ -57,6 +57,9 @@ export const agentConfigSchema = z.object({
57
57
  });
58
58
 
59
59
  export const appConfigSchema = z.object({
60
+ terminal: z.object({
61
+ enabled: z.boolean().default(true),
62
+ }).default({ enabled: true }),
60
63
  updates: z.object({ enabled: z.boolean().default(true) }).default({ enabled: true }),
61
64
  console: z
62
65
  .object({
@@ -108,6 +111,7 @@ export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
108
111
  defaults?: AgentExecutionDefaults;
109
112
  };
110
113
  export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
111
- export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates"> & {
114
+ export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates" | "terminal"> & {
112
115
  updates?: { enabled: boolean };
116
+ terminal?: z.infer<typeof appConfigSchema>["terminal"];
113
117
  };
@@ -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 type { JsonValue } from "../acp/acpTypes.js";
4
5
  import type { RuntimeSession } from "../acp/AcpSessionManager.js";
@@ -14,6 +15,7 @@ import type {
14
15
  import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
15
16
  import { truncateMiddle, truncateText } from "../utils/markdown.js";
16
17
  import type { UpdateNotice } from "../updates/types.js";
18
+ import type { ActiveTerminal, TerminalCardView } from "../terminal/TerminalServer.js";
17
19
  import { localCardImage } from "./LocalCardImage.js";
18
20
 
19
21
  export interface StartupStatusView {
@@ -58,6 +60,9 @@ export interface InitializationWelcomeView {
58
60
  }
59
61
 
60
62
  export interface SafeRestartStatusView {
63
+ activeTerminals?: number;
64
+ terminalBlockers?: ActiveTerminal[];
65
+ contextKey?: string;
61
66
  scheduleId: number;
62
67
  reason: string;
63
68
  phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
@@ -85,6 +90,7 @@ export interface ModelSelectorCardView {
85
90
  sessionId: string;
86
91
  contextKey: string;
87
92
  currentModel?: string;
93
+ modelPage?: number;
88
94
  reasoningEffort?: string;
89
95
  models: ModelOption[];
90
96
  modelProvider?: string;
@@ -131,7 +137,7 @@ export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" |
131
137
 
132
138
  const EXECUTION_SETTING_EFFECTS: Record<ExecutionSettingsTab, string> = {
133
139
  agent: "只影响新建任务,不切换当前任务。",
134
- provider: "只影响当前任务,需空闲时切换。",
140
+ provider: "只影响当前任务;运行中选择将在本轮结束后生效。",
135
141
  model: "只影响当前任务,下一轮生效。",
136
142
  thinking: "只影响当前任务,下一轮生效。",
137
143
  permission: "只影响当前任务,下一轮生效。",
@@ -143,6 +149,8 @@ export interface ExecutionSettingsAgentOption {
143
149
  }
144
150
 
145
151
  export interface ExecutionSettingsCardView {
152
+ pendingNotice?: string;
153
+ pendingFailed?: boolean;
146
154
  sessionId?: string;
147
155
  contextKey: string;
148
156
  activeTab: ExecutionSettingsTab;
@@ -152,6 +160,7 @@ export interface ExecutionSettingsCardView {
152
160
  runtimeSettingsAvailable: boolean;
153
161
  currentProvider?: string;
154
162
  currentModel?: string;
163
+ modelPage?: number;
155
164
  currentEffort?: string;
156
165
  currentPermissionMode: PermissionMode;
157
166
  providers: ModelProviderOption[];
@@ -263,6 +272,7 @@ export interface DismissGroupCardView {
263
272
  }
264
273
 
265
274
  export interface ShellCommandCardView {
275
+ terminalUrl?: string;
266
276
  jobId?: string;
267
277
  contextKey?: string;
268
278
  command: string;
@@ -508,6 +518,9 @@ export class CardRenderer {
508
518
  ...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
509
519
  ].join("\n")),
510
520
  ];
521
+ if (view.terminalUrl) {
522
+ elements.push(markdown(`[Open Terminal](${view.terminalUrl}) · 独立终端,需在本会话由所有者授权`));
523
+ }
511
524
  if (view.status === "running" && view.jobId && view.contextKey) {
512
525
  elements.push({
513
526
  tag: "button",
@@ -553,6 +566,7 @@ export class CardRenderer {
553
566
  `**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
554
567
  ]
555
568
  : []),
569
+ ...(view.pendingNotice ? [view.pendingNotice] : []),
556
570
  ...(view.notice ? [view.notice] : []),
557
571
  ...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
558
572
  ].join("\n")),
@@ -584,7 +598,8 @@ export class CardRenderer {
584
598
  inlineCode(provider.id),
585
599
  provider.isDefault ? " · 默认" : "",
586
600
  ].join(""),
587
- current: provider.id === view.currentProvider,
601
+ current: provider.id === view.currentProvider && !view.pendingFailed,
602
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
588
603
  action: {
589
604
  text: "Switch",
590
605
  value: {
@@ -599,18 +614,24 @@ export class CardRenderer {
599
614
  if (view.models.length === 0) {
600
615
  elements.push(markdown("当前运行时未返回可用模型。"));
601
616
  } else {
602
- elements.push(...view.models.map((model) => settingsOptionRow({
617
+ const page = paginateModels(view.models, view.currentModel, view.modelPage, {
618
+ action: "settings_tab_open", ...baseAction, tab: "model", provider: view.currentProvider ?? "",
619
+ });
620
+ elements.push(...page.models.map((model) => settingsOptionRow({
603
621
  label: `${inlineCode(model.id)}${model.isDefault ? " · 默认" : ""}`,
604
- current: model.id === view.currentModel,
622
+ current: model.id === view.currentModel && !view.pendingFailed,
623
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
605
624
  action: {
606
625
  text: "Switch",
607
626
  value: {
608
627
  action: "settings_model_select",
609
628
  ...baseAction,
629
+ provider: view.currentProvider ?? "",
610
630
  model: model.id,
611
631
  },
612
632
  },
613
633
  })));
634
+ elements.push(...page.navigation);
614
635
  }
615
636
  } else if (view.activeTab === "thinking") {
616
637
  if (!view.currentModel) {
@@ -622,11 +643,13 @@ export class CardRenderer {
622
643
  elements.push(...view.reasoningOptions.map((option) => settingsOptionRow({
623
644
  label: inlineCode(option.value),
624
645
  current: option.value === view.currentEffort,
646
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
625
647
  action: {
626
648
  text: "Switch",
627
649
  value: {
628
650
  action: "settings_thinking_select",
629
651
  ...baseAction,
652
+ provider: view.currentProvider ?? "",
630
653
  model: currentModel,
631
654
  effort: option.value,
632
655
  },
@@ -637,6 +660,7 @@ export class CardRenderer {
637
660
  elements.push(...(["auto", "confirm"] as PermissionMode[]).map((mode) => settingsOptionRow({
638
661
  label: `${inlineCode(mode)} · ${permissionModeLabel(mode)}`,
639
662
  current: mode === view.currentPermissionMode,
663
+ currentLabel: view.pendingNotice ? "待生效" : undefined,
640
664
  action: {
641
665
  text: "Switch",
642
666
  value: {
@@ -649,7 +673,7 @@ export class CardRenderer {
649
673
  }
650
674
 
651
675
  elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
652
- return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
676
+ return sectionCard("运行设置", elements, view.warning || view.pendingFailed ? "orange" : view.notice ? "green" : "blue");
653
677
  }
654
678
 
655
679
  renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
@@ -785,7 +809,14 @@ export class CardRenderer {
785
809
  if (view.models.length === 0) {
786
810
  elements.push(markdown("当前运行时未返回可用模型。"));
787
811
  } else {
788
- elements.push(...view.models.map((model) => {
812
+ const page = paginateModels(view.models, view.currentModel, view.modelPage, {
813
+ action: view.unifiedSettings ? "provider_model_open" : "model_open",
814
+ sessionId: view.sessionId,
815
+ contextKey: view.contextKey,
816
+ ...(view.modelProvider ? { provider: view.modelProvider } : {}),
817
+ ...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
818
+ });
819
+ elements.push(...page.models.map((model) => {
789
820
  const isCurrent = model.id === view.currentModel;
790
821
  return {
791
822
  tag: "column_set",
@@ -823,6 +854,7 @@ export class CardRenderer {
823
854
  ],
824
855
  };
825
856
  }));
857
+ elements.push(...page.navigation);
826
858
  }
827
859
  if (view.unifiedSettings) {
828
860
  elements.push(
@@ -994,6 +1026,24 @@ export class CardRenderer {
994
1026
  view.status === "completed" ? "green" : view.status === "cancelled" ? "grey" : "blue");
995
1027
  }
996
1028
 
1029
+ renderTerminalAuthorization(view: TerminalCardView): Record<string, unknown> {
1030
+ const action = (decision: string) => ({ action: "terminal_authorize", contextKey: view.contextKey, requestId: view.requestId, token: view.approvalToken, decision });
1031
+ const elements: Record<string, unknown>[] = [markdown(`**目录**:${inlineCode(view.cwd)}\n**终端**:${inlineCode(view.reopenToken)}`)];
1032
+ if (view.state === "pending") {
1033
+ elements.push(markdown(`**验证码**:${inlineCode(view.code)}\n仅 AgentBot 所有者可授权。授权后,该浏览器可在本机执行任意命令,拥有 AgentBot 的系统用户权限。请核对网页验证码,仅批准本人发起的请求。5 分钟内有效。${view.recovery ? (view.shellStarted ? "恢复原 Shell,不创建新终端;授权后旧网页失去访问权限。拒绝不影响原终端。" : "继续打开同一个终端;授权后旧网页失去访问权限,Shell 在连接成功后启动。拒绝不影响原授权。") : "不执行原卡片命令。"}`),
1034
+ taskActionRow([{ text: "Allow", value: action("allow") }, { text: "Deny", type: "danger", value: action("deny") }, { text: "刷新", value: action("refresh") }]));
1035
+ } else if (view.state === "closed") {
1036
+ elements.push(markdown(`**状态**:${view.message ? escapeCardHtml(view.message) : "已关闭"}\n原终端已结束时,请从命令卡片创建新终端。`));
1037
+ } else {
1038
+ const waiting = view.state === "approved";
1039
+ const label = waiting ? "打开终端" : "重新打开终端";
1040
+ elements.push(markdown(`**状态**:${waiting ? "已授权,等待网页连接" : view.connected ? "已连接" : "网页已断开,Shell 仍在运行"}\n${waiting ? "请返回原网页连接;原网页已关闭时,点击下方“打开终端”。Shell 尚未启动。" : "点击下方“重新打开终端”恢复原 Shell。前台连接时不因空闲关闭;后台空闲 30 分钟或网页断开 3 分钟后会自动关闭 Shell,成功重连取消断开计时并重置空闲计时;“关闭终端”会中断其中的命令。"}`));
1041
+ elements.push(markdown(`[${label}](${view.reopenUrl}) · 新网页需核对验证码重新授权${waiting ? ",连接成功后启动 Shell" : ",恢复原 Shell"}`));
1042
+ elements.push(taskActionRow([{ text: "关闭终端", type: "danger", value: action("close") }, { text: "刷新", value: action("refresh") }]));
1043
+ }
1044
+ return sectionCard(view.state === "pending" ? "网页终端授权" : "网页终端管理", elements, view.state === "closed" ? "grey" : view.state === "pending" ? "orange" : "blue");
1045
+ }
1046
+
997
1047
  renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
998
1048
  const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
999
1049
  const countdown = view.phase === "countdown"
@@ -1021,8 +1071,35 @@ export class CardRenderer {
1021
1071
  `**重启原因**:${inlineCode(view.reason)}`,
1022
1072
  `**重启倒计时**:${countdown}`,
1023
1073
  `**待投递结果**:${view.pendingFinalDeliveries} 条`,
1074
+ ...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals} 个。结束终端会中断其中的命令;ForceRestart 会中断所有终端。`] : []),
1024
1075
  ];
1025
1076
  const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
1077
+ if (view.terminalBlockers?.length) {
1078
+ elements.push({ tag: "hr" }, markdown(`**阻止重启的终端(${view.terminalBlockers.length})**`));
1079
+ for (const terminal of view.terminalBlockers) {
1080
+ const command = terminal.state === "closing" ? "正在结束,等待 Shell 退出"
1081
+ : terminal.state === "starting" ? "Shell 启动中"
1082
+ : terminal.command.status === "idle" ? "无(Shell 空闲)"
1083
+ : terminal.command.command ? inlineCode(escapeCardHtml(truncateText(terminal.command.command.replace(/\s+/gu, " "), 240)))
1084
+ : terminal.command.status === "running" ? "命令执行中(命令内容不可用)"
1085
+ : "未知(Shell 未提供命令信息)";
1086
+ const connection = terminal.connected ? terminal.foreground ? "网页前台" : "网页已连接" : "网页已断开";
1087
+ const label = `**终端**:${inlineCode(terminal.token)} · ${terminal.active ? "活跃" : "不活跃"} · ${connection}\n**目录**:${inlineCode(escapeCardHtml(truncateText(terminal.cwd, 240)))}\n**${terminal.command.source === "process" ? "前台进程" : "正在执行"}**:${command}`;
1088
+ elements.push({
1089
+ tag: "column_set", flex_mode: "none", horizontal_spacing: "8px",
1090
+ columns: [
1091
+ { tag: "column", width: "weighted", weight: 1, elements: [markdown(label)] },
1092
+ ...(actionable && terminal.state !== "closing" ? [{
1093
+ tag: "column", width: "auto", elements: [taskActionElement({
1094
+ text: "结束终端", type: "danger",
1095
+ value: { action: "safe_restart_terminal_stop", scheduleId: String(view.scheduleId), terminalId: terminal.terminalId, ...(view.contextKey ? { contextKey: view.contextKey } : {}) },
1096
+ })],
1097
+ }] : []),
1098
+ ],
1099
+ });
1100
+ }
1101
+ elements.push(markdown("活跃:前台连接、正在执行命令或最近 30 秒有输入输出。不活跃的 Shell 仍会阻止安全重启。"));
1102
+ }
1026
1103
  if (view.waitingTasks.length > 0) {
1027
1104
  const visible = view.waitingTasks.slice(0, 10);
1028
1105
  elements.push({ tag: "hr" }, markdown(`**当前等待的任务(${view.waitingTasks.length})**`));
@@ -1782,6 +1859,10 @@ function renderApprovalElements(state: TurnViewState, request: ApprovalRequest):
1782
1859
  request.reason,
1783
1860
  ].filter(Boolean).join("\n")));
1784
1861
  }
1862
+ if (request.automatic) {
1863
+ elements.push(markdown("自动执行模式:正在自动同意。"));
1864
+ return elements;
1865
+ }
1785
1866
  elements.push({
1786
1867
  tag: "column_set",
1787
1868
  flex_mode: "flow",
@@ -2246,7 +2327,8 @@ function removeMarkdownBold(value: string): string {
2246
2327
  }
2247
2328
 
2248
2329
  function turnActivities(state: TurnViewState): TurnActivity[] {
2249
- if (state.activities?.length) return state.activities;
2330
+ if (state.activities?.length) return state.approval
2331
+ ? state.activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : state.activities;
2250
2332
 
2251
2333
  const activities: TurnActivity[] = [];
2252
2334
  if (state.progressText) {
@@ -2845,6 +2927,24 @@ function acpPermissionOptionLabel(option: { name: string; kind: string }): strin
2845
2927
  return labels[option.kind] ?? (/^[\x20-\x7E]+$/.test(option.name) ? option.name : "Select");
2846
2928
  }
2847
2929
 
2930
+ const MODEL_PAGE_SIZE = 20;
2931
+
2932
+ function paginateModels(
2933
+ models: ModelOption[],
2934
+ currentModel: string | undefined,
2935
+ requestedPage: number | undefined,
2936
+ action: Record<string, string>,
2937
+ ): { models: ModelOption[]; navigation: Record<string, unknown>[] } {
2938
+ const totalPages = Math.max(1, Math.ceil(models.length / MODEL_PAGE_SIZE));
2939
+ const currentPage = Math.floor(models.findIndex((model) => model.id === currentModel) / MODEL_PAGE_SIZE);
2940
+ const page = Math.max(0, Math.min(requestedPage ?? currentPage, totalPages - 1));
2941
+ const navigation = totalPages > 1 ? [taskActionRow([
2942
+ ...(page > 0 ? [{ text: "上一页", value: { ...action, page: String(page - 1) } }] : []),
2943
+ ...(page < totalPages - 1 ? [{ text: "下一页", value: { ...action, page: String(page + 1) } }] : []),
2944
+ ], `第 ${page + 1}/${totalPages} 页 · 共 ${models.length} 个模型`)] : [];
2945
+ return { models: models.slice(page * MODEL_PAGE_SIZE, (page + 1) * MODEL_PAGE_SIZE), navigation };
2946
+ }
2947
+
2848
2948
  function settingsTabRow(
2849
2949
  activeTab: ExecutionSettingsTab,
2850
2950
  baseAction: Record<string, string>,
@@ -2918,6 +3018,7 @@ function settingsTabElement(
2918
3018
  function settingsOptionRow(input: {
2919
3019
  label: string;
2920
3020
  current: boolean;
3021
+ currentLabel?: string;
2921
3022
  action: TaskListCardAction;
2922
3023
  }): Record<string, unknown> {
2923
3024
  return {
@@ -2937,7 +3038,7 @@ function settingsOptionRow(input: {
2937
3038
  tag: "column",
2938
3039
  width: "auto",
2939
3040
  vertical_align: "center",
2940
- elements: input.current ? [markdown("✅ 当前")] : [taskActionElement(input.action)],
3041
+ elements: input.current ? [markdown(`✅ ${input.currentLabel ?? "当前"}`)] : [taskActionElement(input.action)],
2941
3042
  },
2942
3043
  ],
2943
3044
  };
@@ -262,6 +262,7 @@ function toIncomingMessage(
262
262
  const messageId = message.message_id ?? `${Date.now()}`;
263
263
  return {
264
264
  messageId,
265
+ ...(typeof messageType === "string" ? { messageType } : {}),
265
266
  ...(Number.isFinite(Number(message.create_time)) && Number(message.create_time) > 0
266
267
  ? { createdAt: Number(message.create_time) } : {}),
267
268
  contextKey: chatId