lark-remote 0.1.7 → 0.1.8

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 (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/approval-coordinator.d.ts +15 -1
  3. package/dist/bridge/approval-coordinator.js +35 -2
  4. package/dist/bridge/index.d.ts +19 -6
  5. package/dist/bridge/index.js +25 -8
  6. package/dist/card/approval-render.js +187 -45
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +28 -14
  14. package/dist/router/card-action-payload.d.ts +30 -0
  15. package/dist/router/card-action-payload.js +39 -0
  16. package/dist/router/config/claude.js +8 -3
  17. package/dist/router/config/dsh.d.ts +51 -0
  18. package/dist/router/config/dsh.js +179 -0
  19. package/dist/router/config/index.d.ts +1 -1
  20. package/dist/router/config/index.js +4 -1
  21. package/dist/router/config/types.d.ts +6 -0
  22. package/dist/router/index.d.ts +20 -1
  23. package/dist/router/index.js +113 -7
  24. package/dist/router/utils.js +12 -2
  25. package/dist/runner/agent-choices-common.js +3 -0
  26. package/dist/runner/claude/session.js +24 -5
  27. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  28. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  29. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  30. package/dist/runner/codex/app-server/runner.js +56 -1
  31. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  32. package/dist/runner/codex/app-server/translator.js +45 -0
  33. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  34. package/dist/runner/common/acp/protocol-types.js +2 -0
  35. package/dist/runner/dsh/client.d.ts +95 -0
  36. package/dist/runner/dsh/client.js +418 -0
  37. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  38. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  39. package/dist/runner/dsh/index.d.ts +3 -0
  40. package/dist/runner/dsh/index.js +4 -0
  41. package/dist/runner/dsh/runner.d.ts +57 -0
  42. package/dist/runner/dsh/runner.js +191 -0
  43. package/dist/runner/dsh/translator.d.ts +28 -0
  44. package/dist/runner/dsh/translator.js +98 -0
  45. package/dist/runner/dsh/types.d.ts +155 -0
  46. package/dist/runner/dsh/types.js +14 -0
  47. package/dist/runner/index.d.ts +1 -0
  48. package/dist/runner/index.js +1 -0
  49. package/dist/runner/kimi/acp/runner.js +69 -1
  50. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  51. package/dist/runner/kimi/acp/translator.js +108 -3
  52. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  53. package/dist/runner/pi/rpc/runner.js +2 -11
  54. package/dist/runner/probe.js +6 -1
  55. package/dist/runner/question-common.d.ts +26 -0
  56. package/dist/runner/question-common.js +42 -0
  57. package/dist/runner/types.d.ts +63 -10
  58. package/dist/session/claude/sessions.js +37 -15
  59. package/dist/session/dsh/index.d.ts +1 -0
  60. package/dist/session/dsh/index.js +2 -0
  61. package/dist/session/dsh/sessions.d.ts +39 -0
  62. package/dist/session/dsh/sessions.js +237 -0
  63. package/dist/session/index.d.ts +1 -0
  64. package/dist/session/index.js +1 -0
  65. package/dist/workspace/index.d.ts +4 -0
  66. package/dist/workspace/index.js +8 -1
  67. package/package.json +2 -1
@@ -0,0 +1,179 @@
1
+ /**
2
+ * DshConfigBuilder — /config card fields for the DSH agent.
3
+ *
4
+ * DSH (DeepSeek Harness) is an HTTP-only agent. Beyond the Web Host base URL it
5
+ * exposes three runtime-configurable knobs — preset, model, reasoning effort.
6
+ *
7
+ * 清单来源分两期:
8
+ * - 动态清单(当前实现):打开 /config 前通过 `llm.models` / `agentPreset.list`
9
+ * 预取(Router 调 `prefetch()`),失败/离线回退下方固定兜底清单,卡不依赖
10
+ * DSH 服务在线。
11
+ * - 三个可选字段用空字符串哨兵表示「跟随服务端默认」:select 的 value `''`
12
+ * 在 handleFieldChange 中被转成 undefined patch(router 删除键),config.yaml
13
+ * 不写该键 = 服务端默认。
14
+ */
15
+ import { DEFAULT_DSH_HOST } from '../../config/index.js';
16
+ import { DshClient } from '../../runner/dsh/client.js';
17
+ import { getLogger } from '../../logger/index.js';
18
+ /** 固定兜底 preset 清单(DSH rc.7 实测;动态拉取失败时使用)。 */
19
+ const FALLBACK_PRESET_OPTIONS = [
20
+ { text: 'standard(标准模式)', value: 'standard' },
21
+ { text: 'minimal(极简模式)', value: 'minimal' },
22
+ { text: 'code(PTC 模式)', value: 'code' },
23
+ { text: 'cordis(创造模式)', value: 'cordis' },
24
+ ];
25
+ /** 固定兜底模型清单(DSH rc.7 实测;动态拉取失败时使用)。 */
26
+ const FALLBACK_MODEL_OPTIONS = ['deepseek-v4-flash', 'deepseek-v4-pro'];
27
+ /** 固定兜底推理档位(llm.models 实际档位;动态拉取失败时使用)。 */
28
+ const FALLBACK_EFFORT_OPTIONS = ['off', 'low', 'high', 'max'];
29
+ /** 「跟随服务端默认」哨兵值:select value 用空串,保存时转 undefined 删除键。 */
30
+ const DEFAULT_SENTINEL = '';
31
+ export class DshConfigBuilder {
32
+ /** 最近一次预取用的 host;host 变更时缓存失效(下一张卡重新拉取)。 */
33
+ prefetchHost;
34
+ modelCatalog;
35
+ presets;
36
+ /** 可注入 client 工厂(测试用);默认生产实现。 */
37
+ clientFactory;
38
+ constructor(clientFactory) {
39
+ this.clientFactory = clientFactory ?? ((base) => new DshClient(base));
40
+ }
41
+ /** 异步预取模型/预设目录(§4.2 二期动态清单)。失败/离线静默回退固定兜底清单,
42
+ * 卡不依赖 DSH 服务在线;同一 host 只拉一次,避免每次刷卡重复请求。 */
43
+ async prefetch(host) {
44
+ const base = (host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
45
+ if (!base || base === this.prefetchHost)
46
+ return;
47
+ const client = this.clientFactory(base);
48
+ try {
49
+ this.modelCatalog = await client.listModels();
50
+ }
51
+ catch (err) {
52
+ getLogger().warn(`[dsh-config] llm.models prefetch failed (falling back to fixed catalog): ${err.message}`);
53
+ this.modelCatalog = undefined;
54
+ }
55
+ try {
56
+ this.presets = await client.listPresets();
57
+ }
58
+ catch (err) {
59
+ getLogger().warn(`[dsh-config] agentPreset.list prefetch failed (falling back to fixed presets): ${err.message}`);
60
+ this.presets = undefined;
61
+ }
62
+ this.prefetchHost = base;
63
+ }
64
+ /** 同步注入 catalog/presets(测试用,替代异步 prefetch;标记 prefetchHost 使
65
+ * 后续 prefetch 短路,避免测试误触发网络请求)。 */
66
+ setCatalog(catalog, presets) {
67
+ this.modelCatalog = catalog;
68
+ if (presets)
69
+ this.presets = presets;
70
+ this.prefetchHost = 'injected';
71
+ }
72
+ /** 展平 llm.models 各 provider group 的模型 id;无 catalog 时回退固定清单。 */
73
+ modelOptions() {
74
+ if (this.modelCatalog) {
75
+ const ids = this.modelCatalog.groups.flatMap((g) => g.models.map((m) => m.id));
76
+ if (ids.length > 0)
77
+ return ids;
78
+ }
79
+ return FALLBACK_MODEL_OPTIONS;
80
+ }
81
+ /** preset 清单;无 presets 时回退固定清单。显示名优先用 preset.yml 的 name。 */
82
+ presetOptions() {
83
+ if (this.presets && this.presets.presets.length > 0) {
84
+ return this.presets.presets
85
+ .filter((p) => !p.broken)
86
+ .map((p) => ({ text: p.name ? `${p.id}(${p.name})` : p.id, value: p.id }));
87
+ }
88
+ return FALLBACK_PRESET_OPTIONS;
89
+ }
90
+ /** 模型在 catalog 中声明的推理档位;无 catalog/模型不在目录时回退固定档位。 */
91
+ effortOptionsForModel(model) {
92
+ if (model && this.modelCatalog) {
93
+ for (const group of this.modelCatalog.groups) {
94
+ const entry = group.models.find((m) => m.id === model);
95
+ if (entry?.reasoning && entry.reasoning.efforts.length > 0) {
96
+ return entry.reasoning.efforts.map((e) => e.id);
97
+ }
98
+ }
99
+ }
100
+ return FALLBACK_EFFORT_OPTIONS;
101
+ }
102
+ buildFields(displayConfig) {
103
+ const dsh = displayConfig.agents?.dsh;
104
+ const currentHost = dsh?.host ?? DEFAULT_DSH_HOST;
105
+ const currentPreset = dsh?.agentPreset ?? DEFAULT_SENTINEL;
106
+ const currentModel = dsh?.model ?? DEFAULT_SENTINEL;
107
+ const currentEffort = dsh?.reasoningEffort ?? DEFAULT_SENTINEL;
108
+ const modelOptions = this.modelOptions();
109
+ const isCustomModel = currentModel !== DEFAULT_SENTINEL && !modelOptions.includes(currentModel);
110
+ const effortOptions = this.effortOptionsForModel(currentModel === DEFAULT_SENTINEL ? undefined : currentModel);
111
+ return [
112
+ {
113
+ key: 'agents.dsh.host',
114
+ label: 'DSH Web Host',
115
+ type: 'input',
116
+ currentValue: currentHost,
117
+ },
118
+ {
119
+ key: 'agents.dsh.agentPreset',
120
+ label: '预设模式',
121
+ type: 'select',
122
+ options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...this.presetOptions()],
123
+ currentValue: currentPreset,
124
+ },
125
+ {
126
+ key: 'agents.dsh.model',
127
+ label: '使用模型',
128
+ type: 'select',
129
+ options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...modelOptions],
130
+ currentValue: isCustomModel ? undefined : currentModel,
131
+ },
132
+ // 自定义模型名输入框(与 codex/claude 一致的行为)
133
+ {
134
+ key: 'agents.dsh.model',
135
+ label: '自定义模型名',
136
+ type: 'input',
137
+ currentValue: isCustomModel ? currentModel : '',
138
+ },
139
+ {
140
+ key: 'agents.dsh.reasoningEffort',
141
+ label: '推理强度',
142
+ type: 'select',
143
+ options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...effortOptions],
144
+ currentValue: currentEffort,
145
+ },
146
+ ];
147
+ }
148
+ handleFieldChange(key, value, config) {
149
+ const patches = [];
150
+ // 空串哨兵「跟随服务端默认」→ undefined patch(router 删除键)
151
+ const effective = value === DEFAULT_SENTINEL ? undefined : value;
152
+ patches.push({ key, value: effective });
153
+ // 模型变更时,若当前档位不被新模型支持,重置(参照 kimi/codex 语义)
154
+ if (key === 'agents.dsh.model' && typeof value === 'string') {
155
+ const effortPatch = this.effortPatchForModel(config, effective);
156
+ if (effortPatch)
157
+ patches.push(effortPatch);
158
+ }
159
+ return patches;
160
+ }
161
+ /** 切到 newModel 后的档位补丁:当前档位仍被新模型支持 → 不产出;否则用新模型
162
+ * 目录中位;无目录/空列表 → undefined(清空 = 跟随服务端默认)。 */
163
+ effortPatchForModel(config, newModel) {
164
+ const currentEffort = config.agents?.dsh?.reasoningEffort;
165
+ if (!newModel) {
166
+ // 模型被清空(跟随默认)→ 档位一并清空跟随默认
167
+ return currentEffort ? { key: 'agents.dsh.reasoningEffort', value: undefined } : null;
168
+ }
169
+ const supported = this.effortOptionsForModel(newModel);
170
+ if (currentEffort && supported.includes(currentEffort))
171
+ return null;
172
+ // 新模型目录中位档位;偶数长度取偏高位(off/low → low,符合 DSH 默认 high 语义)
173
+ const middle = supported[Math.floor(supported.length / 2)];
174
+ return middle
175
+ ? { key: 'agents.dsh.reasoningEffort', value: middle }
176
+ : { key: 'agents.dsh.reasoningEffort', value: undefined };
177
+ }
178
+ }
179
+ //# sourceMappingURL=dsh.js.map
@@ -7,7 +7,7 @@
7
7
  import type { AgentKind } from '../../runner/types.js';
8
8
  import type { AgentConfigCardBuilder } from './types.js';
9
9
  /**
10
- * 首次启动默认展示的 Coding Agent 顺序:Codex → Claude → OpenCode → Pi → Kimi。
10
+ * 首次启动默认展示的 Coding Agent 顺序:Codex → Claude → OpenCode → Pi → Kimi → DSH。
11
11
  * 未安装(availability === false)的 agent 在展示时排到后面
12
12
  * (见 sortAgentsForDisplay),同组内保持本顺序。
13
13
  */
@@ -9,8 +9,9 @@ import { CodexConfigBuilder } from './codex.js';
9
9
  import { PiConfigBuilder } from './pi.js';
10
10
  import { OpencodeConfigBuilder } from './opencode.js';
11
11
  import { KimiConfigBuilder } from './kimi.js';
12
+ import { DshConfigBuilder } from './dsh.js';
12
13
  /**
13
- * 首次启动默认展示的 Coding Agent 顺序:Codex → Claude → OpenCode → Pi → Kimi。
14
+ * 首次启动默认展示的 Coding Agent 顺序:Codex → Claude → OpenCode → Pi → Kimi → DSH。
14
15
  * 未安装(availability === false)的 agent 在展示时排到后面
15
16
  * (见 sortAgentsForDisplay),同组内保持本顺序。
16
17
  */
@@ -20,6 +21,7 @@ export const DEFAULT_AGENT_ORDER = [
20
21
  'opencode',
21
22
  'pi',
22
23
  'kimi',
24
+ 'dsh',
23
25
  ];
24
26
  /** Registry of config builders, keyed by agent kind. */
25
27
  const builders = {
@@ -28,6 +30,7 @@ const builders = {
28
30
  pi: new PiConfigBuilder(),
29
31
  opencode: new OpencodeConfigBuilder(),
30
32
  kimi: new KimiConfigBuilder(),
33
+ dsh: new DshConfigBuilder(),
31
34
  };
32
35
  /**
33
36
  * Get the config builder for a specific agent.
@@ -49,4 +49,10 @@ export interface AgentConfigCardBuilder {
49
49
  key: string;
50
50
  value: unknown;
51
51
  }>;
52
+ /**
53
+ * 可选的异步预取钩子:卡片渲染前拉取动态选项(如 DSH 的模型/预设目录)。
54
+ * host 为该 agent 的当前连接地址;实现内部应缓存(同一 host 只拉一次),
55
+ * 失败静默回退固定兜底,不阻断卡片渲染。非异步预取的 agent 可不实现。
56
+ */
57
+ prefetch?(host?: string): Promise<void>;
52
58
  }
@@ -72,6 +72,8 @@ export interface CardActionPayload {
72
72
  selected?: boolean;
73
73
  /** Claude AskUserQuestion 问题索引(approval.answer / approval.answerSubmit)。 */
74
74
  questionIndex?: number;
75
+ /** 多选组件(multi_select_static 等)回传的选中值数组(预留公共层能力)。 */
76
+ options?: string[];
75
77
  }
76
78
  /**
77
79
  * Response a cardAction handler may return to give the clicking user native
@@ -89,7 +91,7 @@ interface CardActionResponse {
89
91
  }
90
92
  export declare class CommandRouter {
91
93
  /** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
92
- static readonly VALID_AGENTS: readonly ["claude", "codex", "opencode", "pi", "kimi"];
94
+ static readonly VALID_AGENTS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
93
95
  private sessionStore;
94
96
  /** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
95
97
  bridge: Bridge;
@@ -203,6 +205,9 @@ export declare class CommandRouter {
203
205
  * Routes to the appropriate bridge method based on value.cmd.
204
206
  * Returns a toast response for immediate user feedback.
205
207
  */
208
+ /** 答案类操作的统一失败反馈:重复投递(同一 nonce 第二次点击)是已生效的
209
+ * 中性事件,报 error 会误导(首次点击实际已成功)。 */
210
+ private answerFailureToast;
206
211
  private handleApprovalAction;
207
212
  /**
208
213
  * Handle queue.cancel: remove this message from the queue.
@@ -434,6 +439,14 @@ export declare class CommandRouter {
434
439
  * /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
435
440
  * 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
436
441
  */
442
+ /**
443
+ * DSH /resume preset 一致性校验:返回 mismatch 详情,或 undefined(一致/不可判定)。
444
+ *
445
+ * session 的 preset 在创建时固定(§2.1)。通过 listSessions 拿到目标 session 的
446
+ * agentPreset 与当前配置比对:不一致时不静默复用(返回提示),一致或查询失败
447
+ * (无法判定)时不阻断,避免 DSH 离线导致 /resume 完全不可用。
448
+ */
449
+ private dshPresetMismatch;
437
450
  cmdResume(args: string[], ctx: CommandContext, offset?: number): CommandResult;
438
451
  private cmdActive;
439
452
  /**
@@ -444,6 +457,12 @@ export declare class CommandRouter {
444
457
  private buildActiveCardFromMemory;
445
458
  private cmdReconnect;
446
459
  private cmdConfig;
460
+ /**
461
+ * DSH /config 卡片构建前异步预取模型/预设目录(§4.2 动态清单)。
462
+ * 只在 defaultAgent 为 dsh 时生效;其他 agent 无 prefetch 钩子直接跳过。
463
+ * 预取失败不阻断——DshConfigBuilder 内部已回退固定兜底清单。
464
+ */
465
+ private prefetchConfigCardCatalogs;
447
466
  /** Build an interactive config card with CardKit 2.0 tabs + batch save */
448
467
  buildConfigCard(): CommandResult;
449
468
  /** /order 实现(public:测试直接调用,替代 as unknown as)。 */
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import YAML from 'yaml';
5
- import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, walkNestedContainer, } from '../config/index.js';
5
+ import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, assertSafeKeyPart, walkNestedContainer, getAgentConfig, } from '../config/index.js';
6
6
  import { syncAgentChoices } from '../runner/index.js';
7
7
  import { WorkspaceStore } from '../workspace/index.js';
8
8
  import { OrderStore } from '../order/index.js';
@@ -121,11 +121,12 @@ export function isImmediateAction(cmd) {
121
121
  cmd === 'approval.toggle' ||
122
122
  cmd === 'approval.answer' ||
123
123
  cmd === 'approval.answerSubmit' ||
124
- cmd === 'approval.answerCustom');
124
+ cmd === 'approval.answerCustom' ||
125
+ cmd === 'approval.answerNote');
125
126
  }
126
127
  export class CommandRouter {
127
128
  /** Valid agent kinds — single source of truth for resume.use / resume.page / cmdResume. */
128
- static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi'];
129
+ static VALID_AGENTS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
129
130
  sessionStore;
130
131
  /** 飞书桥接实例(public:测试直接访问断言,替代 as unknown as)。 */
131
132
  bridge;
@@ -365,6 +366,7 @@ export class CommandRouter {
365
366
  case 'approval.answer':
366
367
  case 'approval.answerSubmit':
367
368
  case 'approval.answerCustom':
369
+ case 'approval.answerNote':
368
370
  return this.handleApprovalAction(value, ctx);
369
371
  case 'codex.compact':
370
372
  await this.bridge.handleCodexCompact(value, ctx);
@@ -403,6 +405,14 @@ export class CommandRouter {
403
405
  * Routes to the appropriate bridge method based on value.cmd.
404
406
  * Returns a toast response for immediate user feedback.
405
407
  */
408
+ /** 答案类操作的统一失败反馈:重复投递(同一 nonce 第二次点击)是已生效的
409
+ * 中性事件,报 error 会误导(首次点击实际已成功)。 */
410
+ answerFailureToast(msg) {
411
+ if (/already submitted \(duplicate nonce\)/.test(msg)) {
412
+ return { toast: { type: 'info', content: '该选项已处理,请勿重复点击' } };
413
+ }
414
+ return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
415
+ }
406
416
  async handleApprovalAction(value, _ctx) {
407
417
  const { runId, requestId, decision, nonce, permId } = value;
408
418
  if (value.cmd === 'approval.respond') {
@@ -465,7 +475,7 @@ export class CommandRouter {
465
475
  }
466
476
  catch (err) {
467
477
  const msg = err instanceof Error ? err.message : String(err);
468
- return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
478
+ return this.answerFailureToast(msg);
469
479
  }
470
480
  }
471
481
  if (value.cmd === 'approval.answerSubmit') {
@@ -483,7 +493,7 @@ export class CommandRouter {
483
493
  }
484
494
  catch (err) {
485
495
  const msg = err instanceof Error ? err.message : String(err);
486
- return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
496
+ return this.answerFailureToast(msg);
487
497
  }
488
498
  }
489
499
  if (value.cmd === 'approval.answerCustom') {
@@ -506,7 +516,30 @@ export class CommandRouter {
506
516
  }
507
517
  catch (err) {
508
518
  const msg = err instanceof Error ? err.message : String(err);
509
- return { toast: { type: 'error', content: `答案提交失败:${msg}` } };
519
+ return this.answerFailureToast(msg);
520
+ }
521
+ }
522
+ if (value.cmd === 'approval.answerNote') {
523
+ if (!runId ||
524
+ requestId === undefined ||
525
+ value.questionIndex === undefined ||
526
+ !nonce ||
527
+ !value.inputValue) {
528
+ return { toast: { type: 'error', content: '缺少补充说明参数' } };
529
+ }
530
+ try {
531
+ await this.bridge.handleApprovalAnswerNote({
532
+ runId,
533
+ requestId,
534
+ questionIndex: value.questionIndex,
535
+ text: value.inputValue,
536
+ nonce,
537
+ });
538
+ return { toast: { type: 'success', content: '补充说明已保存' } };
539
+ }
540
+ catch (err) {
541
+ const msg = err instanceof Error ? err.message : String(err);
542
+ return this.answerFailureToast(msg);
510
543
  }
511
544
  }
512
545
  return { toast: { type: 'error', content: '未知的审批操作' } };
@@ -1144,7 +1177,7 @@ export class CommandRouter {
1144
1177
  // immediate action the callback response is swallowed by enqueueImmediate
1145
1178
  // (fire-and-forget), and even if delivered it is transient — the user gets
1146
1179
  // no perceivable feedback on the switch. Same rationale as config.save
1147
- // agent-switch notice (74e99f0): persistent message first, toast only as a
1180
+ // agent-switch notice: persistent message first, toast only as a
1148
1181
  // fallback if sendResult fails.
1149
1182
  let fallbackToast;
1150
1183
  if (useResult.card || useResult.text) {
@@ -1377,6 +1410,8 @@ export class CommandRouter {
1377
1410
  // 2026-08-03: 卡片刷新失败不吞切换通知——保存已成功,仅记录日志,
1378
1411
  // 后续切换消息照常发送;真正写盘/传播阶段的失败才走外层 catch 报「保存失败」。
1379
1412
  try {
1413
+ // host 可能已变更 → 重新预取 DSH 目录(内部按 host 缓存,未变不重复拉)
1414
+ await this.prefetchConfigCardCatalogs();
1380
1415
  await this.bridge.updateCardInPlace(this.buildConfigCard().card, ctx);
1381
1416
  }
1382
1417
  catch (refreshErr) {
@@ -1553,6 +1588,24 @@ export class CommandRouter {
1553
1588
  switchSessionId = '';
1554
1589
  }
1555
1590
  }
1591
+ // DSH preset 变更 = 下次 run 新建 session(§4.3 D1):
1592
+ // preset 在 session 创建时固定,中途切换会被服务端拒绝(agent-preset-conflict)。
1593
+ // 旧 sessionId 存入 previousSessions 停车位(可手动 /resume,但 /resume 会做
1594
+ // preset 一致性校验),当前 sessionId 清空——下次 run 走 session.create 新建。
1595
+ const hasDshPresetChange = Object.keys(updates).some((k) => k === 'agents.dsh.agentPreset');
1596
+ // 已发生 agent 切换时(switchNotice 已设置)preset 分支跳过,避免覆盖切换 notice;
1597
+ // 切换分支对 dsh 的 session 已做恢复/清空处理。
1598
+ if (hasDshPresetChange && this.config.defaultAgent === 'dsh' && !switchNotice) {
1599
+ const oldSessionId = this.sessionStore.getSessionId(ctx.userId, 'dsh');
1600
+ if (oldSessionId) {
1601
+ this.sessionStore.setPreviousSessionId(ctx.userId, 'dsh', oldSessionId);
1602
+ this.sessionStore.clearSessionId(ctx.userId, 'dsh');
1603
+ switchNotice = `DSH preset 已变更,旧会话已停放到恢复槽(可 /resume 校验后恢复),下次消息将新建会话`;
1604
+ }
1605
+ else {
1606
+ switchNotice = `DSH preset 已变更,下次消息将新建会话`;
1607
+ }
1608
+ }
1556
1609
  // 2026-07-13: 同步 agent 配置到 agentChoices(用于切换 agent 时恢复配置)
1557
1610
  const currentAgent = this.config.defaultAgent;
1558
1611
  const currentAgentConfigKey = `agents.${currentAgent}`;
@@ -2579,6 +2632,32 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2579
2632
  * /resume 核心实现(public:handleResumePage / handle / resume.use 共用,
2580
2633
  * 也是测试的直接 seam,避免测试用 @ts-expect-error 摸私有方法)。
2581
2634
  */
2635
+ /**
2636
+ * DSH /resume preset 一致性校验:返回 mismatch 详情,或 undefined(一致/不可判定)。
2637
+ *
2638
+ * session 的 preset 在创建时固定(§2.1)。通过 listSessions 拿到目标 session 的
2639
+ * agentPreset 与当前配置比对:不一致时不静默复用(返回提示),一致或查询失败
2640
+ * (无法判定)时不阻断,避免 DSH 离线导致 /resume 完全不可用。
2641
+ */
2642
+ dshPresetMismatch(reader, sessionId, cwd) {
2643
+ const currentPreset = this.config.agents?.dsh?.agentPreset;
2644
+ if (!currentPreset)
2645
+ return undefined; // 配置跟随服务端默认,无法判定 → 放行
2646
+ let sessionPreset;
2647
+ try {
2648
+ const { sessions } = reader.listSessions(cwd, { limit: 100 });
2649
+ sessionPreset = sessions.find((s) => s.sessionId === sessionId)?.agentPreset;
2650
+ }
2651
+ catch (err) {
2652
+ getLogger().warn(`[router] dshPresetMismatch listSessions failed for ${sessionId}: ${err.message}`);
2653
+ return undefined;
2654
+ }
2655
+ if (!sessionPreset)
2656
+ return undefined; // 服务端未返回 preset → 无法判定,放行
2657
+ if (sessionPreset === currentPreset)
2658
+ return undefined;
2659
+ return { sessionPreset, currentPreset };
2660
+ }
2582
2661
  cmdResume(args, ctx, offset = 0) {
2583
2662
  const entry = this.sessionStore.get(ctx.userId);
2584
2663
  const cwd = entry?.cwd;
@@ -2625,6 +2704,19 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2625
2704
  text: `未找到 session ${sessionIdArg}(当前目录: ${cwd})。请确认该 session 属于此目录,或先 /cd 到正确目录。`,
2626
2705
  };
2627
2706
  }
2707
+ // DSH preset 一致性校验(§6 风险 1):session 的 preset 在创建时固定,
2708
+ // 若与当前配置不符,直接复用会让后续 run 报 agent-preset-conflict。
2709
+ // 不静默复用——明确提示用户,由用户决定是否继续(配置里手动改/清 preset)。
2710
+ if (agentKind === 'dsh' && this.config.defaultAgent === 'dsh') {
2711
+ const presetMismatch = this.dshPresetMismatch(reader, sessionIdArg, cwd);
2712
+ if (presetMismatch) {
2713
+ return {
2714
+ text: `⚠️ 该 DSH session 的 preset 是「${presetMismatch.sessionPreset}」,与当前配置「${presetMismatch.currentPreset}」不一致。\n` +
2715
+ `preset 在 session 创建时固定,无法中途切换(DSH 返回 agent-preset-conflict)。\n` +
2716
+ `继续恢复将在下次 run 失败。请在 /config 中把预设模式改回「${presetMismatch.sessionPreset}」后重试,或保持当前配置并发新消息新建会话。`,
2717
+ };
2718
+ }
2719
+ }
2628
2720
  // L3: 校验通过后才写入
2629
2721
  this.sessionStore.setSessionId(ctx.userId, agentKind, sessionIdArg, cwd);
2630
2722
  const { events: content, usage, isActive, aiTitle, recap, displayTitle, activeRunRunId, } = this.readSessionDisplayState(sessionIdArg, cwd, {
@@ -2984,6 +3076,8 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
2984
3076
  async cmdConfig(args, ctx) {
2985
3077
  // Ensure probe cache is populated before building the card.
2986
3078
  await probeAllAgents();
3079
+ // 动态清单预取(DSH 模型/预设目录):卡渲染前拉取,失败回退兜底清单
3080
+ await this.prefetchConfigCardCatalogs();
2987
3081
  // Check if this is a direct set command: /config <key> <value>
2988
3082
  // Command-style set: immediate write to disk, clear pendingConfig
2989
3083
  if (args.length >= 2) {
@@ -3030,6 +3124,18 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3030
3124
  this.ensurePendingConfig();
3031
3125
  return this.buildConfigCard();
3032
3126
  }
3127
+ /**
3128
+ * DSH /config 卡片构建前异步预取模型/预设目录(§4.2 动态清单)。
3129
+ * 只在 defaultAgent 为 dsh 时生效;其他 agent 无 prefetch 钩子直接跳过。
3130
+ * 预取失败不阻断——DshConfigBuilder 内部已回退固定兜底清单。
3131
+ */
3132
+ async prefetchConfigCardCatalogs() {
3133
+ if (this.config.defaultAgent !== 'dsh')
3134
+ return;
3135
+ const builder = getConfigBuilder('dsh');
3136
+ const dshConf = getAgentConfig(this.config, 'dsh');
3137
+ await builder.prefetch?.(dshConf?.host);
3138
+ }
3033
3139
  /** Build an interactive config card with CardKit 2.0 tabs + batch save */
3034
3140
  buildConfigCard() {
3035
3141
  // 使用 pendingConfig(若存在)或当前 config
@@ -80,6 +80,16 @@ export function formatUsageStats(usage, options) {
80
80
  const cumTotalSuffix = usage?.cumulativeTotalTokens !== undefined
81
81
  ? ` · 累计 ${formatTokenK(usage.cumulativeTotalTokens)}`
82
82
  : '';
83
+ // 一致性护栏:本 run ≤ 累计 是硬不变量(累计 = 所有 run 之和,必 ≥ 本 run)。
84
+ // 违反说明来源 scope 混用(如 DeepSeek live result usage 是累积/超大 scope,
85
+ // 大于 jsonl 去重聚合的 session 累计)。只标记不修正——数字保持原样便于排查,
86
+ // 由数据源修复。两侧都有值时校验(避免旧数据/缺失字段误报)。
87
+ // 两个分支(real input/output / 10% 估计)共用,保证"本 run > 累计"在任意
88
+ // 路径都显式暴露。
89
+ const withCumGuard = (totalTokens) => cumTotalSuffix +
90
+ (usage?.cumulativeTotalTokens !== undefined && totalTokens > usage.cumulativeTotalTokens
91
+ ? ' · ⚠️ 累计异常'
92
+ : '');
83
93
  // 计算累计 cache 命中率
84
94
  const cumInput = usage?.cumulativeInputTokens;
85
95
  const cumCacheRead = usage?.cumulativeCacheReadTokens;
@@ -110,7 +120,7 @@ export function formatUsageStats(usage, options) {
110
120
  lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
111
121
  }
112
122
  lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
113
- lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
123
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
114
124
  }
115
125
  else {
116
126
  // Estimate path: when real input/output are absent,
@@ -127,7 +137,7 @@ export function formatUsageStats(usage, options) {
127
137
  lines.push(`Cache create - ${formatTokenK(cacheCreation)}${cumCacheCreateSuffix}`);
128
138
  }
129
139
  lines.push(`Cached token - ${formatTokenK(cacheRead)} (${cachePercent}%)${cumCacheReadSuffix}`);
130
- lines.push(`Total token - ${formatTokenK(totalTokens)}${cumTotalSuffix}`);
140
+ lines.push(`Total token - ${formatTokenK(totalTokens)}${withCumGuard(totalTokens)}`);
131
141
  }
132
142
  }
133
143
  return lines.join('\n');
@@ -23,6 +23,9 @@ export const AGENT_CHOICE_FIELDS = {
23
23
  { configKey: 'model', choicesKey: 'model' },
24
24
  { configKey: 'thinkingEffort', choicesKey: 'thinkingEffort' },
25
25
  ],
26
+ // DSH has no model/provider choice fields (host endpoint is connection
27
+ // config, not a per-run choice) — empty to satisfy the Record type.
28
+ dsh: [],
26
29
  };
27
30
  /** 返回指定 agent 的字段映射;claude 或未知 agent 返回 undefined。 */
28
31
  export function choiceFieldsFor(agent) {
@@ -32,6 +32,7 @@ import { getLogger } from '../../logger/index.js';
32
32
  import { SpawningRunner } from '../common/spawning-runner.js';
33
33
  import { pipeAllStdio } from '../common/runner-utils.js';
34
34
  import { authErrorEvent, syntheticInitEvent } from '../common/runner-utils.js';
35
+ import { makeQuestionApprovalEvent } from '../question-common.js';
35
36
  /** 拒绝权限时的默认提示(进入 claude 上下文,中文用户可读)。 */
36
37
  const DENY_MESSAGE = '用户拒绝了此工具调用,请停止并等待用户指令。';
37
38
  /** 会话级空闲回收默认 TTL(对齐 codex appServer.idleTtlMs 默认 30 分钟)。 */
@@ -639,17 +640,30 @@ export class ClaudeSession extends SpawningRunner {
639
640
  this.pendingToolInputs.delete(requestId);
640
641
  return [];
641
642
  }
643
+ return [makeQuestionApprovalEvent(requestId, questions, this.sessionId)];
644
+ }
645
+ // ExitPlanMode:语义是「退出计划模式」的通知型工具,input 天然为空(plan
646
+ // 内容在 plan 文件 + 前置 thinking/text 里,不走 tool input)。它没有
647
+ // command 语义,塞进 command 槽位会显示无意义 `{}` → 用独立 tool kind,
648
+ // 标题/工具名 + reason 表达审批内容。
649
+ if (toolName === 'ExitPlanMode') {
650
+ const rawInput = JSON.stringify(input);
642
651
  const view = {
643
652
  requestId,
644
- kind: 'question',
645
- questions,
646
- availableDecisions: [],
653
+ kind: 'tool',
654
+ toolName,
655
+ reason: typeof request.description === 'string' ? request.description : undefined,
656
+ // 非空 input 携带给渲染层(当前 ExitPlanMode 为 `{}`,渲染层不展示)。
657
+ ...(rawInput && rawInput !== '{}' ? { toolInput: rawInput.slice(0, 500) } : {}),
658
+ // 计划审批没有「后续自动放行」语义,acceptAll 无意义;只留 accept/decline。
659
+ availableDecisions: ['accept', 'decline'],
647
660
  };
661
+ getLogger().info(`[${this.logTag}] permission request requestId=${requestId} tool=${toolName}`);
648
662
  return [
649
663
  {
650
664
  type: 'approval_requested',
651
665
  requestId,
652
- kind: 'question',
666
+ kind: 'tool',
653
667
  threadId: this.sessionId,
654
668
  turnId: '',
655
669
  itemId: '',
@@ -658,7 +672,12 @@ export class ClaudeSession extends SpawningRunner {
658
672
  },
659
673
  ];
660
674
  }
661
- const command = typeof input.command === 'string' ? input.command : JSON.stringify(input ?? {});
675
+ // 非命令工具的兜底防御:input 无 command 字段时 JSON 序列化,但空对象
676
+ // 序列化成 `{}` 对用户无意义——内容为空时回退展示工具名(review P3-5)。
677
+ const rawCommand = typeof input.command === 'string' ? input.command : JSON.stringify(input);
678
+ const command = rawCommand && rawCommand !== '{}' && rawCommand !== 'null'
679
+ ? rawCommand
680
+ : toolName || '(unknown tool)';
662
681
  const view = {
663
682
  requestId,
664
683
  kind: 'command',
@@ -83,6 +83,8 @@ export interface ThreadResumeParams {
83
83
  modelProvider?: string | null;
84
84
  approvalPolicy?: AskForApproval;
85
85
  sandbox?: SandboxMode;
86
+ /** Configuration overrides for the resumed thread (codex v2 wire field). */
87
+ config?: Record<string, unknown> | null;
86
88
  }
87
89
  export interface ThreadResumeResponse {
88
90
  thread: Thread;
@@ -432,6 +434,7 @@ export declare const ServerRequestMethod: {
432
434
  readonly COMMAND_EXECUTION_APPROVAL: "item/commandExecution/requestApproval";
433
435
  readonly FILE_CHANGE_APPROVAL: "item/fileChange/requestApproval";
434
436
  readonly PERMISSIONS_APPROVAL: "item/permissions/requestApproval";
437
+ readonly REQUEST_USER_INPUT: "item/tool/requestUserInput";
435
438
  };
436
439
  export interface NetworkApprovalContext {
437
440
  host: string;
@@ -515,5 +518,37 @@ export interface PermissionsRequestApprovalResponse {
515
518
  permissions: GrantedPermissionProfile;
516
519
  scope?: 'turn' | 'session';
517
520
  }
521
+ /**
522
+ * Codex AskUserQuestion(request_user_input)协议形状。
523
+ * 来源:codex-rs/codex-rs/protocol/src/request_user_input.rs +
524
+ * app-server-protocol v2/item.rs ToolRequestUserInput*(EXPERIMENTAL)。
525
+ * isOther=true → 渲染层提供自定义答案输入;isSecret 仅影响展示(按普通输入
526
+ * 处理);options 为 null/空 → 自由文本题。
527
+ */
528
+ export interface ToolRequestUserInputOption {
529
+ label: string;
530
+ description: string;
531
+ }
532
+ export interface ToolRequestUserInputQuestion {
533
+ id: string;
534
+ header: string;
535
+ question: string;
536
+ isOther: boolean;
537
+ isSecret: boolean;
538
+ options: ToolRequestUserInputOption[] | null;
539
+ }
540
+ export interface ToolRequestUserInputParams {
541
+ threadId: string;
542
+ turnId: string;
543
+ itemId: string;
544
+ questions: ToolRequestUserInputQuestion[];
545
+ autoResolutionMs?: number | null;
546
+ }
547
+ export interface ToolRequestUserInputAnswer {
548
+ answers: string[];
549
+ }
550
+ export interface ToolRequestUserInputResponse {
551
+ answers: Record<string, ToolRequestUserInputAnswer>;
552
+ }
518
553
  /** Unsupported / ignored server request methods. */
519
554
  export declare const UNSUPPORTED_SERVER_REQUEST_METHODS: Set<string>;