lark-remote 0.5.3 → 0.5.5

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 (66) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridge/approval-coordinator.d.ts +18 -1
  5. package/dist/bridge/approval-coordinator.js +57 -10
  6. package/dist/bridge/index.js +90 -17
  7. package/dist/card/approval-render.js +18 -0
  8. package/dist/card/run-state.js +10 -5
  9. package/dist/config/index.d.ts +16 -0
  10. package/dist/config/index.js +46 -6
  11. package/dist/config/opencode-config.d.ts +3 -3
  12. package/dist/config/opencode-config.js +38 -22
  13. package/dist/inbound/authorize.d.ts +34 -0
  14. package/dist/inbound/authorize.js +31 -0
  15. package/dist/index.js +116 -63
  16. package/dist/instance-lock.d.ts +9 -0
  17. package/dist/instance-lock.js +113 -42
  18. package/dist/router/config/dsh.d.ts +51 -21
  19. package/dist/router/config/dsh.js +255 -66
  20. package/dist/router/config/opencode.js +16 -6
  21. package/dist/router/config/types.d.ts +6 -3
  22. package/dist/router/index.d.ts +1 -0
  23. package/dist/router/index.js +13 -5
  24. package/dist/runner/codex/app-server/translator.js +21 -7
  25. package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
  26. package/dist/runner/common/acp/base-acp-runner.js +83 -8
  27. package/dist/runner/common/acp/protocol-types.d.ts +10 -2
  28. package/dist/runner/common/acp/protocol-types.js +3 -1
  29. package/dist/runner/common/connection-based-runner.d.ts +15 -0
  30. package/dist/runner/common/connection-based-runner.js +40 -5
  31. package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
  32. package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
  33. package/dist/runner/dsh/approval.d.ts +53 -0
  34. package/dist/runner/dsh/approval.js +161 -0
  35. package/dist/runner/dsh/auth.d.ts +36 -0
  36. package/dist/runner/dsh/auth.js +118 -0
  37. package/dist/runner/dsh/client.d.ts +161 -42
  38. package/dist/runner/dsh/client.js +551 -244
  39. package/dist/runner/dsh/daemon.d.ts +85 -0
  40. package/dist/runner/dsh/daemon.js +272 -0
  41. package/dist/runner/dsh/merge.d.ts +9 -0
  42. package/dist/runner/dsh/merge.js +51 -0
  43. package/dist/runner/dsh/runner.d.ts +94 -11
  44. package/dist/runner/dsh/runner.js +553 -67
  45. package/dist/runner/dsh/translator.d.ts +43 -3
  46. package/dist/runner/dsh/translator.js +115 -38
  47. package/dist/runner/dsh/types.d.ts +189 -27
  48. package/dist/runner/dsh/types.js +11 -5
  49. package/dist/runner/kimi/acp/runner.js +15 -3
  50. package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
  51. package/dist/runner/opencode/acp/error-monitor.js +2 -2
  52. package/dist/runner/opencode/acp/runner.d.ts +9 -0
  53. package/dist/runner/opencode/acp/runner.js +40 -1
  54. package/dist/runner/opencode/acp/translator.d.ts +4 -2
  55. package/dist/runner/opencode/acp/translator.js +12 -3
  56. package/dist/runner/pi/rpc/translator.d.ts +2 -0
  57. package/dist/runner/pi/rpc/translator.js +27 -1
  58. package/dist/runner/types.d.ts +34 -0
  59. package/dist/runner/types.js +24 -0
  60. package/dist/session/codex/rollout-reader.js +44 -7
  61. package/dist/session/dsh/sessions.d.ts +45 -6
  62. package/dist/session/dsh/sessions.js +171 -27
  63. package/dist/session/opencode/sessions.d.ts +18 -7
  64. package/dist/session/opencode/sessions.js +294 -15
  65. package/dist/session/pi/sessions.js +6 -2
  66. package/package.json +3 -1
@@ -1,82 +1,176 @@
1
1
  /**
2
2
  * DshConfigBuilder — /config card fields for the DSH agent.
3
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.
4
+ * DSH (DeepSeek Harness) is an HTTP-only agent. The card exposes the Web Host
5
+ * base URL plus preset/provider/model/reasoning-effort knobs. The launch token
6
+ * is a YAML-only fallback and is never rendered or saved through the card.
6
7
  *
7
- * 清单来源分两期:
8
- * - 动态清单(当前实现):打开 /config 前通过 `llm.models` / `agentPreset.list`
9
- * 预取(Router 调 `prefetch()`),失败/离线回退下方固定兜底清单,卡不依赖
8
+ * 清单来源:
9
+ * - 动态清单:打开 /config 前通过 `session/modelCatalog` / `agentPresets/list`
10
+ * 预取(Router 调 `prefetch()`),失败/离线回退固定兜底清单,卡不依赖
10
11
  * DSH 服务在线。
11
- * - 三个可选字段用空字符串哨兵表示「跟随服务端默认」:select 的 value `''`
12
- * 在 handleFieldChange 中被转成 undefined patch(router 删除键),config.yaml
13
- * 不写该键 = 服务端默认。
12
+ * - provider / model / reasoningEffort 用空字符串哨兵表示「跟随默认」:select
13
+ * 的 value `''` 在 handleFieldChange 中被转成 undefined patch(router 删除
14
+ * 键),config.yaml 不写该键 = 服务端默认。
15
+ * - provider 没有哨兵:旧配置没有 provider 时按 `deepseek-official` 生效,
16
+ * 与 runner 的兼容规则一致。provider 清单不硬编码,来自服务端目录。
14
17
  */
15
18
  import { DEFAULT_DSH_HOST } from '../../config/index.js';
16
19
  import { DshClient } from '../../runner/dsh/client.js';
17
20
  import { getLogger } from '../../logger/index.js';
18
- /** 固定兜底 preset 清单(DSH rc.7 实测;动态拉取失败时使用)。 */
21
+ /** 旧配置没有 provider 时的生效值(与 DshRunner 的兼容规则一致)。 */
22
+ const LEGACY_PROVIDER = 'deepseek-official';
23
+ /** 固定兜底 preset 清单(DSH 0.2.x 实测;动态拉取失败时使用)。 */
19
24
  const FALLBACK_PRESET_OPTIONS = [
20
25
  { text: 'standard(标准模式)', value: 'standard' },
21
26
  { text: 'minimal(极简模式)', value: 'minimal' },
22
- { text: 'code(PTC 模式)', value: 'code' },
27
+ { text: 'ptc(PTC 模式)', value: 'ptc' },
23
28
  { text: 'cordis(创造模式)', value: 'cordis' },
24
29
  ];
25
- /** 固定兜底模型清单(DSH rc.7 实测;动态拉取失败时使用)。 */
26
- const FALLBACK_MODEL_OPTIONS = ['deepseek-v4-flash', 'deepseek-v4-pro'];
27
- /** 固定兜底推理档位(llm.models 实际档位;动态拉取失败时使用)。 */
30
+ /** 固定兜底模型清单(DSH 0.2.x 实测;动态拉取失败时使用)。 */
31
+ const FALLBACK_MODEL_OPTIONS = ['deepseek-flash', 'deepseek-v4-pro'];
32
+ /** 固定兜底推理档位(session/modelCatalog 实际档位;动态拉取失败时使用)。 */
28
33
  const FALLBACK_EFFORT_OPTIONS = ['off', 'low', 'high', 'max'];
29
- /** 「跟随服务端默认」哨兵值:select value 用空串,保存时转 undefined 删除键。 */
34
+ /** 固定兜底权限预设(DSH 0.2.x 实测;动态拉取失败时使用)。 */
35
+ const FALLBACK_PERMISSION_OPTIONS = [
36
+ { text: 'read-only(只读)', value: 'read-only' },
37
+ { text: 'workspace-write(工作区可写)', value: 'workspace-write' },
38
+ { text: 'danger-full-access(完全访问,关闭审批)', value: 'danger-full-access' },
39
+ ];
40
+ /** 「跟随默认」哨兵值:select value 用空串,保存时转 undefined 删除键。 */
30
41
  const DEFAULT_SENTINEL = '';
31
42
  export class DshConfigBuilder {
32
- /** 最近一次预取用的 host;host 变更时缓存失效(下一张卡重新拉取)。 */
43
+ /** 最近一次预取用的 host;host 变更时缓存失效。 */
33
44
  prefetchHost;
34
45
  modelCatalog;
35
46
  presets;
47
+ permissionCatalog;
36
48
  /** 可注入 client 工厂(测试用);默认生产实现。 */
37
49
  clientFactory;
38
50
  constructor(clientFactory) {
39
- this.clientFactory = clientFactory ?? ((base) => new DshClient(base));
51
+ this.clientFactory =
52
+ clientFactory ??
53
+ ((base, token, credentialsFile) => new DshClient(base, {
54
+ token,
55
+ credentialsFile,
56
+ // 卡片预取是交互路径:DSH 卡死时最多等 5 秒后回退固定清单。
57
+ requestTimeoutMs: 5_000,
58
+ }));
40
59
  }
41
- /** 异步预取模型/预设目录(§4.2 二期动态清单)。失败/离线静默回退固定兜底清单,
42
- * 卡不依赖 DSH 服务在线;同一 host 只拉一次,避免每次刷卡重复请求。 */
43
- async prefetch(host) {
60
+ /**
61
+ * 异步预取模型/预设目录。失败/离线静默回退固定兜底清单,卡不依赖 DSH
62
+ * 服务在线。`force` 用于打开 /config 时强制刷新(在 DSH Web UI 新增
63
+ * provider 后不需要重启 lark-remote)。三个请求并行,最坏等待一个超时。
64
+ */
65
+ async prefetch(host, token, credentialsFile, options = {}) {
44
66
  const base = (host ?? DEFAULT_DSH_HOST).replace(/\/$/, '');
45
- if (!base || base === this.prefetchHost)
67
+ const key = `${base}\u0000${token ?? ''}\u0000${credentialsFile ?? ''}`;
68
+ if (!base || (!options.force && key === this.prefetchHost))
46
69
  return;
47
- const client = this.clientFactory(base);
48
- try {
49
- this.modelCatalog = await client.listModels();
70
+ const client = this.clientFactory(base, token, credentialsFile);
71
+ const [models, presets, permissions] = await Promise.allSettled([
72
+ client.listModels(),
73
+ client.listPresets(),
74
+ client.listPermissionPresets(),
75
+ ]);
76
+ if (models.status === 'fulfilled') {
77
+ this.modelCatalog = models.value;
50
78
  }
51
- catch (err) {
52
- getLogger().warn(`[dsh-config] llm.models prefetch failed (falling back to fixed catalog): ${err.message}`);
79
+ else {
80
+ getLogger().warn(`[dsh-config] session/modelCatalog prefetch failed (falling back to fixed catalog): ${models.reason.message}`);
53
81
  this.modelCatalog = undefined;
54
82
  }
55
- try {
56
- this.presets = await client.listPresets();
83
+ if (presets.status === 'fulfilled') {
84
+ this.presets = presets.value;
57
85
  }
58
- catch (err) {
59
- getLogger().warn(`[dsh-config] agentPreset.list prefetch failed (falling back to fixed presets): ${err.message}`);
86
+ else {
87
+ getLogger().warn(`[dsh-config] agentPresets/list prefetch failed (falling back to fixed presets): ${presets.reason.message}`);
60
88
  this.presets = undefined;
61
89
  }
62
- this.prefetchHost = base;
90
+ if (permissions.status === 'fulfilled') {
91
+ this.permissionCatalog = permissions.value;
92
+ }
93
+ else {
94
+ getLogger().warn(`[dsh-config] permissionPresets/catalog prefetch failed (falling back to fixed presets): ${permissions.reason.message}`);
95
+ this.permissionCatalog = undefined;
96
+ }
97
+ this.prefetchHost = key;
63
98
  }
64
99
  /** 同步注入 catalog/presets(测试用,替代异步 prefetch;标记 prefetchHost 使
65
100
  * 后续 prefetch 短路,避免测试误触发网络请求)。 */
66
- setCatalog(catalog, presets) {
101
+ setCatalog(catalog, presets, permissionCatalog) {
67
102
  this.modelCatalog = catalog;
68
103
  if (presets)
69
104
  this.presets = presets;
105
+ if (permissionCatalog)
106
+ this.permissionCatalog = permissionCatalog;
70
107
  this.prefetchHost = 'injected';
71
108
  }
72
- /** 展平 llm.models 各 provider group 的模型 id;无 catalog 时回退固定清单。 */
73
- modelOptions() {
109
+ /** Provider 下拉:目录 groups + 当前值兜底;离线时只保留当前值。 */
110
+ providerOptions(currentProvider, configured) {
111
+ const options = [];
74
112
  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;
113
+ for (const group of this.modelCatalog.groups) {
114
+ options.push({
115
+ text: group.name && group.name !== group.id ? `${group.name}(${group.id})` : group.id,
116
+ value: group.id,
117
+ });
118
+ }
119
+ }
120
+ if (!options.some((option) => option.value === currentProvider)) {
121
+ const source = configured ? '当前配置' : '默认';
122
+ options.push({
123
+ text: this.modelCatalog
124
+ ? `${currentProvider}(${source},服务端未列出)`
125
+ : `${currentProvider}(${source},离线未校验)`,
126
+ value: currentProvider,
127
+ });
78
128
  }
79
- return FALLBACK_MODEL_OPTIONS;
129
+ return options;
130
+ }
131
+ /**
132
+ * 当前 provider 的模型清单。目录在线但该 provider 不在目录中时返回空列表
133
+ * (不把 DeepSeek 模型冒充成其他 provider 的模型);目录离线时用固定兜底。
134
+ */
135
+ modelOptionsFor(provider) {
136
+ if (!this.modelCatalog)
137
+ return FALLBACK_MODEL_OPTIONS;
138
+ const group = this.modelCatalog.groups.find((entry) => entry.id === provider);
139
+ if (!group)
140
+ return [];
141
+ return group.models.map((model) => ({
142
+ text: model.name && model.name !== model.id ? `${model.name}(${model.id})` : model.id,
143
+ value: model.id,
144
+ }));
145
+ }
146
+ modelValues(provider) {
147
+ return this.modelOptionsFor(provider).map((option) => typeof option === 'string' ? option : option.value);
148
+ }
149
+ /** Provider 的默认模型:目录默认值属于该 provider 时优先,否则首个模型。 */
150
+ defaultModelFor(provider) {
151
+ const group = this.modelCatalog?.groups.find((entry) => entry.id === provider);
152
+ if (!group || group.models.length === 0)
153
+ return undefined;
154
+ const defaultModel = this.modelCatalog?.default?.provider === provider
155
+ ? this.modelCatalog.default.model
156
+ : undefined;
157
+ return defaultModel && group.models.some((model) => model.id === defaultModel)
158
+ ? defaultModel
159
+ : group.models[0].id;
160
+ }
161
+ /**
162
+ * 模型可用的推理档位。目录在线时严格按模型声明:模型没有 reasoning 元数据
163
+ * 就返回空(只留「跟随模型默认」),避免把 A 模型的档位塞给 B 模型。
164
+ */
165
+ effortOptionsFor(provider, model) {
166
+ if (!this.modelCatalog)
167
+ return FALLBACK_EFFORT_OPTIONS;
168
+ const group = this.modelCatalog.groups.find((entry) => entry.id === provider);
169
+ if (!group)
170
+ return [];
171
+ const target = model ?? this.defaultModelFor(provider);
172
+ const entry = target ? group.models.find((candidate) => candidate.id === target) : undefined;
173
+ return entry?.reasoning?.efforts.map((effort) => effort.id) ?? [];
80
174
  }
81
175
  /** preset 清单;无 presets 时回退固定清单。显示名优先用 preset.yml 的 name。 */
82
176
  presetOptions() {
@@ -87,28 +181,60 @@ export class DshConfigBuilder {
87
181
  }
88
182
  return FALLBACK_PRESET_OPTIONS;
89
183
  }
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
- }
184
+ /** 权限预设清单;配置默认只允许 defaultOptions(auto 只适用于当前会话)。 */
185
+ permissionOptions() {
186
+ const entries = this.permissionCatalog?.defaultOptions;
187
+ if (entries && entries.length > 0) {
188
+ return entries.map((p) => ({
189
+ text: p.value === 'read-only'
190
+ ? 'read-only(只读)'
191
+ : p.value === 'workspace-write'
192
+ ? 'workspace-write(工作区可写)'
193
+ : p.value === 'danger-full-access'
194
+ ? 'danger-full-access(完全访问,关闭审批)'
195
+ : p.name && p.name !== p.value
196
+ ? `${p.value}(${p.name})`
197
+ : p.value,
198
+ value: p.value,
199
+ }));
99
200
  }
100
- return FALLBACK_EFFORT_OPTIONS;
201
+ return FALLBACK_PERMISSION_OPTIONS;
202
+ }
203
+ /** “跟随服务端默认”文案带上实际默认值,避免用户不知道默认是什么。 */
204
+ permissionDefaultText() {
205
+ const current = this.permissionCatalog?.defaultPreset;
206
+ return current ? `跟随服务端默认(当前 ${current})` : '跟随服务端默认';
101
207
  }
102
208
  buildFields(displayConfig) {
103
209
  const dsh = displayConfig.agents?.dsh;
104
210
  const currentHost = dsh?.host ?? DEFAULT_DSH_HOST;
105
211
  const currentPreset = dsh?.agentPreset ?? DEFAULT_SENTINEL;
212
+ const currentPermission = dsh?.permissionPreset ?? DEFAULT_SENTINEL;
213
+ const configuredProvider = dsh?.provider;
214
+ const currentProvider = configuredProvider ?? LEGACY_PROVIDER;
106
215
  const currentModel = dsh?.model ?? DEFAULT_SENTINEL;
107
216
  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 [
217
+ const providerOptions = this.providerOptions(currentProvider, configuredProvider !== undefined);
218
+ const modelOptions = this.modelOptionsFor(currentProvider);
219
+ const isCustomModel = currentModel !== DEFAULT_SENTINEL &&
220
+ !this.modelValues(currentProvider).includes(currentModel);
221
+ const permissionOptions = [...this.permissionOptions()];
222
+ if (currentPermission !== DEFAULT_SENTINEL &&
223
+ !permissionOptions.some((option) => option.value === currentPermission)) {
224
+ permissionOptions.push({
225
+ text: `${currentPermission}(当前配置,服务端未列出)`,
226
+ value: currentPermission,
227
+ });
228
+ }
229
+ const effortOptions = this.effortOptionsFor(currentProvider, currentModel || undefined).map((value) => ({ text: value, value }));
230
+ if (currentEffort !== DEFAULT_SENTINEL &&
231
+ !effortOptions.some((option) => option.value === currentEffort)) {
232
+ effortOptions.push({
233
+ text: `${currentEffort}(当前配置,模型未列出)`,
234
+ value: currentEffort,
235
+ });
236
+ }
237
+ const fields = [
112
238
  {
113
239
  key: 'agents.dsh.host',
114
240
  label: 'DSH Web Host',
@@ -117,17 +243,35 @@ export class DshConfigBuilder {
117
243
  },
118
244
  {
119
245
  key: 'agents.dsh.agentPreset',
120
- label: '预设模式',
246
+ label: '预设模式(工具集)',
121
247
  type: 'select',
122
248
  options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...this.presetOptions()],
123
249
  currentValue: currentPreset,
124
250
  },
251
+ {
252
+ key: 'agents.dsh.permissionPreset',
253
+ label: '权限预设(沙箱 + 审批)',
254
+ type: 'select',
255
+ options: [
256
+ { text: this.permissionDefaultText(), value: DEFAULT_SENTINEL },
257
+ ...permissionOptions,
258
+ ],
259
+ currentValue: currentPermission,
260
+ },
261
+ {
262
+ key: 'agents.dsh.provider',
263
+ label: 'Provider(模型服务商)',
264
+ type: 'select',
265
+ options: providerOptions,
266
+ currentValue: currentProvider,
267
+ },
125
268
  {
126
269
  key: 'agents.dsh.model',
127
270
  label: '使用模型',
128
271
  type: 'select',
129
- options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...modelOptions],
130
- currentValue: isCustomModel ? undefined : currentModel,
272
+ options: [{ text: '跟随该 Provider 默认', value: DEFAULT_SENTINEL }, ...modelOptions],
273
+ // 自定义模型不在下拉里:让下拉停在哨兵,模型名由下方输入框承载。
274
+ currentValue: isCustomModel ? DEFAULT_SENTINEL : currentModel,
131
275
  },
132
276
  // 自定义模型名输入框(与 codex/claude 一致的行为)
133
277
  {
@@ -140,40 +284,85 @@ export class DshConfigBuilder {
140
284
  key: 'agents.dsh.reasoningEffort',
141
285
  label: '推理强度',
142
286
  type: 'select',
143
- options: [{ text: '跟随服务端默认', value: DEFAULT_SENTINEL }, ...effortOptions],
287
+ options: [{ text: '跟随模型默认', value: DEFAULT_SENTINEL }, ...effortOptions],
144
288
  currentValue: currentEffort,
145
289
  },
146
290
  ];
291
+ const failures = this.modelCatalog?.failures ?? [];
292
+ if (failures.length > 0) {
293
+ fields.push({
294
+ key: 'agents.dsh.providerFailures',
295
+ label: `⚠️ Provider 目录异常:${failures
296
+ .map((failure) => `${failure.id}(${failure.message})`)
297
+ .join(';')}`,
298
+ type: 'note',
299
+ });
300
+ }
301
+ return fields;
147
302
  }
148
303
  handleFieldChange(key, value, config) {
304
+ // provider 没有哨兵:切换时联动 model/effort,避免 provider 与 model 来自
305
+ // 不同目录(selectModel 会以 session/model-unavailable 失败)。
306
+ if (key === 'agents.dsh.provider' && typeof value === 'string') {
307
+ return this.providerChangePatches(value, config);
308
+ }
149
309
  const patches = [];
150
- // 空串哨兵「跟随服务端默认」→ undefined patch(router 删除键)
310
+ // 空串哨兵「跟随默认」→ undefined patch(router 删除键)
151
311
  const effective = value === DEFAULT_SENTINEL ? undefined : value;
152
312
  patches.push({ key, value: effective });
153
- // 模型变更时,若当前档位不被新模型支持,重置(参照 kimi/codex 语义)
313
+ // 模型变更时,把 provider 固定到当前 provider(旧配置只有 model 时,
314
+ // 第一次选模型即完成迁移),并按新模型重置不被支持的档位。
154
315
  if (key === 'agents.dsh.model' && typeof value === 'string') {
155
- const effortPatch = this.effortPatchForModel(config, effective);
316
+ const newModel = effective;
317
+ const provider = config.agents?.dsh?.provider ?? LEGACY_PROVIDER;
318
+ if (newModel) {
319
+ patches.push({ key: 'agents.dsh.provider', value: provider });
320
+ }
321
+ const effortPatch = this.effortPatchForModel(config, provider, newModel);
156
322
  if (effortPatch)
157
323
  patches.push(effortPatch);
158
324
  }
159
325
  return patches;
160
326
  }
161
- /** 切到 newModel 后的档位补丁:当前档位仍被新模型支持 → 不产出;否则用新模型
162
- * 目录中位;无目录/空列表 → undefined(清空 = 跟随服务端默认)。 */
163
- effortPatchForModel(config, newModel) {
327
+ /** 切 provider 后的联动:provider → 新 provider 的默认/首个模型 → 合法档位。 */
328
+ providerChangePatches(provider, config) {
329
+ const patches = [
330
+ { key: 'agents.dsh.provider', value: provider },
331
+ ];
332
+ if (!this.modelCatalog)
333
+ return patches;
334
+ const group = this.modelCatalog.groups.find((entry) => entry.id === provider);
335
+ if (!group)
336
+ return patches;
337
+ const currentModel = config.agents?.dsh?.model;
338
+ const keepModel = currentModel !== undefined && group.models.some((model) => model.id === currentModel);
339
+ const nextModel = keepModel ? currentModel : this.defaultModelFor(provider);
340
+ if (nextModel !== currentModel) {
341
+ patches.push({ key: 'agents.dsh.model', value: nextModel });
342
+ }
343
+ const effortPatch = this.effortPatchForModel(config, provider, nextModel);
344
+ if (effortPatch)
345
+ patches.push(effortPatch);
346
+ return patches;
347
+ }
348
+ /**
349
+ * 切到 (provider, newModel) 后的档位补丁:当前档位仍被新模型支持 → 不产出;
350
+ * 否则用新模型目录中位;目录离线 → 不产出(保留现状,由服务端校验);
351
+ * 模型无 reasoning 元数据 → 清空档位(跟随模型默认)。
352
+ */
353
+ effortPatchForModel(config, provider, newModel) {
164
354
  const currentEffort = config.agents?.dsh?.reasoningEffort;
165
355
  if (!newModel) {
166
- // 模型被清空(跟随默认)→ 档位一并清空跟随默认
167
356
  return currentEffort ? { key: 'agents.dsh.reasoningEffort', value: undefined } : null;
168
357
  }
169
- const supported = this.effortOptionsForModel(newModel);
358
+ if (!this.modelCatalog)
359
+ return null;
360
+ const supported = this.effortOptionsFor(provider, newModel);
170
361
  if (currentEffort && supported.includes(currentEffort))
171
362
  return null;
172
363
  // 新模型目录中位档位;偶数长度取偏高位(off/low → low,符合 DSH 默认 high 语义)
173
364
  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 };
365
+ return { key: 'agents.dsh.reasoningEffort', value: middle };
177
366
  }
178
367
  }
179
368
  //# sourceMappingURL=dsh.js.map
@@ -5,18 +5,27 @@ import { loadOpencodeConfig } from '../../config/opencode-config.js';
5
5
  import { DEFAULT_TURN_IDLE_TIMEOUT_MINUTES } from '../../config/index.js';
6
6
  import { resetModelPatch } from './common/model-patch.js';
7
7
  const MODE_OPTIONS = [
8
- { text: 'build(默认,逐项审批)', value: 'build' },
8
+ { text: 'build(默认,按 opencode 权限配置)', value: 'build' },
9
9
  { text: 'plan(规划模式)', value: 'plan' },
10
10
  ];
11
+ /** Keep the currently configured value selectable even when the CLI failed and
12
+ * we are showing the static fallback catalog. */
13
+ function withCurrent(options, current) {
14
+ if (current === undefined || options.includes(current))
15
+ return options;
16
+ return [current, ...options];
17
+ }
11
18
  export class OpencodeConfigBuilder {
12
19
  buildFields(displayConfig) {
13
20
  const fields = [];
14
21
  // 从 opencode models 命令动态读取 provider 和 model
15
- const opencodeCfg = loadOpencodeConfig();
16
- const providerNames = opencodeCfg.providerNames;
17
- // 根据 displayConfig 中已选的 provider 过滤 model 选项
22
+ const binary = displayConfig.agents?.opencode?.acp?.binary;
23
+ const opencodeCfg = loadOpencodeConfig(binary);
18
24
  const currentProvider = displayConfig.agents?.opencode?.providerID;
19
- const modelOptions = opencodeCfg.modelOptions(currentProvider);
25
+ const currentModel = displayConfig.agents?.opencode?.modelID;
26
+ const providerNames = withCurrent(opencodeCfg.providerNames, currentProvider);
27
+ // 根据 displayConfig 中已选的 provider 过滤 model 选项
28
+ const modelOptions = withCurrent(opencodeCfg.modelOptions(currentProvider), currentModel);
20
29
  fields.push({
21
30
  key: 'agents.opencode.providerID',
22
31
  label: 'Provider',
@@ -56,7 +65,8 @@ export class OpencodeConfigBuilder {
56
65
  patches.push({ key, value });
57
66
  // provider 变更时,重置 model 为新 provider 的首个模型
58
67
  if (key === 'agents.opencode.providerID' && typeof value === 'string') {
59
- const opencodeCfg = loadOpencodeConfig();
68
+ const binary = config.agents?.opencode?.acp?.binary;
69
+ const opencodeCfg = loadOpencodeConfig(binary);
60
70
  const currentModel = config.agents?.opencode?.modelID;
61
71
  const modelPatch = resetModelPatch('agents.opencode.modelID', currentModel, opencodeCfg.modelOptions(value));
62
72
  if (modelPatch)
@@ -51,8 +51,11 @@ export interface AgentConfigCardBuilder {
51
51
  }>;
52
52
  /**
53
53
  * 可选的异步预取钩子:卡片渲染前拉取动态选项(如 DSH 的模型/预设目录)。
54
- * host 为该 agent 的当前连接地址;实现内部应缓存(同一 host 只拉一次),
55
- * 失败静默回退固定兜底,不阻断卡片渲染。非异步预取的 agent 可不实现。
54
+ * host 为该 agent 的当前连接地址;实现内部默认按 host 缓存,`force` 用于
55
+ * 打开 /config 时强制刷新(外部新增 provider 后无需重启)。失败静默回退
56
+ * 固定兜底,不阻断卡片渲染。非异步预取的 agent 可不实现。
56
57
  */
57
- prefetch?(host?: string): Promise<void>;
58
+ prefetch?(host?: string, token?: string, credentialsFile?: string, options?: {
59
+ force?: boolean;
60
+ }): Promise<void>;
58
61
  }
@@ -664,6 +664,7 @@ export declare class CommandRouter {
664
664
  * DSH /config 卡片构建前异步预取模型/预设目录(§4.2 动态清单)。
665
665
  * 只在 defaultAgent 为 dsh 时生效;其他 agent 无 prefetch 钩子直接跳过。
666
666
  * 预取失败不阻断——DshConfigBuilder 内部已回退固定兜底清单。
667
+ * `force` 在打开 /config 时使用:DSH Web UI 刚新增的 provider 立即出现。
667
668
  */
668
669
  private prefetchConfigCardCatalogs;
669
670
  /** Build an interactive config card with CardKit 2.0 tabs + batch save */
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import YAML from 'yaml';
5
5
  import { getConfigDir, getConfigValue, setConfigValue, setConfigValues, mapAgentKey, getAgentConfig, } from '../config/index.js';
6
6
  import { syncAgentChoices } from '../runner/index.js';
7
+ import { resolveDshCredentialsFile } from '../runner/dsh/auth.js';
7
8
  import { WorkspaceStore } from '../workspace/index.js';
8
9
  import { OrderStore } from '../order/index.js';
9
10
  import { resolveAlias } from '../order/alias-resolve.js';
@@ -2144,7 +2145,8 @@ export class CommandRouter {
2144
2145
  k === 'agents.opencode' ||
2145
2146
  k.startsWith('agents.opencode.') ||
2146
2147
  k === 'opencode' ||
2147
- k.startsWith('opencode.'));
2148
+ k.startsWith('opencode.') ||
2149
+ k === 'agents.dsh.permissionPreset');
2148
2150
  if (hasApprovalConfigChange) {
2149
2151
  this.bridge.syncActiveApprovalModes();
2150
2152
  }
@@ -3848,8 +3850,9 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3848
3850
  async cmdConfig(args, ctx) {
3849
3851
  // Ensure probe cache is populated before building the card.
3850
3852
  await probeAllAgents();
3851
- // 动态清单预取(DSH 模型/预设目录):卡渲染前拉取,失败回退兜底清单
3852
- await this.prefetchConfigCardCatalogs();
3853
+ // 动态清单预取(DSH 模型/预设目录):卡渲染前强制刷新,DSH Web UI 刚新增
3854
+ // 的 provider 立即可见;失败回退兜底清单。
3855
+ await this.prefetchConfigCardCatalogs(true);
3853
3856
  // Check if this is a direct set command: /config <key> <value>
3854
3857
  // Command-style set: immediate write to disk, clear pendingConfig
3855
3858
  if (args.length >= 2) {
@@ -3900,13 +3903,18 @@ ${sessionCwdLine}${agentLines.map((l) => `- ${l}`).join('\n')}
3900
3903
  * DSH /config 卡片构建前异步预取模型/预设目录(§4.2 动态清单)。
3901
3904
  * 只在 defaultAgent 为 dsh 时生效;其他 agent 无 prefetch 钩子直接跳过。
3902
3905
  * 预取失败不阻断——DshConfigBuilder 内部已回退固定兜底清单。
3906
+ * `force` 在打开 /config 时使用:DSH Web UI 刚新增的 provider 立即出现。
3903
3907
  */
3904
- async prefetchConfigCardCatalogs() {
3908
+ async prefetchConfigCardCatalogs(force = false) {
3905
3909
  if (this.config.defaultAgent !== 'dsh')
3906
3910
  return;
3907
3911
  const builder = getConfigBuilder('dsh');
3908
3912
  const dshConf = getAgentConfig(this.config, 'dsh');
3909
- await builder.prefetch?.(dshConf?.host);
3913
+ const credentialsFile = dshConf?.credentialsFile?.trim() ||
3914
+ (dshConf?.dshHome?.trim()
3915
+ ? resolveDshCredentialsFile(process.env, dshConf.dshHome)
3916
+ : undefined);
3917
+ await builder.prefetch?.(dshConf?.host, dshConf?.token, credentialsFile, { force });
3910
3918
  }
3911
3919
  /** Build an interactive config card with CardKit 2.0 tabs + batch save */
3912
3920
  buildConfigCard() {
@@ -222,6 +222,11 @@ export class CodexAppServerTranslator {
222
222
  this.threadId = params.threadId;
223
223
  this.turnId = params.turn.id;
224
224
  this.turnStatus = params.turn.status ?? 'inProgress';
225
+ // 单 turn 用量从零开始累计:turn 内每次采样都会发一条
226
+ // `thread/tokenUsage/updated`,本 run 的用量是这些增量之和(不是最后一条)。
227
+ // resume 后服务端重放的上一 turn 用量若在此之后到达,由 handleTokenUsageUpdated
228
+ // 的 turnId 闸门挡掉。
229
+ this.usage = { inputTokens: 0, outputTokens: 0 };
225
230
  this.textByItem.clear();
226
231
  this.reasoningByItem.clear();
227
232
  this.toolOutputByItem.clear();
@@ -455,22 +460,31 @@ export class CodexAppServerTranslator {
455
460
  ];
456
461
  }
457
462
  handleTokenUsageUpdated(params) {
463
+ // 归属闸门:resume 之后服务端可能重放上一 turn 的 tokenUsage(turnId 不同),
464
+ // 计入会把上一 turn 的消耗算成本 run。本轮 turnId 由 turn/started 确立。
465
+ if (params.turnId && this.turnId && params.turnId !== this.turnId) {
466
+ return [];
467
+ }
458
468
  const last = params.tokenUsage.last;
459
469
  // 协议原生 inputTokens 含 cached(total = input + output),而 lark-remote 的
460
470
  // 卡片契约是 ccusage 口径「input_tokens 表示未命中缓存的输入」(design §9.21)。
461
471
  // 在入口归一,否则 done 卡 Input 会等于 Context、cache% 分母重复计数、
462
472
  // Total 被 max(total, input+output+cache) 抬高一倍(实测 364K → 727K)。
463
473
  // jsonl 链路有同一处减法(session/codex/rollout-reader.ts buildUsageFromScan)。
474
+ // 单 turn 口径:一个 turn 内每次采样各发一条通知(实测 7 次工具往返 = 7 条),
475
+ // 本 run 是这些增量之和——只取最后一条会把多步 run 低报(实测 3,648 output 显示成 679)。
464
476
  const cachedInputTokens = last.cachedInputTokens === undefined
465
477
  ? undefined
466
478
  : Math.min(last.cachedInputTokens, last.inputTokens);
467
- this.usage = {
468
- inputTokens: last.inputTokens - (cachedInputTokens ?? 0),
469
- outputTokens: last.outputTokens,
470
- cachedInputTokens,
471
- totalTokens: last.totalTokens,
472
- contextLimit: params.tokenUsage.modelContextWindow,
473
- };
479
+ this.usage.inputTokens += last.inputTokens - (cachedInputTokens ?? 0);
480
+ this.usage.outputTokens += last.outputTokens;
481
+ if (cachedInputTokens !== undefined) {
482
+ this.usage.cachedInputTokens = (this.usage.cachedInputTokens ?? 0) + cachedInputTokens;
483
+ }
484
+ if (last.totalTokens !== undefined) {
485
+ this.usage.totalTokens = (this.usage.totalTokens ?? 0) + last.totalTokens;
486
+ }
487
+ this.usage.contextLimit = params.tokenUsage.modelContextWindow ?? this.usage.contextLimit;
474
488
  return [];
475
489
  }
476
490
  /**
@@ -53,12 +53,24 @@ export declare abstract class BaseAcpRunner<TTranslator extends BaseAcpTranslato
53
53
  * matches. Failures are non-fatal (warn only).
54
54
  */
55
55
  protected abstract applyTurnSettings(client: JsonRpcClient, sessionId: string): Promise<void>;
56
+ /**
57
+ * session/new|resume retry policy. Default: no retry. Agents whose ACP
58
+ * server sits behind a separately-starting service (opencode v2) override
59
+ * this to absorb a transient `ServiceUnavailableError` without failing the
60
+ * whole turn.
61
+ */
62
+ protected sessionSetupRetryDelaysMs(): number[];
63
+ /** Whether a failed session/new|resume is safe to retry. */
64
+ protected isRetryableSessionSetupError(_err: unknown): boolean;
65
+ /** Final error surfaced to the user after retries are exhausted. */
66
+ protected enrichSessionSetupError(err: unknown): Error;
56
67
  /**
57
68
  * Dispatch the compact prompt result once fired. kimi: background
58
69
  * compaction — poll wire.jsonl for a NEW terminal record before translating
59
70
  * (§5.2). opencode: synchronous — standard prompt-settle translation.
60
71
  */
61
72
  protected abstract onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: TTranslator, sessionId: string, opts: SpawnOptions, baselineTotalCount: number): void;
73
+ private withSessionSetupRetry;
62
74
  protected currentSessionId(): string | null;
63
75
  protected cancelCurrentTurn(): Promise<void>;
64
76
  protected clearTurnState(): void;