@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 +72 -51
- package/lib/index.js +29 -93
- package/package.json +11 -9
- package/src/client/api.ts +2 -37
- package/src/client/card.tsx +76 -49
- package/src/client/client.ts +26 -7
- package/src/client/scope.ts +33 -0
- package/src/config-api.ts +5 -85
- package/src/config.ts +10 -55
- package/src/ns.ts +9 -0
- package/src/service.ts +10 -6
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
|
|
122
|
+
function draftFrom(value, catalog) {
|
|
123
123
|
const rows = {};
|
|
124
|
-
const names =
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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)(() =>
|
|
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 (
|
|
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,
|
|
316
|
+
const edit = (key, editValue) => {
|
|
313
317
|
setDraft({
|
|
314
318
|
...draft,
|
|
315
|
-
[key]:
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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 = !
|
|
347
|
-
const rowNames =
|
|
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(
|
|
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 = [
|
|
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
|
-
|
|
614
|
-
order: 100,
|
|
631
|
+
key: SETTINGS_NS,
|
|
615
632
|
locale: NS,
|
|
616
|
-
inject: () => ({
|
|
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(
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
for (const [name$1, entry] of Object.entries(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
/**
|
|
232
|
-
function
|
|
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(`
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
41
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.
|
|
42
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.
|
|
43
|
-
"@deepseek-ai/dsh-session": "0.1.0-rc.
|
|
44
|
-
"@deepseek-ai/dsh-subagent": "0.1.0-rc.
|
|
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-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
}
|
package/src/client/card.tsx
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 「子代理模型配置」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
|
|
3
|
-
* 交互对齐官方卡片:折叠/展开、staged draft
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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,
|
|
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
|
|
60
|
+
function draftFrom(value: ConfigValue, catalog: ModelCatalog | null): Draft {
|
|
43
61
|
const rows: Record<string, DraftRow> = {}
|
|
44
|
-
const names =
|
|
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 =
|
|
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:
|
|
76
|
+
return { enabled: value.enabled, rows }
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
function toPatch(draft: Draft):
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
() =>
|
|
187
|
-
|
|
188
|
-
|
|
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 (
|
|
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,
|
|
201
|
-
setDraft({ ...draft, [key]:
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 = !
|
|
227
|
-
const rowNames =
|
|
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(
|
|
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}
|
package/src/client/client.ts
CHANGED
|
@@ -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
|
-
*
|
|
8
|
-
* dsh-client-ui-settings-plugins 的
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
/**
|
|
101
|
-
export function
|
|
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(`
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
/**
|
|
146
|
-
export
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
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
|
-
* -
|
|
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 {
|
|
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
|
-
|
|
46
|
+
registerCatalogApi(webCtx)
|
|
43
47
|
})
|
|
44
48
|
}
|
|
45
49
|
|