@dsh-plus/subagent-model 0.1.0 → 0.1.2

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.
package/lib/client.js CHANGED
@@ -32,25 +32,25 @@ react = __toESM(react);
32
32
  let react_jsx_runtime = require("react/jsx-runtime");
33
33
  react_jsx_runtime = __toESM(react_jsx_runtime);
34
34
 
35
+ //#region src/ns.ts
36
+ /**
37
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
38
+ * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
39
+ * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
40
+ * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
41
+ * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
42
+ * @module subagent-model/ns
43
+ */
44
+ const SETTINGS_NS = "dsh-plus-subagent-model";
45
+
46
+ //#endregion
35
47
  //#region src/client/api.ts
36
- const ROUTE_CONFIG = "/dsh-plus/subagent-model/config";
37
48
  const ROUTE_CATALOG = "/dsh-plus/subagent-model/catalog";
38
49
  async function parse(res) {
39
50
  const body = await res.json();
40
51
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
41
52
  return body;
42
53
  }
43
- async function fetchConfig() {
44
- return parse(await fetch(ROUTE_CONFIG, { credentials: "same-origin" }));
45
- }
46
- async function saveConfig(patch) {
47
- return parse(await fetch(ROUTE_CONFIG, {
48
- method: "PUT",
49
- credentials: "same-origin",
50
- headers: { "content-type": "application/json" },
51
- body: JSON.stringify(patch)
52
- }));
53
- }
54
54
  async function fetchCatalog() {
55
55
  return parse(await fetch(ROUTE_CATALOG, { credentials: "same-origin" }));
56
56
  }
@@ -119,11 +119,13 @@ const EMPTY_ROW = {
119
119
  };
120
120
  const EFFORT_INHERIT = "inherit";
121
121
  const EFFORT_DEFAULT = "default";
122
- function draftFromWire(wire) {
122
+ function draftFrom(value, catalog) {
123
123
  const rows = {};
124
- const names = [...wire.subagentProviders, ...Object.keys(wire.entries).sort()];
124
+ const names = /* @__PURE__ */ new Set();
125
+ for (const name$1 of catalog?.subagentProviders ?? []) names.add(name$1);
126
+ for (const name$1 of Object.keys(value.entries).sort()) names.add(name$1);
125
127
  for (const name$1 of names) {
126
- const entry = wire.entries[name$1];
128
+ const entry = value.entries[name$1];
127
129
  rows[name$1] = entry === void 0 ? { ...EMPTY_ROW } : {
128
130
  enabled: entry.enabled,
129
131
  provider: entry.provider,
@@ -132,7 +134,7 @@ function draftFromWire(wire) {
132
134
  };
133
135
  }
134
136
  return {
135
- enabled: wire.enabled,
137
+ enabled: value.enabled,
136
138
  rows
137
139
  };
138
140
  }
@@ -142,12 +144,6 @@ function toPatch(draft) {
142
144
  entries: draft.rows
143
145
  };
144
146
  }
145
- function rowNamesOf(wire, draft) {
146
- const names = /* @__PURE__ */ new Set();
147
- for (const name$1 of wire.subagentProviders) names.add(name$1);
148
- for (const name$1 of Object.keys(draft.rows).sort()) names.add(name$1);
149
- return [...names];
150
- }
151
147
  function unknownOption(value, label) {
152
148
  return {
153
149
  value,
@@ -263,28 +259,33 @@ function RowBlock(props) {
263
259
  });
264
260
  }
265
261
  function SubagentModelCard(props) {
266
- const { t } = props;
262
+ const { t, scope, api } = props;
263
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
264
+ const value = snapshot.value;
267
265
  const [open, setOpen] = (0, react.useState)(false);
268
- const [wire, setWire] = (0, react.useState)(null);
269
266
  const [draft, setDraft] = (0, react.useState)(null);
270
267
  const [catalog, setCatalog] = (0, react.useState)(null);
271
268
  const [catalogFailed, setCatalogFailed] = (0, react.useState)(false);
272
- const [loadFailed, setLoadFailed] = (0, react.useState)(false);
273
269
  const [saving, setSaving] = (0, react.useState)(false);
274
270
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
275
271
  (0, react.useEffect)(() => {
276
- let alive = true;
277
- fetchConfig().then((loaded) => {
278
- if (!alive) return;
279
- setWire(loaded);
280
- setDraft(draftFromWire(loaded));
281
- }).catch(() => {
282
- if (alive) setLoadFailed(true);
272
+ if (value === void 0) return;
273
+ setDraft((current) => {
274
+ if (current === null) return draftFrom(value, catalog);
275
+ if (catalog === null) return current;
276
+ const rows = { ...current.rows };
277
+ let changed = false;
278
+ for (const name$1 of catalog.subagentProviders) {
279
+ if (name$1 in rows) continue;
280
+ rows[name$1] = { ...EMPTY_ROW };
281
+ changed = true;
282
+ }
283
+ return changed ? {
284
+ ...current,
285
+ rows
286
+ } : current;
283
287
  });
284
- return () => {
285
- alive = false;
286
- };
287
- }, []);
288
+ }, [value, catalog]);
288
289
  (0, react.useEffect)(() => {
289
290
  if (!open || catalog !== null) return;
290
291
  let alive = true;
@@ -299,20 +300,23 @@ function SubagentModelCard(props) {
299
300
  alive = false;
300
301
  };
301
302
  }, [open, catalog]);
302
- const dirty = (0, react.useMemo)(() => wire !== null && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromWire(wire))), [wire, draft]);
303
+ const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))), [
304
+ value,
305
+ draft,
306
+ catalog
307
+ ]);
303
308
  const invalid = (0, react.useMemo)(() => draft !== null && Object.values(draft.rows).some((row) => row.model.length > 0 && row.provider.length === 0), [draft]);
304
- if (loadFailed) return null;
305
- if (wire === null || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
309
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
306
310
  className: "dsm-card",
307
311
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
308
312
  className: "dsm-empty",
309
313
  children: t("loading")
310
314
  })
311
315
  });
312
- const edit = (key, value) => {
316
+ const edit = (key, editValue) => {
313
317
  setDraft({
314
318
  ...draft,
315
- [key]: value
319
+ [key]: editValue
316
320
  });
317
321
  setStatus(IDLE_STATUS);
318
322
  };
@@ -328,9 +332,16 @@ function SubagentModelCard(props) {
328
332
  };
329
333
  const onSave = () => {
330
334
  setSaving(true);
331
- saveConfig(toPatch(draft)).then((saved) => {
332
- setWire(saved);
333
- setDraft(draftFromWire(saved));
335
+ const revision = scope.getSnapshot().revision;
336
+ api.update({
337
+ ns: SETTINGS_NS,
338
+ patch: toPatch(draft),
339
+ ...revision !== void 0 ? { expectedRevision: revision } : {}
340
+ }).then(async (response) => {
341
+ if (!response.result.ok) throw new Error(response.result.error?.message ?? t("saveFailed"));
342
+ await scope.load();
343
+ const next = scope.getSnapshot().value;
344
+ if (next !== void 0) setDraft(draftFrom(next, catalog));
334
345
  setStatus(IDLE_STATUS);
335
346
  }).catch((error) => {
336
347
  setStatus({
@@ -343,8 +354,8 @@ function SubagentModelCard(props) {
343
354
  setCatalog(null);
344
355
  setCatalogFailed(false);
345
356
  };
346
- const disabled = !wire.writable;
347
- const rowNames = rowNamesOf(wire, draft);
357
+ const disabled = !snapshot.writable;
358
+ const rowNames = Object.keys(draft.rows).sort();
348
359
  const providerCount = (catalog?.providers ?? []).length;
349
360
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
350
361
  className: `dsm-card${open ? " dsm-cardOpen" : ""}`,
@@ -434,7 +445,7 @@ function SubagentModelCard(props) {
434
445
  className: "dsm-btn dsm-btnGhost",
435
446
  disabled: !dirty || saving,
436
447
  onClick: () => {
437
- setDraft(draftFromWire(wire));
448
+ setDraft(draftFrom(value, catalog));
438
449
  setStatus(IDLE_STATUS);
439
450
  },
440
451
  children: t("discard")
@@ -597,7 +608,12 @@ function injectStyle() {
597
608
  //#region src/client/client.ts
598
609
  const name = "dsh-plus-subagent-model";
599
610
  /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
600
- const inject = ["slots", "locale"];
611
+ const inject = [
612
+ "slots",
613
+ "locale",
614
+ "settingsScope",
615
+ "connection"
616
+ ];
601
617
  function apply(ctx) {
602
618
  const c = ctx;
603
619
  const tag = injectStyle();
@@ -608,12 +624,17 @@ function apply(ctx) {
608
624
  zh,
609
625
  en
610
626
  }), "subagent-model: locale");
627
+ const scope = c.settingsScope.bind({ namespace: SETTINGS_NS });
628
+ const api = c.get("connection").api.settings;
611
629
  c.slots.inject("settings.plugin.item", () => c.slots.register({
612
630
  name: "settings.plugin.item",
613
- id: "subagent-model",
614
- order: 100,
631
+ key: SETTINGS_NS,
615
632
  locale: NS,
616
- inject: () => ({ t: c.locale.bind(NS) })
633
+ inject: () => ({
634
+ t: c.locale.bind(NS),
635
+ scope,
636
+ api
637
+ })
617
638
  }, SubagentModelCard));
618
639
  }
619
640
 
package/lib/index.js CHANGED
@@ -2,9 +2,21 @@ import z from "@deepseek-ai/schemastery";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { Context, Service } from "@deepseek-ai/cordis";
4
4
 
5
+ //#region src/ns.ts
6
+ /**
7
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
8
+ * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
9
+ * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
10
+ * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
11
+ * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
12
+ * @module subagent-model/ns
13
+ */
14
+ const SETTINGS_NS$1 = "dsh-plus-subagent-model";
15
+
16
+ //#endregion
5
17
  //#region src/config.ts
6
- /** settings 命名空间;webui 配置卡片与插件运行期读取同一份。 */
7
- const SETTINGS_NS = settingsNamespace("dsh-plus-subagent-model");
18
+ /** settings 命名空间;webui 配置卡片与插件运行期读取同一份(字面量见 ./ns.ts)。 */
19
+ const SETTINGS_NS = settingsNamespace(SETTINGS_NS$1);
8
20
  /** 思考程度哨兵值:继承主代理(不注入 effort 字段)。 */
9
21
  const EFFORT_INHERIT = "inherit";
10
22
  /** 思考程度哨兵值:跟随模型默认(显式剥离任何继承/推导出的 effort)。 */
@@ -67,28 +79,13 @@ function validateEntry(entry) {
67
79
  if (effort.length === 0) return "reasoningEffort 不能为空(inherit / default / 档位 id)";
68
80
  return null;
69
81
  }
70
- function toWire(cfg, subagentProviders, writable) {
71
- const entries = {};
72
- for (const [name$1, entry] of Object.entries(cfg.entries)) entries[name$1] = {
73
- enabled: entry.enabled,
74
- provider: entry.provider,
75
- model: entry.model,
76
- reasoningEffort: entry.reasoningEffort
77
- };
78
- return {
79
- enabled: cfg.enabled,
80
- entries,
81
- subagentProviders: [...subagentProviders],
82
- writable
83
- };
84
- }
85
- const WirePatch = z.object({
86
- enabled: z.boolean(),
87
- entries: z.dict(EntrySchema)
88
- });
89
- /** 把卡片提交的 patch 规整为 settings.update 的用户层 patch(entries 整体替换)。 */
90
- function toUserPatch(input) {
91
- return { ...input };
82
+ /** 校验整组条目(settings 写入的 validate 钩子用);返回首个错误消息,合法时返回 null。 */
83
+ function validateEntries(entries) {
84
+ for (const [name$1, entry] of Object.entries(entries)) {
85
+ const error = validateEntry(entry);
86
+ if (error !== null) return `条目 ${name$1}: ${error}`;
87
+ }
88
+ return null;
92
89
  }
93
90
 
94
91
  //#endregion
@@ -160,76 +157,16 @@ function subagentProvidersOf(ctx) {
160
157
 
161
158
  //#endregion
162
159
  //#region src/config-api.ts
163
- const ROUTE_CONFIG = "/dsh-plus/subagent-model/config";
164
160
  const ROUTE_CATALOG = "/dsh-plus/subagent-model/catalog";
165
- const MAX_BODY_BYTES = 256 * 1024;
166
161
  function sendJson(res, status, body) {
167
162
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
168
163
  res.end(JSON.stringify(body));
169
164
  }
170
- function readBody(req) {
171
- return new Promise((resolve, reject) => {
172
- const chunks = [];
173
- let size = 0;
174
- req.on("data", (chunk) => {
175
- size += chunk.length;
176
- if (size > MAX_BODY_BYTES) {
177
- reject(/* @__PURE__ */ new Error("request body too large"));
178
- req.destroy();
179
- return;
180
- }
181
- chunks.push(chunk);
182
- });
183
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
184
- req.on("error", reject);
185
- });
186
- }
187
- async function readPatch(req) {
188
- const raw = await readBody(req);
189
- let parsed;
190
- try {
191
- parsed = JSON.parse(raw);
192
- } catch {
193
- throw new Error("request body is not valid JSON");
194
- }
195
- return WirePatch(parsed);
196
- }
197
- /** 校验用户提交的 entries(model 不能脱离 provider 等);返回首个错误消息或 null。 */
198
- function firstEntryError(patch) {
199
- for (const [name$1, entry] of Object.entries(patch.entries)) {
200
- const error = validateEntry(entry);
201
- if (error !== null) return `条目 ${name$1}: ${error}`;
202
- }
203
- return null;
204
- }
205
- async function handleConfig(ctx, service, req, res) {
206
- const settings = ctx.get("settings");
207
- if (req.method === "GET") {
208
- sendJson(res, 200, toWire(service.currentConfig(), service.subagentProviders(), settings !== void 0));
209
- return;
210
- }
211
- if (req.method !== "PUT") {
212
- sendJson(res, 405, { error: "method not allowed" });
213
- return;
214
- }
215
- if (settings === void 0) {
216
- sendJson(res, 503, { error: "settings provider 不可用,无法在线保存;请编辑 settings.yaml" });
217
- return;
218
- }
219
- const patch = await readPatch(req);
220
- const entryError = firstEntryError(patch);
221
- if (entryError !== null) {
222
- sendJson(res, 400, { error: entryError });
223
- return;
224
- }
225
- await settings.update(SETTINGS_NS, toUserPatch(patch));
226
- sendJson(res, 200, toWire(service.currentConfig(), service.subagentProviders(), true));
227
- }
228
165
  async function handleCatalog(ctx, res) {
229
166
  sendJson(res, 200, await buildModelCatalog(ctx));
230
167
  }
231
- /** 注册配置读写与目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
232
- function registerConfigApi(ctx, service) {
168
+ /** 注册目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
169
+ function registerCatalogApi(ctx) {
233
170
  const logger = ctx.logger("subagent-model");
234
171
  const guard = (handler) => {
235
172
  return async (req, res) => {
@@ -237,17 +174,12 @@ function registerConfigApi(ctx, service) {
237
174
  await handler(req, res);
238
175
  } catch (error) {
239
176
  const message = error instanceof Error ? error.message : String(error);
240
- logger.warn(`config api ${req.method ?? "?"} ${req.url ?? "?"} failed: ${message}`);
177
+ logger.warn(`catalog api ${req.method ?? "?"} ${req.url ?? "?"} failed: ${message}`);
241
178
  if (!res.headersSent) sendJson(res, 400, { error: message });
242
179
  else res.end();
243
180
  }
244
181
  };
245
182
  };
246
- ctx.webServer.register({
247
- kind: "exact",
248
- path: ROUTE_CONFIG,
249
- handler: guard((req, res) => handleConfig(ctx, service, req, res))
250
- });
251
183
  ctx.webServer.register({
252
184
  kind: "exact",
253
185
  path: ROUTE_CATALOG,
@@ -346,6 +278,10 @@ var SubagentModelService = class extends Service {
346
278
  super(ctx, "subagentModel");
347
279
  this.current = () => config;
348
280
  installSettingsSection(ctx, SETTINGS_NS, Config, config, {
281
+ validate: (cfg) => {
282
+ const error = validateEntries(cfg.entries);
283
+ if (error !== null) throw new Error(`subagent-model: ${error}`);
284
+ },
349
285
  setSource: (source) => {
350
286
  this.current = source;
351
287
  },
@@ -354,7 +290,7 @@ var SubagentModelService = class extends Service {
354
290
  installDelegationHook(ctx, () => this.current());
355
291
  installChildRoute(ctx);
356
292
  ctx.inject(["webServer"], (webCtx) => {
357
- registerConfigApi(webCtx, this);
293
+ registerCatalogApi(webCtx);
358
294
  });
359
295
  }
360
296
  /** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/subagent-model",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "dsh-plus service+ui plugin: 为 subagent/subagent_fork 等子代理按提供商配置模型与思考程度(或继承主代理)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -24,24 +24,26 @@
24
24
  "client": {
25
25
  "inject": [
26
26
  "@deepseek-ai/dsh-client-runtime",
27
- "@deepseek-ai/dsh-client-locale"
27
+ "@deepseek-ai/dsh-client-locale",
28
+ "@deepseek-ai/dsh-client-connection",
29
+ "@deepseek-ai/dsh-client-ui-settings"
28
30
  ],
29
31
  "platform": "web"
30
32
  }
31
33
  },
32
34
  "dependencies": {
33
35
  "@deepseek-ai/cordis": "4.0.1",
34
- "@deepseek-ai/dsh-settings": "0.1.0-rc.6",
36
+ "@deepseek-ai/dsh-settings": "0.1.0-rc.7",
35
37
  "@deepseek-ai/schemastery": "3.18.1"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@deepseek-ai/cordis": "4.0.1",
39
41
  "@deepseek-ai/cosmokit": "1.8.2",
40
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
41
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.6",
42
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
43
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
44
- "@deepseek-ai/dsh-subagent": "0.1.0-rc.6",
42
+ "@deepseek-ai/dsh-agent": "0.1.0-rc.7",
43
+ "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.7",
44
+ "@deepseek-ai/dsh-llm": "0.1.0-rc.7",
45
+ "@deepseek-ai/dsh-session": "0.1.0-rc.7",
46
+ "@deepseek-ai/dsh-subagent": "0.1.0-rc.7",
45
47
  "@types/react": "~18.3.1",
46
48
  "react": "^18.2.0"
47
49
  },
@@ -51,7 +53,7 @@
51
53
  "license": "MIT",
52
54
  "repository": {
53
55
  "type": "git",
54
- "url": "git+https://github.com/A-G-guy/dsh-plugins.git",
56
+ "url": "git+https://github.com/A-G-guy/dsh-plus.git",
55
57
  "directory": "packages/subagent-model"
56
58
  },
57
59
  "scripts": {
package/src/client/api.ts CHANGED
@@ -1,28 +1,9 @@
1
1
  /**
2
- * 配置卡片数据通道:同源 fetch 调自建 webServer 路由。
3
- * (官方 settings.* RPC 白名单硬编码不含第三方 namespace,见 config-api.ts 注释。)
2
+ * 自定义端点通道:仅剩「模型目录」(配置读写已迁移到官方 settingsScope
3
+ * 传输,见 scope.ts 与 card.tsx)。
4
4
  * @module subagent-model/client/api
5
5
  */
6
6
 
7
- export interface WireEntry {
8
- enabled: boolean
9
- provider: string
10
- model: string
11
- reasoningEffort: string
12
- }
13
-
14
- export interface WireConfig {
15
- enabled: boolean
16
- entries: Record<string, WireEntry>
17
- subagentProviders: string[]
18
- writable: boolean
19
- }
20
-
21
- export interface WirePatch {
22
- enabled: boolean
23
- entries: Record<string, WireEntry>
24
- }
25
-
26
7
  export interface CatalogEffort {
27
8
  id: string
28
9
  name: string
@@ -51,7 +32,6 @@ export interface ModelCatalog {
51
32
  subagentProviders: string[]
52
33
  }
53
34
 
54
- const ROUTE_CONFIG = '/dsh-plus/subagent-model/config'
55
35
  const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
56
36
 
57
37
  async function parse<T>(res: Response): Promise<T> {
@@ -60,21 +40,6 @@ async function parse<T>(res: Response): Promise<T> {
60
40
  return body
61
41
  }
62
42
 
63
- export async function fetchConfig(): Promise<WireConfig> {
64
- return parse<WireConfig>(await fetch(ROUTE_CONFIG, { credentials: 'same-origin' }))
65
- }
66
-
67
- export async function saveConfig(patch: WirePatch): Promise<WireConfig> {
68
- return parse<WireConfig>(
69
- await fetch(ROUTE_CONFIG, {
70
- method: 'PUT',
71
- credentials: 'same-origin',
72
- headers: { 'content-type': 'application/json' },
73
- body: JSON.stringify(patch),
74
- }),
75
- )
76
- }
77
-
78
43
  export async function fetchCatalog(): Promise<ModelCatalog> {
79
44
  return parse<ModelCatalog>(await fetch(ROUTE_CATALOG, { credentials: 'same-origin' }))
80
45
  }
@@ -1,20 +1,38 @@
1
1
  /**
2
2
  * 「子代理模型配置」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
3
- * 交互对齐官方卡片:折叠/展开、staged draft、未保存标记、保存/放弃;
4
- * 数据走自建同源路由(api.ts)。行集合 = 已注册子代理 provider ∪ 已配置条目,
5
- * 保存时全量写回 entries(未配置的新 provider 行以默认空值落盘,自文档化)。
3
+ * 交互对齐官方卡片:折叠/展开、staged draft、未保存标记、保存/放弃。
4
+ * 配置读写走官方 settingsScope 传输(scope.ts):value 为 schema 解析后的
5
+ * 命名空间值(enabled + entries),行集合 = 目录返回的已注册子代理 provider
6
+ * ∪ 已配置条目,保存时全量写回 entries(未配置的新 provider 行以默认空值
7
+ * 落盘,自文档化);「模型目录」为唯一保留的自定义端点。
6
8
  * @module subagent-model/client/card
7
9
  */
8
- import { useEffect, useMemo, useState, type ReactElement } from 'react'
10
+ import { useEffect, useMemo, useSyncExternalStore, useState, type ReactElement } from 'react'
9
11
 
12
+ import { SETTINGS_NS } from '../ns.ts'
10
13
  import {
11
- fetchCatalog, fetchConfig, saveConfig,
12
- type CatalogProvider, type ModelCatalog, type WireConfig, type WireEntry, type WirePatch,
14
+ fetchCatalog, type CatalogProvider, type ModelCatalog,
13
15
  } from './api.ts'
14
16
  import { CheckRow, SelectField, type SelectOption } from './fields.tsx'
17
+ import type { Scope, SettingsApi } from './scope.ts'
15
18
 
16
19
  export interface CardProps {
17
20
  t(key: string): string
21
+ scope: Scope
22
+ api: SettingsApi
23
+ }
24
+
25
+ /** settings 命名空间的解析值(无 secret 字段)。 */
26
+ export interface ConfigValue {
27
+ enabled: boolean
28
+ entries: Record<string, WireEntry>
29
+ }
30
+
31
+ export interface WireEntry {
32
+ enabled: boolean
33
+ provider: string
34
+ model: string
35
+ reasoningEffort: string
18
36
  }
19
37
 
20
38
  interface DraftRow {
@@ -39,11 +57,13 @@ const EMPTY_ROW: DraftRow = { enabled: false, provider: '', model: '', reasoning
39
57
  const EFFORT_INHERIT = 'inherit'
40
58
  const EFFORT_DEFAULT = 'default'
41
59
 
42
- function draftFromWire(wire: WireConfig): Draft {
60
+ function draftFrom(value: ConfigValue, catalog: ModelCatalog | null): Draft {
43
61
  const rows: Record<string, DraftRow> = {}
44
- const names = [...wire.subagentProviders, ...Object.keys(wire.entries).sort()]
62
+ const names = new Set<string>()
63
+ for (const name of catalog?.subagentProviders ?? []) names.add(name)
64
+ for (const name of Object.keys(value.entries).sort()) names.add(name)
45
65
  for (const name of names) {
46
- const entry = wire.entries[name]
66
+ const entry = value.entries[name]
47
67
  rows[name] = entry === undefined
48
68
  ? { ...EMPTY_ROW }
49
69
  : {
@@ -53,20 +73,13 @@ function draftFromWire(wire: WireConfig): Draft {
53
73
  reasoningEffort: entry.reasoningEffort,
54
74
  }
55
75
  }
56
- return { enabled: wire.enabled, rows }
76
+ return { enabled: value.enabled, rows }
57
77
  }
58
78
 
59
- function toPatch(draft: Draft): WirePatch {
79
+ function toPatch(draft: Draft): Record<string, unknown> {
60
80
  return { enabled: draft.enabled, entries: draft.rows }
61
81
  }
62
82
 
63
- function rowNamesOf(wire: WireConfig, draft: Draft): string[] {
64
- const names = new Set<string>()
65
- for (const name of wire.subagentProviders) names.add(name)
66
- for (const name of Object.keys(draft.rows).sort()) names.add(name)
67
- return [...names]
68
- }
69
-
70
83
  function unknownOption(value: string, label: string): SelectOption {
71
84
  return { value, label: `${value}(${label})` }
72
85
  }
@@ -139,31 +152,36 @@ function RowBlock(props: RowBlockProps): ReactElement {
139
152
  }
140
153
 
141
154
  export function SubagentModelCard(props: CardProps): ReactElement | null {
142
- const { t } = props
155
+ const { t, scope, api } = props
156
+ const snapshot = useSyncExternalStore(
157
+ (listener: () => void) => scope.subscribe(listener),
158
+ () => scope.getSnapshot(),
159
+ )
160
+ const value = snapshot.value as ConfigValue | undefined
143
161
  const [open, setOpen] = useState(false)
144
- const [wire, setWire] = useState<WireConfig | null>(null)
145
162
  const [draft, setDraft] = useState<Draft | null>(null)
146
163
  const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
147
164
  const [catalogFailed, setCatalogFailed] = useState(false)
148
- const [loadFailed, setLoadFailed] = useState(false)
149
165
  const [saving, setSaving] = useState(false)
150
166
  const [status, setStatus] = useState<Status>(IDLE_STATUS)
151
167
 
168
+ // 播种草稿(行集 = 已配置条目);catalog 到达后只补缺失的 provider 空行,
169
+ // 不覆盖在途编辑;后续 Host 更新同样不覆盖。
152
170
  useEffect(() => {
153
- let alive = true
154
- fetchConfig()
155
- .then((loaded) => {
156
- if (!alive) return
157
- setWire(loaded)
158
- setDraft(draftFromWire(loaded))
159
- })
160
- .catch(() => {
161
- if (alive) setLoadFailed(true)
162
- })
163
- return () => {
164
- alive = false
165
- }
166
- }, [])
171
+ if (value === undefined) return
172
+ setDraft((current) => {
173
+ if (current === null) return draftFrom(value, catalog)
174
+ if (catalog === null) return current
175
+ const rows = { ...current.rows }
176
+ let changed = false
177
+ for (const name of catalog.subagentProviders) {
178
+ if (name in rows) continue
179
+ rows[name] = { ...EMPTY_ROW }
180
+ changed = true
181
+ }
182
+ return changed ? { ...current, rows } : current
183
+ })
184
+ }, [value, catalog])
167
185
 
168
186
  useEffect(() => {
169
187
  if (!open || catalog !== null) return
@@ -183,9 +201,10 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
183
201
  }, [open, catalog])
184
202
 
185
203
  const dirty = useMemo(
186
- () => wire !== null && draft !== null &&
187
- JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromWire(wire))),
188
- [wire, draft],
204
+ () =>
205
+ value !== undefined && draft !== null &&
206
+ JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))),
207
+ [value, draft, catalog],
189
208
  )
190
209
  const invalid = useMemo(
191
210
  () => draft !== null && Object.values(draft.rows).some((row) =>
@@ -193,12 +212,11 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
193
212
  [draft],
194
213
  )
195
214
 
196
- if (loadFailed) return null
197
- if (wire === null || draft === null) {
215
+ if (value === undefined || draft === null) {
198
216
  return <li className="dsm-card"><p className="dsm-empty">{t('loading')}</p></li>
199
217
  }
200
- const edit = <K extends keyof Draft>(key: K, value: Draft[K]): void => {
201
- setDraft({ ...draft, [key]: value })
218
+ const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
219
+ setDraft({ ...draft, [key]: editValue })
202
220
  setStatus(IDLE_STATUS)
203
221
  }
204
222
  const editRow = (name: string, row: DraftRow): void => {
@@ -207,10 +225,19 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
207
225
  }
208
226
  const onSave = (): void => {
209
227
  setSaving(true)
210
- saveConfig(toPatch(draft))
211
- .then((saved) => {
212
- setWire(saved)
213
- setDraft(draftFromWire(saved))
228
+ const revision = scope.getSnapshot().revision
229
+ api.update({
230
+ ns: SETTINGS_NS,
231
+ patch: toPatch(draft),
232
+ ...(revision !== undefined ? { expectedRevision: revision } : {}),
233
+ })
234
+ .then(async (response) => {
235
+ if (!response.result.ok) {
236
+ throw new Error(response.result.error?.message ?? t('saveFailed'))
237
+ }
238
+ await scope.load()
239
+ const next = scope.getSnapshot().value as ConfigValue | undefined
240
+ if (next !== undefined) setDraft(draftFrom(next, catalog))
214
241
  setStatus(IDLE_STATUS)
215
242
  })
216
243
  .catch((error: unknown) => {
@@ -223,8 +250,8 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
223
250
  setCatalogFailed(false)
224
251
  }
225
252
 
226
- const disabled = !wire.writable
227
- const rowNames = rowNamesOf(wire, draft)
253
+ const disabled = !snapshot.writable
254
+ const rowNames = Object.keys(draft.rows).sort()
228
255
  const providerCount = (catalog?.providers ?? []).length
229
256
  return (
230
257
  <li className={`dsm-card${open ? ' dsm-cardOpen' : ''}`}>
@@ -276,7 +303,7 @@ export function SubagentModelCard(props: CardProps): ReactElement | null {
276
303
  </p>
277
304
  ) : null}
278
305
  <button type="button" className="dsm-btn dsm-btnGhost" disabled={!dirty || saving}
279
- onClick={() => { setDraft(draftFromWire(wire)); setStatus(IDLE_STATUS) }}>
306
+ onClick={() => { setDraft(draftFrom(value, catalog)); setStatus(IDLE_STATUS) }}>
280
307
  {t('discard')}
281
308
  </button>
282
309
  <button type="button" className="dsm-btn dsm-btnPrimary" disabled={!dirty || invalid || saving || disabled}
@@ -3,21 +3,29 @@
3
3
  * 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
4
4
  * (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
5
5
  *
6
- * 类型说明:浏览器半只用到 slots/locale 的很窄一面,此处以最小本地接口声明,
7
- * 避免为构建期类型引入整条官方 client 依赖树;运行时契约以官方
8
- * dsh-client-ui-settings-plugins 的 settings.plugin.item 插槽为准。
6
+ * 类型说明:浏览器半只用到 slots/locale/settingsScope/connection 的很窄一面,
7
+ * 此处以最小本地接口声明(见 ./scope.ts),避免为构建期类型引入整条官方
8
+ * client 依赖树;运行时契约以官方 dsh-client-ui-settings-plugins 的
9
+ * settings.plugin.item 插槽与 dsh-client-ui-settings 的 settingsScope 服务为准。
10
+ * rc7 起该插槽为 keyed 槽位:key 必须是卡片编辑的 settings 命名空间
11
+ * (即服务端 settingsNamespace() 注册的同一字面量,见 ../ns.ts),
12
+ * 官方配置页按此 key 与 Host 已注册命名空间配对分发。
13
+ * 配置读写经官方 settingsScope 传输(rc7 起第三方命名空间对 settings RPC
14
+ * 全量开放);自定义端点仅剩「模型目录」(api.ts)。
9
15
  * @module @dsh-plus/subagent-model/client
10
16
  */
11
17
  import type { Context } from '@deepseek-ai/cordis'
12
18
 
19
+ import { SETTINGS_NS } from '../ns.ts'
13
20
  import { SubagentModelCard } from './card.tsx'
14
21
  import { en, NS, zh } from './i18n.ts'
22
+ import type { Scope, SettingsApi } from './scope.ts'
15
23
  import { injectStyle } from './styles.ts'
16
24
 
17
25
  export const name = 'dsh-plus-subagent-model'
18
26
 
19
27
  /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
20
- export const inject = ['slots', 'locale'] as const
28
+ export const inject = ['slots', 'locale', 'settingsScope', 'connection'] as const
21
29
 
22
30
  interface SlotsLike {
23
31
  inject(key: string, callback: () => unknown): unknown
@@ -29,9 +37,19 @@ interface LocaleLike {
29
37
  bind(ns: string): (key: string) => string
30
38
  }
31
39
 
40
+ interface SettingsScopeServiceLike {
41
+ bind(spec: { namespace: string }): Scope
42
+ }
43
+
44
+ interface ConnectionLike {
45
+ settings: SettingsApi
46
+ }
47
+
32
48
  interface ClientContext {
33
49
  slots: SlotsLike
34
50
  locale: LocaleLike
51
+ settingsScope: SettingsScopeServiceLike
52
+ get(key: 'connection'): ConnectionLike
35
53
  effect(execute: () => () => void, label?: string): unknown
36
54
  }
37
55
 
@@ -48,14 +66,15 @@ export function apply(ctx: Context): void {
48
66
  () => c.locale.register(NS, { zh, en }),
49
67
  'subagent-model: locale',
50
68
  )
69
+ const scope = c.settingsScope.bind({ namespace: SETTINGS_NS })
70
+ const api = c.get('connection').api.settings
51
71
  c.slots.inject('settings.plugin.item', () =>
52
72
  c.slots.register(
53
73
  {
54
74
  name: 'settings.plugin.item',
55
- id: 'subagent-model',
56
- order: 100,
75
+ key: SETTINGS_NS,
57
76
  locale: NS,
58
- inject: () => ({ t: c.locale.bind(NS) }),
77
+ inject: () => ({ t: c.locale.bind(NS), scope, api }),
59
78
  },
60
79
  SubagentModelCard,
61
80
  ),
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 浏览器半 settingsScope/connection 的最小本地接口声明。
3
+ * 只声明本插件用到的窄面,避免为构建期类型引入整条官方 client 依赖树;
4
+ * 运行时契约以官方 dsh-client-ui-settings 的 settingsScope 服务与
5
+ * dsh-client-connection 的 settings RPC 面为准(rc7:白名单已移除,
6
+ * 任何已注册命名空间均可 describe/update/replace)。
7
+ * @module subagent-model/client/scope
8
+ */
9
+
10
+ /** settings 命名空间 scope 的快照(官方 SettingsScopeSnapshot 的最小投影)。 */
11
+ export interface ScopeSnapshot {
12
+ status: 'loading' | 'ready' | 'unavailable'
13
+ /** schema 解析后的配置值(secret 字段已脱敏剥除)。 */
14
+ value: unknown
15
+ /** 命名空间 revision,写操作 fencing 用;首次 Host 应答前为 undefined。 */
16
+ revision: number | undefined
17
+ /** Host 文档是否可写。 */
18
+ writable: boolean
19
+ }
20
+
21
+ /** settingsScope.bind({namespace}) 返回的控制器面。 */
22
+ export interface Scope {
23
+ getSnapshot(): ScopeSnapshot
24
+ subscribe(listener: () => void): () => void
25
+ load(): Promise<void>
26
+ }
27
+
28
+ /** connection api.settings 的 RPC 面(本插件用到的三个方法)。 */
29
+ export interface SettingsApi {
30
+ describe(): Promise<{ result: { ok: boolean; value?: { namespaces: Array<{ ns: string; secrets: Array<{ path: string[]; set: boolean }> }> }; error?: { message?: string } } }>
31
+ update(request: { ns: string; patch: Record<string, unknown>; expectedRevision?: number }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
32
+ replace(request: { ns: string; section: Record<string, unknown>; expectedRevision?: number }): Promise<{ result: { ok: boolean; error?: { message?: string } } }>
33
+ }
package/src/config-api.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  /**
2
- * 配置 HTTP 通道:webui 插件配置卡片的后端。
3
- * 背景:官方 apiproxy 的 settings.* RPC 对 namespace 有硬编码白名单,第三方
4
- * namespace 一律 settings-not-exposed;故卡片数据走自建 webServer 路由,
5
- * 读写仍经 ctx.settings 用户层(持久化 $DSH_HOME/settings.yaml)。
2
+ * 自定义端点 HTTP 通道:配置读写已迁移到官方 settings RPC(rc7 起第三方
3
+ * 命名空间全量开放),这里只保留官方传输没有的「模型目录」端点。
6
4
  * 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
7
5
  * @module subagent-model/config-api
8
6
  */
@@ -11,94 +9,21 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
11
9
  import type { Context } from '@deepseek-ai/cordis'
12
10
 
13
11
  import { buildModelCatalog, type ModelCatalog } from './catalog.ts'
14
- import {
15
- SETTINGS_NS, toUserPatch, toWire, validateEntry, WirePatch, type WirePatchOutput,
16
- } from './config.ts'
17
- import type { SubagentModelService } from './service.ts'
18
12
 
19
- const ROUTE_CONFIG = '/dsh-plus/subagent-model/config'
20
13
  const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
21
- const MAX_BODY_BYTES = 256 * 1024
22
14
 
23
15
  function sendJson(res: ServerResponse, status: number, body: unknown): void {
24
16
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
25
17
  res.end(JSON.stringify(body))
26
18
  }
27
19
 
28
- function readBody(req: IncomingMessage): Promise<string> {
29
- return new Promise((resolve, reject) => {
30
- const chunks: Buffer[] = []
31
- let size = 0
32
- req.on('data', (chunk: Buffer) => {
33
- size += chunk.length
34
- if (size > MAX_BODY_BYTES) {
35
- reject(new Error('request body too large'))
36
- req.destroy()
37
- return
38
- }
39
- chunks.push(chunk)
40
- })
41
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
42
- req.on('error', reject)
43
- })
44
- }
45
-
46
- async function readPatch(req: IncomingMessage): Promise<WirePatchOutput> {
47
- const raw = await readBody(req)
48
- let parsed: unknown
49
- try {
50
- parsed = JSON.parse(raw)
51
- } catch {
52
- throw new Error('request body is not valid JSON')
53
- }
54
- return WirePatch(parsed)
55
- }
56
-
57
- /** 校验用户提交的 entries(model 不能脱离 provider 等);返回首个错误消息或 null。 */
58
- function firstEntryError(patch: WirePatchOutput): string | null {
59
- for (const [name, entry] of Object.entries(patch.entries)) {
60
- const error = validateEntry(entry)
61
- if (error !== null) return `条目 ${name}: ${error}`
62
- }
63
- return null
64
- }
65
-
66
- async function handleConfig(
67
- ctx: Context,
68
- service: SubagentModelService,
69
- req: IncomingMessage,
70
- res: ServerResponse,
71
- ): Promise<void> {
72
- const settings = ctx.get('settings')
73
- if (req.method === 'GET') {
74
- sendJson(res, 200, toWire(service.currentConfig(), service.subagentProviders(), settings !== undefined))
75
- return
76
- }
77
- if (req.method !== 'PUT') {
78
- sendJson(res, 405, { error: 'method not allowed' })
79
- return
80
- }
81
- if (settings === undefined) {
82
- sendJson(res, 503, { error: 'settings provider 不可用,无法在线保存;请编辑 settings.yaml' })
83
- return
84
- }
85
- const patch = await readPatch(req)
86
- const entryError = firstEntryError(patch)
87
- if (entryError !== null) {
88
- sendJson(res, 400, { error: entryError })
89
- return
90
- }
91
- await settings.update(SETTINGS_NS, toUserPatch(patch))
92
- sendJson(res, 200, toWire(service.currentConfig(), service.subagentProviders(), true))
93
- }
94
-
95
20
  async function handleCatalog(ctx: Context, res: ServerResponse): Promise<void> {
96
21
  const catalog: ModelCatalog = await buildModelCatalog(ctx)
97
22
  sendJson(res, 200, catalog)
98
23
  }
99
24
 
100
- /** 注册配置读写与目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
101
- export function registerConfigApi(ctx: Context, service: SubagentModelService): void {
25
+ /** 注册目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
26
+ export function registerCatalogApi(ctx: Context): void {
102
27
  const logger = ctx.logger('subagent-model')
103
28
  const guard = (
104
29
  handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>,
@@ -108,17 +33,12 @@ export function registerConfigApi(ctx: Context, service: SubagentModelService):
108
33
  await handler(req, res)
109
34
  } catch (error) {
110
35
  const message = error instanceof Error ? error.message : String(error)
111
- logger.warn(`config api ${req.method ?? '?'} ${req.url ?? '?'} failed: ${message}`)
36
+ logger.warn(`catalog api ${req.method ?? '?'} ${req.url ?? '?'} failed: ${message}`)
112
37
  if (!res.headersSent) sendJson(res, 400, { error: message })
113
38
  else res.end()
114
39
  }
115
40
  }
116
41
  }
117
- ctx.webServer.register({
118
- kind: 'exact',
119
- path: ROUTE_CONFIG,
120
- handler: guard((req, res) => handleConfig(ctx, service, req, res)),
121
- })
122
42
  ctx.webServer.register({
123
43
  kind: 'exact',
124
44
  path: ROUTE_CATALOG,
package/src/config.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  import z from '@deepseek-ai/schemastery'
18
18
  import { settingsNamespace } from '@deepseek-ai/dsh-settings'
19
19
 
20
- /** settings 命名空间;webui 配置卡片与插件运行期读取同一份。 */
21
- export const SETTINGS_NS = settingsNamespace('dsh-plus-subagent-model')
20
+ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
21
+
22
+ /** settings 命名空间;webui 配置卡片与插件运行期读取同一份(字面量见 ./ns.ts)。 */
23
+ export const SETTINGS_NS = settingsNamespace(NS_LITERAL)
22
24
 
23
25
  /** 思考程度哨兵值:继承主代理(不注入 effort 字段)。 */
24
26
  export const EFFORT_INHERIT = 'inherit'
@@ -142,58 +144,11 @@ export function validateEntry(entry: EntryValidationInput): string | null {
142
144
  return null
143
145
  }
144
146
 
145
- /** 配置卡片读取用的传输对象(writable 表示是否存在可写的 settings provider)。 */
146
- export interface WireEntry {
147
- enabled: boolean
148
- provider: string
149
- model: string
150
- reasoningEffort: string
151
- }
152
-
153
- export interface WireConfig {
154
- enabled: boolean
155
- entries: Record<string, WireEntry>
156
- /** 当前已注册的子代理 provider 名(spawn/fork/…),供卡片合成空行。 */
157
- subagentProviders: string[]
158
- writable: boolean
159
- }
160
-
161
- export function toWire(cfg: SubagentModelConfig, subagentProviders: string[], writable: boolean): WireConfig {
162
- const entries: Record<string, WireEntry> = {}
163
- for (const [name, entry] of Object.entries(cfg.entries)) {
164
- entries[name] = {
165
- enabled: entry.enabled,
166
- provider: entry.provider,
167
- model: entry.model,
168
- reasoningEffort: entry.reasoningEffort,
169
- }
147
+ /** 校验整组条目(settings 写入的 validate 钩子用);返回首个错误消息,合法时返回 null。 */
148
+ export function validateEntries(entries: Record<string, EntryValidationInput>): string | null {
149
+ for (const [name, entry] of Object.entries(entries)) {
150
+ const error = validateEntry(entry)
151
+ if (error !== null) return `条目 ${name}: ${error}`
170
152
  }
171
- return { enabled: cfg.enabled, entries, subagentProviders: [...subagentProviders], writable }
172
- }
173
-
174
- /** 配置卡片写回用的宽松输入形态(字段全部可选,无默认值,深合并进用户层)。 */
175
- export interface WirePatchInput {
176
- enabled?: boolean | null
177
- entries?: Record<string, {
178
- enabled?: boolean | null
179
- provider?: string | null
180
- model?: string | null
181
- reasoningEffort?: string | null
182
- }> | null
183
- }
184
-
185
- /** 配置卡片写回用的规范输出形态(校验后条目已填默认)。 */
186
- export interface WirePatchOutput {
187
- enabled: boolean
188
- entries: Record<string, EntryConfig>
189
- }
190
-
191
- export const WirePatch: z<WirePatchInput, WirePatchOutput> = z.object({
192
- enabled: z.boolean(),
193
- entries: z.dict(EntrySchema),
194
- })
195
-
196
- /** 把卡片提交的 patch 规整为 settings.update 的用户层 patch(entries 整体替换)。 */
197
- export function toUserPatch(input: WirePatchInput): Record<string, unknown> {
198
- return { ...input }
153
+ return null
199
154
  }
package/src/ns.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
3
+ * 服务端经 settingsNamespace() 品牌化后使用(config.ts),浏览器半将其作为
4
+ * settings.plugin.item keyed 槽位的 key —— 同一字面量两处共用,防止漂移。
5
+ * 契约:dsh rc7 起该槽位按「卡片编辑的 settings 命名空间」作 key 分发,
6
+ * 官方配置页只渲染 key 命中 Host 已注册命名空间的卡片。
7
+ * @module subagent-model/ns
8
+ */
9
+ export const SETTINGS_NS = 'dsh-plus-subagent-model'
package/src/service.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * SubagentModelService:子代理模型配置中枢。
3
3
  * - 配置:installSettingsSection 接入 settings 用户层($DSH_HOME/settings.yaml,热生效),
4
- * 无 settings provider 时退化为 cordis 行级 config。
4
+ * 无 settings provider 时退化为 cordis 行级 config;写入经 validate 钩子
5
+ * 走 validateEntries 规则(model 不能脱离 provider 等),非法即拒。
5
6
  * - 委托挂钩:包装 ctx.subagents.start/startContinuable,按 provider 名注入
6
7
  * agentOptions(provider/model/effort 私有标记),未命中条目完全直通。
7
8
  * - 子代理路由:根 ctx 的 agent/request 瀑布监听,把条目 effort 应用到子代理请求。
8
- * - 配置通道:ctx.webServer 注册 config/catalog 同源路由(官方 settings RPC
9
- * 白名单不含第三方 namespace)。
9
+ * - 自定义端点:ctx.webServer 仅注册模型目录路由(配置读写已走官方 settings RPC)。
10
10
  * @module @dsh-plus/subagent-model
11
11
  */
12
12
  import { Context, Service } from '@deepseek-ai/cordis'
13
13
  import { installSettingsSection } from '@deepseek-ai/dsh-settings'
14
14
 
15
- import { registerConfigApi } from './config-api.ts'
16
- import { Config, SETTINGS_NS, type SubagentModelConfig } from './config.ts'
15
+ import { registerCatalogApi } from './config-api.ts'
16
+ import { Config, SETTINGS_NS, validateEntries, type SubagentModelConfig } from './config.ts'
17
17
  import { installChildRoute, installDelegationHook } from './delegation.ts'
18
18
 
19
19
  declare module '@deepseek-ai/cordis' {
@@ -31,6 +31,10 @@ export class SubagentModelService extends Service {
31
31
  super(ctx, 'subagentModel')
32
32
  this.current = () => config
33
33
  installSettingsSection(ctx, SETTINGS_NS, Config, config, {
34
+ validate: (cfg: SubagentModelConfig) => {
35
+ const error = validateEntries(cfg.entries)
36
+ if (error !== null) throw new Error(`subagent-model: ${error}`)
37
+ },
34
38
  setSource: (source) => {
35
39
  this.current = source
36
40
  },
@@ -39,7 +43,7 @@ export class SubagentModelService extends Service {
39
43
  installDelegationHook(ctx, () => this.current())
40
44
  installChildRoute(ctx)
41
45
  ctx.inject(['webServer'], (webCtx) => {
42
- registerConfigApi(webCtx, this)
46
+ registerCatalogApi(webCtx)
43
47
  })
44
48
  }
45
49