@keyou007/agent-bot 0.1.10 → 0.1.11

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 (68) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +16 -10
  3. package/README.zh.md +16 -10
  4. package/assets/agent-bot-logo.png +0 -0
  5. package/dist/acp/AcpSessionManager.js +78 -46
  6. package/dist/acp/AcpSessionManager.js.map +1 -1
  7. package/dist/codex/AppServerConnection.js +15 -1
  8. package/dist/codex/AppServerConnection.js.map +1 -1
  9. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  10. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  11. package/dist/codex/CodexProcessManager.js +1 -0
  12. package/dist/codex/CodexProcessManager.js.map +1 -1
  13. package/dist/codex/CodexRuntime.js +77 -2
  14. package/dist/codex/CodexRuntime.js.map +1 -1
  15. package/dist/commands/CommandRouter.js +64 -49
  16. package/dist/commands/CommandRouter.js.map +1 -1
  17. package/dist/console/ConsoleTurnPresenter.js +1 -1
  18. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  19. package/dist/feishu/CardRenderer.js +345 -11
  20. package/dist/feishu/CardRenderer.js.map +1 -1
  21. package/dist/feishu/FeishuMessageClient.js +51 -5
  22. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  23. package/dist/feishu/FeishuTurnPresenter.js +8 -3
  24. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  25. package/dist/index.js +18 -17
  26. package/dist/index.js.map +1 -1
  27. package/dist/proxy/ProxySessionController.js +589 -205
  28. package/dist/proxy/ProxySessionController.js.map +1 -1
  29. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  30. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  31. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  32. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  33. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  34. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  35. package/dist/startup/SessionMetadataHydrator.js +2 -2
  36. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  37. package/dist/startup/StartupNotifier.js +1 -0
  38. package/dist/startup/StartupNotifier.js.map +1 -1
  39. package/dist/state/StateStore.js +21 -15
  40. package/dist/state/StateStore.js.map +1 -1
  41. package/dist/supervision/SafeRestartScheduler.js +5 -0
  42. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  43. package/docs/technical-reference.md +15 -5
  44. package/docs/technical-reference.zh.md +15 -5
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +2 -1
  47. package/skills/agent-bot/SKILL.md +13 -3
  48. package/src/acp/AcpSessionManager.ts +73 -54
  49. package/src/codex/AppServerConnection.ts +21 -1
  50. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  51. package/src/codex/CodexProcessManager.ts +1 -0
  52. package/src/codex/CodexRuntime.ts +91 -2
  53. package/src/commands/CommandRouter.ts +71 -49
  54. package/src/commands/commandTypes.ts +6 -11
  55. package/src/console/ConsoleTurnPresenter.ts +1 -1
  56. package/src/feishu/CardRenderer.ts +418 -11
  57. package/src/feishu/FeishuMessageClient.ts +66 -3
  58. package/src/feishu/FeishuTurnPresenter.ts +7 -2
  59. package/src/index.ts +21 -22
  60. package/src/proxy/ProxySessionController.ts +706 -208
  61. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  62. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  63. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  64. package/src/runtime/types.ts +18 -0
  65. package/src/startup/SessionMetadataHydrator.ts +2 -2
  66. package/src/startup/StartupNotifier.ts +1 -0
  67. package/src/state/StateStore.ts +29 -15
  68. package/src/supervision/SafeRestartScheduler.ts +5 -0
@@ -5,6 +5,7 @@ import type { RuntimeSession } from "../acp/AcpSessionManager.js";
5
5
  import type {
6
6
  ApprovalDecision,
7
7
  ModelOption,
8
+ ModelProviderOption,
8
9
  PermissionMode,
9
10
  ReasoningEffortOption,
10
11
  ToolState,
@@ -24,6 +25,7 @@ export interface StartupStatusView {
24
25
  currentTask?: {
25
26
  id: string;
26
27
  title?: string;
28
+ modelProvider?: string;
27
29
  model?: string;
28
30
  reasoningEffort?: string;
29
31
  permissionMode?: PermissionMode;
@@ -60,6 +62,9 @@ export interface ModelSelectorCardView {
60
62
  currentModel?: string;
61
63
  reasoningEffort?: string;
62
64
  models: ModelOption[];
65
+ modelProvider?: string;
66
+ permissionMode?: PermissionMode;
67
+ unifiedSettings?: boolean;
63
68
  notice?: string;
64
69
  }
65
70
 
@@ -69,6 +74,55 @@ export interface ReasoningSelectorCardView {
69
74
  model: string;
70
75
  currentEffort?: string;
71
76
  options: ReasoningEffortOption[];
77
+ modelProvider?: string;
78
+ permissionMode?: PermissionMode;
79
+ unifiedSettings?: boolean;
80
+ notice?: string;
81
+ }
82
+
83
+ export interface ProviderSelectorCardView {
84
+ sessionId: string;
85
+ contextKey: string;
86
+ currentProvider?: string;
87
+ currentModel?: string;
88
+ reasoningEffort?: string;
89
+ permissionMode: PermissionMode;
90
+ providers: ModelProviderOption[];
91
+ notice?: string;
92
+ }
93
+
94
+ export interface PermissionSelectorCardView {
95
+ sessionId: string;
96
+ contextKey: string;
97
+ modelProvider: string;
98
+ model: string;
99
+ reasoningEffort: string;
100
+ currentMode: PermissionMode;
101
+ }
102
+
103
+ export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" | "permission";
104
+
105
+ export interface ExecutionSettingsAgentOption {
106
+ name: string;
107
+ title: string;
108
+ }
109
+
110
+ export interface ExecutionSettingsCardView {
111
+ sessionId?: string;
112
+ contextKey: string;
113
+ activeTab: ExecutionSettingsTab;
114
+ currentAgent: string;
115
+ taskAgent?: string;
116
+ agents: ExecutionSettingsAgentOption[];
117
+ runtimeSettingsAvailable: boolean;
118
+ currentProvider?: string;
119
+ currentModel?: string;
120
+ currentEffort?: string;
121
+ currentPermissionMode: PermissionMode;
122
+ providers: ModelProviderOption[];
123
+ providerSupported: boolean;
124
+ models: ModelOption[];
125
+ reasoningOptions: ReasoningEffortOption[];
72
126
  notice?: string;
73
127
  }
74
128
 
@@ -91,9 +145,183 @@ export interface TaskListCardEntry {
91
145
  }
92
146
 
93
147
  export class CardRenderer {
148
+ renderExecutionSettings(view: ExecutionSettingsCardView): Record<string, unknown> {
149
+ const baseAction: Record<string, string> = {
150
+ contextKey: view.contextKey,
151
+ ...(view.sessionId ? { sessionId: view.sessionId } : {}),
152
+ };
153
+ const tabRow = settingsTabRow(
154
+ view.activeTab,
155
+ baseAction,
156
+ view.agents.length > 1,
157
+ view.runtimeSettingsAvailable,
158
+ );
159
+ const elements: Record<string, unknown>[] = [
160
+ markdown([
161
+ view.taskAgent
162
+ ? `**默认 Agent / 当前任务 Agent**:${inlineCode(view.currentAgent)} / ${inlineCode(view.taskAgent)}`
163
+ : `**默认 Agent**:${inlineCode(view.currentAgent)}`,
164
+ ...(view.runtimeSettingsAvailable
165
+ ? [
166
+ `**Provider / 模型**:${inlineCode(view.currentProvider ?? "Codex 默认")} / ${inlineCode(view.currentModel ?? "默认")}`,
167
+ `**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
168
+ ]
169
+ : []),
170
+ ...(view.notice ? [view.notice] : []),
171
+ ].join("\n")),
172
+ ...(tabRow ? [tabRow] : []),
173
+ { tag: "hr" },
174
+ ];
175
+
176
+ if (view.activeTab === "agent") {
177
+ elements.push(...view.agents.map((agent) => settingsOptionRow({
178
+ label: `${inlineCode(agent.name)} · ${escapeCardHtml(agent.title)}`,
179
+ current: agent.name === view.currentAgent,
180
+ action: {
181
+ text: "Switch",
182
+ value: {
183
+ action: "settings_agent_select",
184
+ ...baseAction,
185
+ agent: agent.name,
186
+ },
187
+ },
188
+ })));
189
+ } else if (view.activeTab === "provider") {
190
+ if (!view.providerSupported) {
191
+ elements.push(markdown("当前运行时不支持 Provider 切换;其他设置仍可通过上方 tab 修改。"));
192
+ } else if (view.providers.length === 0) {
193
+ elements.push(markdown("Codex 配置中没有可用的 Provider。"));
194
+ } else {
195
+ elements.push(...view.providers.map((provider) => settingsOptionRow({
196
+ label: [
197
+ inlineCode(provider.id),
198
+ provider.displayName && provider.displayName !== provider.id
199
+ ? ` · ${escapeCardHtml(provider.displayName)}`
200
+ : "",
201
+ provider.isDefault ? " · 默认" : "",
202
+ ].join(""),
203
+ current: provider.id === view.currentProvider,
204
+ action: {
205
+ text: "Switch",
206
+ value: {
207
+ action: "settings_provider_select",
208
+ ...baseAction,
209
+ provider: provider.id,
210
+ },
211
+ },
212
+ })));
213
+ }
214
+ } else if (view.activeTab === "model") {
215
+ if (view.models.length === 0) {
216
+ elements.push(markdown("当前运行时未返回可用模型。"));
217
+ } else {
218
+ elements.push(...view.models.map((model) => settingsOptionRow({
219
+ label: `${inlineCode(model.id)}${model.isDefault ? " · 默认" : ""}`,
220
+ current: model.id === view.currentModel,
221
+ action: {
222
+ text: "Switch",
223
+ value: {
224
+ action: "settings_model_select",
225
+ ...baseAction,
226
+ model: model.id,
227
+ },
228
+ },
229
+ })));
230
+ }
231
+ } else if (view.activeTab === "thinking") {
232
+ if (!view.currentModel) {
233
+ elements.push(markdown("请先选择模型。"));
234
+ } else if (view.reasoningOptions.length === 0) {
235
+ elements.push(markdown("当前模型没有可配置的思考强度。"));
236
+ } else {
237
+ const currentModel = view.currentModel;
238
+ elements.push(...view.reasoningOptions.map((option) => settingsOptionRow({
239
+ label: inlineCode(option.value),
240
+ current: option.value === view.currentEffort,
241
+ action: {
242
+ text: "Switch",
243
+ value: {
244
+ action: "settings_thinking_select",
245
+ ...baseAction,
246
+ model: currentModel,
247
+ effort: option.value,
248
+ },
249
+ },
250
+ })));
251
+ }
252
+ } else {
253
+ elements.push(...(["auto", "confirm"] as PermissionMode[]).map((mode) => settingsOptionRow({
254
+ label: `${inlineCode(mode)} · ${permissionModeLabel(mode)}`,
255
+ current: mode === view.currentPermissionMode,
256
+ action: {
257
+ text: "Switch",
258
+ value: {
259
+ action: "settings_permission_select",
260
+ ...baseAction,
261
+ permissionMode: mode,
262
+ },
263
+ },
264
+ })));
265
+ }
266
+
267
+ return sectionCard("运行设置", elements, view.notice ? "green" : "blue");
268
+ }
269
+
270
+ renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
271
+ const elements: Record<string, unknown>[] = [
272
+ markdown([
273
+ `**当前 Provider**:${inlineCode(view.currentProvider ?? "Codex 默认")}`,
274
+ `**模型 / 思考强度 / 权限**:${inlineCode(view.currentModel ?? "默认")} / ${inlineCode(view.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.permissionMode))}`,
275
+ ...(view.notice ? [view.notice] : []),
276
+ ].join("\n")),
277
+ { tag: "hr" },
278
+ ];
279
+ if (view.providers.length === 0) {
280
+ elements.push(markdown("Codex 配置中没有可用的 Provider。"));
281
+ } else {
282
+ elements.push(...view.providers.map((provider) => ({
283
+ tag: "column_set",
284
+ flex_mode: "none",
285
+ horizontal_spacing: "8px",
286
+ vertical_align: "center",
287
+ columns: [
288
+ {
289
+ tag: "column",
290
+ width: "weighted",
291
+ weight: 1,
292
+ vertical_align: "center",
293
+ elements: [markdown([
294
+ inlineCode(provider.id),
295
+ provider.displayName && provider.displayName !== provider.id ? ` · ${escapeCardHtml(provider.displayName)}` : "",
296
+ provider.isDefault ? " · 默认" : "",
297
+ provider.id === view.currentProvider ? " · ✅ 当前" : "",
298
+ ].join(""))],
299
+ },
300
+ {
301
+ tag: "column",
302
+ width: "auto",
303
+ vertical_align: "center",
304
+ elements: [taskActionElement({
305
+ text: provider.id === view.currentProvider ? "Configure" : "Select",
306
+ value: {
307
+ action: "provider_select",
308
+ sessionId: view.sessionId,
309
+ contextKey: view.contextKey,
310
+ provider: provider.id,
311
+ permissionMode: view.permissionMode,
312
+ },
313
+ })],
314
+ },
315
+ ],
316
+ })));
317
+ }
318
+ return sectionCard("Provider 设置", elements, view.notice ? "green" : "blue");
319
+ }
320
+
94
321
  renderReasoningSelector(view: ReasoningSelectorCardView): Record<string, unknown> {
95
322
  const elements: Record<string, unknown>[] = [
96
323
  markdown([
324
+ ...(view.modelProvider ? [`**Provider**:${inlineCode(view.modelProvider)}`] : []),
97
325
  `**模型**:${inlineCode(view.model)}`,
98
326
  `**当前思考模式**:${inlineCode(view.currentEffort ?? "默认")}`,
99
327
  ...(view.notice ? [view.notice] : []),
@@ -122,16 +350,18 @@ export class CardRenderer {
122
350
  tag: "column",
123
351
  width: "auto",
124
352
  vertical_align: "center",
125
- elements: isCurrent
353
+ elements: isCurrent && !view.unifiedSettings
126
354
  ? [markdown("✅ 当前")]
127
355
  : [taskActionElement({
128
- text: "Switch",
356
+ text: view.unifiedSettings ? (isCurrent ? "Continue" : "Select") : "Switch",
129
357
  value: {
130
- action: "reasoning_select",
358
+ action: view.unifiedSettings ? "provider_reasoning_select" : "reasoning_select",
131
359
  sessionId: view.sessionId,
132
360
  contextKey: view.contextKey,
361
+ ...(view.modelProvider ? { provider: view.modelProvider } : {}),
133
362
  model: view.model,
134
363
  effort: option.value,
364
+ ...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
135
365
  },
136
366
  })],
137
367
  },
@@ -144,9 +374,11 @@ export class CardRenderer {
144
374
  taskActionRow([{
145
375
  text: "Back",
146
376
  value: {
147
- action: "model_open",
377
+ action: view.unifiedSettings ? "provider_model_open" : "model_open",
148
378
  sessionId: view.sessionId,
149
379
  contextKey: view.contextKey,
380
+ ...(view.modelProvider ? { provider: view.modelProvider } : {}),
381
+ ...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
150
382
  },
151
383
  }]),
152
384
  );
@@ -156,6 +388,7 @@ export class CardRenderer {
156
388
  renderModelSelector(view: ModelSelectorCardView): Record<string, unknown> {
157
389
  const elements: Record<string, unknown>[] = [
158
390
  markdown([
391
+ ...(view.modelProvider ? [`**Provider**:${inlineCode(view.modelProvider)}`] : []),
159
392
  `**当前模型**:${inlineCode(view.currentModel ?? "默认")}`,
160
393
  `**思考强度**:${inlineCode(view.reasoningEffort ?? "默认")}`,
161
394
  ...(view.notice ? [view.notice] : []),
@@ -186,15 +419,17 @@ export class CardRenderer {
186
419
  tag: "column",
187
420
  width: "auto",
188
421
  vertical_align: "center",
189
- elements: isCurrent
422
+ elements: isCurrent && !view.unifiedSettings
190
423
  ? [markdown("✅ 当前")]
191
424
  : [taskActionElement({
192
- text: "Switch",
425
+ text: view.unifiedSettings ? (isCurrent ? "Continue" : "Select") : "Switch",
193
426
  value: {
194
- action: "model_select",
427
+ action: view.unifiedSettings ? "provider_model_select" : "model_select",
195
428
  sessionId: view.sessionId,
196
429
  contextKey: view.contextKey,
430
+ ...(view.modelProvider ? { provider: view.modelProvider } : {}),
197
431
  model: model.id,
432
+ ...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
198
433
  },
199
434
  })],
200
435
  },
@@ -202,7 +437,77 @@ export class CardRenderer {
202
437
  };
203
438
  }));
204
439
  }
205
- return sectionCard("模型", elements, view.notice ? "green" : "blue");
440
+ if (view.unifiedSettings) {
441
+ elements.push(
442
+ { tag: "hr" },
443
+ taskActionRow([{
444
+ text: "Back",
445
+ value: {
446
+ action: "provider_open",
447
+ sessionId: view.sessionId,
448
+ contextKey: view.contextKey,
449
+ },
450
+ }]),
451
+ );
452
+ }
453
+ return sectionCard(view.unifiedSettings ? "Provider 设置 · 模型" : "模型", elements, view.notice ? "green" : "blue");
454
+ }
455
+
456
+ renderPermissionSelector(view: PermissionSelectorCardView): Record<string, unknown> {
457
+ const elements: Record<string, unknown>[] = [
458
+ markdown([
459
+ `**Provider**:${inlineCode(view.modelProvider)}`,
460
+ `**模型**:${inlineCode(view.model)}`,
461
+ `**思考强度**:${inlineCode(view.reasoningEffort)}`,
462
+ `**当前权限**:${inlineCode(permissionModeLabel(view.currentMode))}`,
463
+ ].join("\n")),
464
+ { tag: "hr" },
465
+ ...(["auto", "confirm"] as PermissionMode[]).map((mode) => ({
466
+ tag: "column_set",
467
+ flex_mode: "none",
468
+ horizontal_spacing: "8px",
469
+ vertical_align: "center",
470
+ columns: [
471
+ {
472
+ tag: "column",
473
+ width: "weighted",
474
+ weight: 1,
475
+ vertical_align: "center",
476
+ elements: [markdown(`${inlineCode(mode)} · ${permissionModeLabel(mode)}`)],
477
+ },
478
+ {
479
+ tag: "column",
480
+ width: "auto",
481
+ vertical_align: "center",
482
+ elements: [taskActionElement({
483
+ text: mode === view.currentMode ? "Apply" : "Select",
484
+ value: {
485
+ action: "provider_permission_select",
486
+ sessionId: view.sessionId,
487
+ contextKey: view.contextKey,
488
+ provider: view.modelProvider,
489
+ model: view.model,
490
+ effort: view.reasoningEffort,
491
+ permissionMode: mode,
492
+ },
493
+ })],
494
+ },
495
+ ],
496
+ })),
497
+ { tag: "hr" },
498
+ taskActionRow([{
499
+ text: "Back",
500
+ value: {
501
+ action: "provider_reasoning_open",
502
+ sessionId: view.sessionId,
503
+ contextKey: view.contextKey,
504
+ provider: view.modelProvider,
505
+ model: view.model,
506
+ permissionMode: view.currentMode,
507
+ },
508
+ }]),
509
+ ];
510
+ return sectionCard("Provider 设置 · 权限", elements);
206
511
  }
207
512
 
208
513
  renderPromptQueue(view: PromptQueueCardView): Record<string, unknown> {
@@ -330,14 +635,14 @@ export class CardRenderer {
330
635
  ? [
331
636
  `**当前任务**:${inlineCode(view.currentTask.title ?? view.currentTask.id)}`,
332
637
  workspaceLine,
333
- `**模型 / 思考强度 / 权限**:${inlineCode(view.currentTask.model ?? "默认")} / ${inlineCode(view.currentTask.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentTask.permissionMode))}`,
638
+ `**Provider / 模型 / 思考强度 / 权限**:${inlineCode(view.currentTask.modelProvider ?? "Codex 默认")} / ${inlineCode(view.currentTask.model ?? "默认")} / ${inlineCode(view.currentTask.reasoningEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentTask.permissionMode))}`,
334
639
  `**任务状态 / Agent**:${persistedTaskStatus(view.currentTask.sessionStatus, view.currentTask.lastTurnStatus)} / ${inlineCode(view.currentTask.agentName)}`,
335
640
  `**任务 ID**:${inlineCode(view.currentTask.id)}`,
336
641
  ]
337
642
  : [
338
643
  "**当前任务**:无,下一条普通消息会创建新任务",
339
644
  workspaceLine,
340
- `**模型 / 思考强度 / 权限**:${inlineCode("默认")} / ${inlineCode("自动")} / ${inlineCode(permissionModeLabel())}`,
645
+ `**Provider / 模型 / 思考强度 / 权限**:${inlineCode("Codex 默认")} / ${inlineCode("默认")} / ${inlineCode("自动")} / ${inlineCode(permissionModeLabel())}`,
341
646
  `**默认 Agent**:${view.defaultAgentTitle} (${inlineCode(view.defaultAgentName)})`,
342
647
  ];
343
648
  lines.push(
@@ -646,7 +951,7 @@ function fileSummaryPanel(state: TurnViewState): Record<string, unknown> {
646
951
  return collapsiblePanel(
647
952
  `文件变更 · ${state.fileSummary.length}`,
648
953
  state.fileSummary
649
- .map((file) => `- ${displayFilePath(file.path, state.projectCwd)} +${file.additions ?? 0} -${file.deletions ?? 0}`)
954
+ .map((file) => `- ${escapeMarkdownFilePath(displayFilePath(file.path, state.projectCwd))} +${file.additions ?? 0} -${file.deletions ?? 0}`)
650
955
  .join("\n"),
651
956
  { elementId: "turn_files" },
652
957
  );
@@ -859,6 +1164,10 @@ function displayFilePath(filePath: string, projectCwd?: string): string {
859
1164
  return isInsideProject ? relativePath || "." : absolutePath;
860
1165
  }
861
1166
 
1167
+ function escapeMarkdownFilePath(filePath: string): string {
1168
+ return filePath.replaceAll("\\.", "\\\\.");
1169
+ }
1170
+
862
1171
  function usesWindowsPaths(...values: string[]): boolean {
863
1172
  return values.some((value) => /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value));
864
1173
  }
@@ -1062,6 +1371,104 @@ function acpPermissionOptionLabel(option: { name: string; kind: string }): strin
1062
1371
  return labels[option.kind] ?? (/^[\x20-\x7E]+$/.test(option.name) ? option.name : "Select");
1063
1372
  }
1064
1373
 
1374
+ function settingsTabRow(
1375
+ activeTab: ExecutionSettingsTab,
1376
+ baseAction: Record<string, string>,
1377
+ showAgentTab: boolean,
1378
+ showRuntimeTabs: boolean,
1379
+ ): Record<string, unknown> | undefined {
1380
+ const tabs: Array<{ id: ExecutionSettingsTab; label: string }> = [
1381
+ ...(showAgentTab ? [{ id: "agent" as const, label: "Agent" }] : []),
1382
+ ...(showRuntimeTabs
1383
+ ? [
1384
+ { id: "provider" as const, label: "Provider" },
1385
+ { id: "model" as const, label: "Model" },
1386
+ { id: "thinking" as const, label: "Thinking" },
1387
+ { id: "permission" as const, label: "Permission" },
1388
+ ]
1389
+ : []),
1390
+ ];
1391
+ if (tabs.length === 0) return undefined;
1392
+ return {
1393
+ tag: "column_set",
1394
+ flex_mode: "none",
1395
+ horizontal_spacing: "2px",
1396
+ margin: "8px 0 0 0",
1397
+ columns: tabs.flatMap((tab, index) => [
1398
+ ...(index > 0
1399
+ ? [{
1400
+ tag: "column",
1401
+ width: "auto",
1402
+ vertical_align: "center",
1403
+ elements: [{ ...markdown("·"), text_align: "center", text_size: "notation" }],
1404
+ }]
1405
+ : []),
1406
+ {
1407
+ tag: "column",
1408
+ width: "auto",
1409
+ vertical_align: "center",
1410
+ elements: [settingsTabElement(tab.label, tab.id === activeTab, {
1411
+ action: "settings_tab_open",
1412
+ ...baseAction,
1413
+ tab: tab.id,
1414
+ })],
1415
+ },
1416
+ ]),
1417
+ };
1418
+ }
1419
+
1420
+ function settingsTabElement(
1421
+ label: string,
1422
+ active: boolean,
1423
+ value: Record<string, string>,
1424
+ ): Record<string, unknown> {
1425
+ if (active) {
1426
+ return {
1427
+ ...markdown(escapeCardHtml(label)),
1428
+ text_align: "center",
1429
+ text_size: "notation",
1430
+ };
1431
+ }
1432
+ const action = taskActionElement({ text: label, value });
1433
+ return {
1434
+ ...action,
1435
+ padding: "6px 0px",
1436
+ elements: (action.elements as Array<Record<string, unknown>>).map((element) => ({
1437
+ ...element,
1438
+ text_align: "center",
1439
+ text_size: "notation",
1440
+ })),
1441
+ };
1442
+ }
1443
+
1444
+ function settingsOptionRow(input: {
1445
+ label: string;
1446
+ current: boolean;
1447
+ action: TaskListCardAction;
1448
+ }): Record<string, unknown> {
1449
+ return {
1450
+ tag: "column_set",
1451
+ flex_mode: "none",
1452
+ horizontal_spacing: "8px",
1453
+ vertical_align: "center",
1454
+ columns: [
1455
+ {
1456
+ tag: "column",
1457
+ width: "weighted",
1458
+ weight: 1,
1459
+ vertical_align: "center",
1460
+ elements: [markdown(input.label)],
1461
+ },
1462
+ {
1463
+ tag: "column",
1464
+ width: "auto",
1465
+ vertical_align: "center",
1466
+ elements: input.current ? [markdown("✅ 当前")] : [taskActionElement(input.action)],
1467
+ },
1468
+ ],
1469
+ };
1470
+ }
1471
+
1065
1472
  function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
1066
1473
  return {
1067
1474
  tag: "column_set",
@@ -233,8 +233,16 @@ export class FeishuMessageClient implements FeishuOutbound {
233
233
  }
234
234
 
235
235
  async updateInteractiveCard(messageId: string, card: Record<string, unknown>): Promise<void> {
236
+ const preparedCard = await this.prepareInteractiveCard(card);
237
+ await this.updateInteractiveCardNow(messageId, preparedCard);
238
+ }
239
+
240
+ private async updateInteractiveCardNow(
241
+ messageId: string,
242
+ card: Record<string, unknown>,
243
+ allowAuditFallback = true,
244
+ ): Promise<void> {
236
245
  try {
237
- const preparedCard = await this.prepareInteractiveCard(card);
238
246
  const token = await this.getTenantAccessToken();
239
247
  const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages/${messageId}`, {
240
248
  method: "PATCH",
@@ -242,14 +250,26 @@ export class FeishuMessageClient implements FeishuOutbound {
242
250
  Authorization: `Bearer ${token}`,
243
251
  "Content-Type": "application/json; charset=utf-8",
244
252
  },
245
- body: JSON.stringify({ content: JSON.stringify(preparedCard) }),
253
+ body: JSON.stringify({ content: JSON.stringify(card) }),
246
254
  });
247
255
  const payload = (await response.json()) as SendMessageResponse;
248
256
  if (!response.ok || payload.code !== 0) {
249
257
  throw new FeishuApiError(payload.msg || response.statusText, payload.code, payload, "update", response.status);
250
258
  }
251
259
  } catch (error) {
252
- throw normalizeTransportError(error, "update");
260
+ const normalized = normalizeTransportError(error, "update");
261
+ const fallback = allowAuditFallback && normalized.isEmailAddressAuditFailure
262
+ ? sanitizeEmailAddressesInContent(card)
263
+ : undefined;
264
+ if (fallback) {
265
+ this.logger.warn(
266
+ { code: normalized.code, messageId },
267
+ "Feishu rejected a card update as email-sensitive; retrying with audit-safe text.",
268
+ );
269
+ await this.updateInteractiveCardNow(messageId, fallback, false);
270
+ return;
271
+ }
272
+ throw normalized;
253
273
  }
254
274
  }
255
275
 
@@ -278,6 +298,7 @@ export class FeishuMessageClient implements FeishuOutbound {
278
298
  msgType: string,
279
299
  content: Record<string, unknown>,
280
300
  idempotencyKey?: string,
301
+ allowAuditFallback = true,
281
302
  ): Promise<string | undefined> {
282
303
  let token: string;
283
304
  try {
@@ -317,6 +338,16 @@ export class FeishuMessageClient implements FeishuOutbound {
317
338
  lastError = error;
318
339
 
319
340
  if (!error.isRetryable || attempt === 2) {
341
+ const fallback = allowAuditFallback && error.isEmailAddressAuditFailure
342
+ ? sanitizeEmailAddressesInContent(content)
343
+ : undefined;
344
+ if (fallback) {
345
+ this.logger.warn(
346
+ { code: error.code, messageId: target.messageId, msgType },
347
+ "Feishu rejected a reply as email-sensitive; retrying with audit-safe text.",
348
+ );
349
+ return this.replyMessageNow(target, msgType, fallback, idempotencyKey, false);
350
+ }
320
351
  this.logger.error({ error, messageId: target.messageId, msgType }, "Failed to reply to Feishu message.");
321
352
  throw error;
322
353
  }
@@ -334,6 +365,7 @@ export class FeishuMessageClient implements FeishuOutbound {
334
365
  msgType: string,
335
366
  content: Record<string, unknown>,
336
367
  idempotencyKey?: string,
368
+ allowAuditFallback = true,
337
369
  ): Promise<string | undefined> {
338
370
  let token: string;
339
371
  try {
@@ -376,6 +408,16 @@ export class FeishuMessageClient implements FeishuOutbound {
376
408
  lastError = error;
377
409
 
378
410
  if (!error.isRetryable || attempt === 2) {
411
+ const fallback = allowAuditFallback && error.isEmailAddressAuditFailure
412
+ ? sanitizeEmailAddressesInContent(content)
413
+ : undefined;
414
+ if (fallback) {
415
+ this.logger.warn(
416
+ { code: error.code, contextKey, msgType },
417
+ "Feishu rejected a message as email-sensitive; retrying with audit-safe text.",
418
+ );
419
+ return this.sendMessageNow(contextKey, msgType, fallback, idempotencyKey, false);
420
+ }
379
421
  this.logger.error({ error, contextKey, msgType }, "Failed to send Feishu message.");
380
422
  throw error;
381
423
  }
@@ -573,6 +615,7 @@ export class FeishuMessageClient implements FeishuOutbound {
573
615
  export class FeishuApiError extends Error {
574
616
  readonly isRateLimit: boolean;
575
617
  readonly isRetryable: boolean;
618
+ readonly isEmailAddressAuditFailure: boolean;
576
619
 
577
620
  constructor(
578
621
  message: string,
@@ -591,6 +634,7 @@ export class FeishuApiError extends Error {
591
634
  this.name = "FeishuApiError";
592
635
  this.isRateLimit = code === 230020 || message.toLowerCase().includes("frequency limit");
593
636
  this.isRetryable = forceRetryable || this.isRateLimit || (httpStatus !== undefined && httpStatus >= 500);
637
+ this.isEmailAddressAuditFailure = code === 230028 && message.includes("EMAIL_ADDRESS");
594
638
  }
595
639
  }
596
640
 
@@ -642,6 +686,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
642
686
  return typeof value === "object" && value !== null && !Array.isArray(value);
643
687
  }
644
688
 
689
+ function sanitizeEmailAddressesInContent(content: Record<string, unknown>): Record<string, unknown> | undefined {
690
+ let changed = false;
691
+ const sanitize = (value: unknown): unknown => {
692
+ if (typeof value === "string") {
693
+ const sanitized = value.replace(
694
+ /\b([a-z0-9._%+-]+)@([a-z0-9.-]+\.[a-z]{2,})\b/gi,
695
+ "$1 [at] $2",
696
+ );
697
+ if (sanitized !== value) changed = true;
698
+ return sanitized;
699
+ }
700
+ if (Array.isArray(value)) return value.map(sanitize);
701
+ if (!isRecord(value)) return value;
702
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, sanitize(child)]));
703
+ };
704
+ const sanitized = sanitize(content) as Record<string, unknown>;
705
+ return changed ? sanitized : undefined;
706
+ }
707
+
645
708
  function cardImageLabel(image: Record<string, unknown>): string {
646
709
  const alt = image.alt;
647
710
  if (isRecord(alt) && typeof alt.content === "string" && alt.content.trim()) return alt.content;