@dsh-plus/subagent-model 0.1.0
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/LICENSE +21 -0
- package/lib/client.js +626 -0
- package/lib/index.d.ts +59 -0
- package/lib/index.js +380 -0
- package/package.json +61 -0
- package/src/catalog.ts +114 -0
- package/src/client/api.ts +80 -0
- package/src/client/card.tsx +293 -0
- package/src/client/client.ts +63 -0
- package/src/client/fields.tsx +75 -0
- package/src/client/i18n.ts +76 -0
- package/src/client/styles.ts +66 -0
- package/src/config-api.ts +133 -0
- package/src/config.ts +199 -0
- package/src/delegation.ts +119 -0
- package/src/index.ts +25 -0
- package/src/service.ts +56 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import "@deepseek-ai/dsh-settings";
|
|
3
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region src/config.d.ts
|
|
6
|
+
|
|
7
|
+
/** 一条条目的规范输出形态。 */
|
|
8
|
+
interface EntryConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
reasoningEffort: string;
|
|
13
|
+
}
|
|
14
|
+
/** 配置的规范输出形态。 */
|
|
15
|
+
interface SubagentModelConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
entries: Record<string, EntryConfig>;
|
|
18
|
+
}
|
|
19
|
+
/** 配置的宽松输入形态(schema 校验前的用户数据)。 */
|
|
20
|
+
interface SubagentModelConfigInput {
|
|
21
|
+
enabled?: boolean | null;
|
|
22
|
+
entries?: Record<string, {
|
|
23
|
+
enabled?: boolean | null;
|
|
24
|
+
provider?: string | null;
|
|
25
|
+
model?: string | null;
|
|
26
|
+
reasoningEffort?: string | null;
|
|
27
|
+
}> | null;
|
|
28
|
+
}
|
|
29
|
+
declare const Config: z<SubagentModelConfigInput, SubagentModelConfig>;
|
|
30
|
+
/** 注入子代理 AgentOptions 的结果对象:只含需要覆盖的字段。 */
|
|
31
|
+
interface InjectedOptions {
|
|
32
|
+
provider?: string;
|
|
33
|
+
model?: string;
|
|
34
|
+
/** 私有标记:随 agentOptions 抵达子代理 options,由 agent/request 监听器消费。 */
|
|
35
|
+
reasoningEffort?: string;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/service.d.ts
|
|
39
|
+
declare module '@deepseek-ai/cordis' {
|
|
40
|
+
interface Context {
|
|
41
|
+
subagentModel: SubagentModelService;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
declare class SubagentModelService extends Service {
|
|
45
|
+
static [Context.inject]: string[];
|
|
46
|
+
private current;
|
|
47
|
+
constructor(ctx: Context, config: SubagentModelConfig);
|
|
48
|
+
/** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */
|
|
49
|
+
currentConfig(): SubagentModelConfig;
|
|
50
|
+
/** 已注册的子代理 provider 名(配置卡片据此合成行)。 */
|
|
51
|
+
subagentProviders(): string[];
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/index.d.ts
|
|
55
|
+
declare const name = "dsh-plus-subagent-model";
|
|
56
|
+
declare const inject: readonly ["subagents", "llm"];
|
|
57
|
+
declare function apply(ctx: Context, config: SubagentModelConfig): void;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { Config, type EntryConfig, type InjectedOptions, type SubagentModelConfig, SubagentModelService, apply, inject, name };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
3
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region src/config.ts
|
|
6
|
+
/** settings 命名空间;webui 配置卡片与插件运行期读取同一份。 */
|
|
7
|
+
const SETTINGS_NS = settingsNamespace("dsh-plus-subagent-model");
|
|
8
|
+
/** 思考程度哨兵值:继承主代理(不注入 effort 字段)。 */
|
|
9
|
+
const EFFORT_INHERIT = "inherit";
|
|
10
|
+
/** 思考程度哨兵值:跟随模型默认(显式剥离任何继承/推导出的 effort)。 */
|
|
11
|
+
const EFFORT_DEFAULT = "default";
|
|
12
|
+
const EntrySchema = z.object({
|
|
13
|
+
enabled: z.boolean().description("该子代理 provider 行的开关").default(false),
|
|
14
|
+
provider: z.string().description("LLM 提供商 id;留空 = 继承主代理").default(""),
|
|
15
|
+
model: z.string().description("模型 id;留空 = 继承主代理").default(""),
|
|
16
|
+
reasoningEffort: z.string().description(`思考程度:${EFFORT_INHERIT}(继承主代理)/ ${EFFORT_DEFAULT}(跟随模型默认)/ 提供商目录档位 id`).default(EFFORT_INHERIT)
|
|
17
|
+
});
|
|
18
|
+
const Config = z.object({
|
|
19
|
+
enabled: z.boolean().description("总开关").default(false),
|
|
20
|
+
entries: z.dict(EntrySchema).description("按子代理 provider 名(spawn/fork/…)配置").default({})
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* 解析一条条目为要注入的 agentOptions 片段。
|
|
24
|
+
* 未启用或全继承时返回 undefined(完全保持 dsh 原生行为)。
|
|
25
|
+
*/
|
|
26
|
+
function resolveEntry(entry) {
|
|
27
|
+
if (!entry.enabled) return void 0;
|
|
28
|
+
const out = {};
|
|
29
|
+
if (entry.provider.length > 0) out.provider = entry.provider;
|
|
30
|
+
if (entry.model.length > 0) out.model = entry.model;
|
|
31
|
+
if (entry.reasoningEffort !== EFFORT_INHERIT) out.reasoningEffort = entry.reasoningEffort;
|
|
32
|
+
return out.provider !== void 0 || out.model !== void 0 || out.reasoningEffort !== void 0 ? out : void 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 合并注入片段与工具行已有的 agentOptions:部署级显式配置优先,
|
|
36
|
+
* 插件只补空缺(standard preset 的工具行无 agentOptions,插件即生效)。
|
|
37
|
+
*/
|
|
38
|
+
function mergeAgentOptions(injected, existing) {
|
|
39
|
+
return {
|
|
40
|
+
...injected,
|
|
41
|
+
...existing
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 把条目的 effort 决策应用到一次 LLM 请求配置上(纯函数):
|
|
46
|
+
* - undefined:直通(未配置 effort,保持原生推导);
|
|
47
|
+
* - EFFORT_DEFAULT:剥离 reasoningEffort(跟随模型默认,覆盖 fork 的 seed 继承);
|
|
48
|
+
* - 其余:显式覆盖。
|
|
49
|
+
*/
|
|
50
|
+
function applyEffort(resolved, effort) {
|
|
51
|
+
if (effort === void 0) return resolved;
|
|
52
|
+
if (effort === EFFORT_DEFAULT) {
|
|
53
|
+
const { reasoningEffort: _dropped,...rest } = resolved;
|
|
54
|
+
return rest;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...resolved,
|
|
58
|
+
reasoningEffort: effort
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** 校验一条用户提交的条目;返回错误消息,合法时返回 null。 */
|
|
62
|
+
function validateEntry(entry) {
|
|
63
|
+
const model = entry.model ?? "";
|
|
64
|
+
const provider = entry.provider ?? "";
|
|
65
|
+
const effort = entry.reasoningEffort ?? "";
|
|
66
|
+
if (model.length > 0 && provider.length === 0) return "model 不能脱离 provider 单独配置(请先选择提供商或改回继承)";
|
|
67
|
+
if (effort.length === 0) return "reasoningEffort 不能为空(inherit / default / 档位 id)";
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
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 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/catalog.ts
|
|
96
|
+
function toEffort(effort) {
|
|
97
|
+
return {
|
|
98
|
+
id: String(effort.id),
|
|
99
|
+
name: effort.name,
|
|
100
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 构建目录。单个提供商失败只进入 failures,不拖垮其余组;
|
|
105
|
+
* 无模型的组被丢弃(与官方 buildModelCatalog 同语义)。
|
|
106
|
+
*/
|
|
107
|
+
async function buildModelCatalog(ctx) {
|
|
108
|
+
const llm = ctx.get("llm");
|
|
109
|
+
if (llm === void 0) return {
|
|
110
|
+
providers: [],
|
|
111
|
+
failures: [],
|
|
112
|
+
subagentProviders: subagentProvidersOf(ctx)
|
|
113
|
+
};
|
|
114
|
+
const catalog = await Promise.all(llm.listProviders().map(async (provider) => {
|
|
115
|
+
try {
|
|
116
|
+
const models = await llm.listModels(provider.id);
|
|
117
|
+
const entries = await Promise.all(models.map(async (model) => {
|
|
118
|
+
const resolved = await llm.resolveModelInfo(provider.id, model.id);
|
|
119
|
+
const reasoning = resolved.reasoning === void 0 ? void 0 : {
|
|
120
|
+
efforts: resolved.reasoning.efforts.map(toEffort),
|
|
121
|
+
...resolved.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: String(resolved.reasoning.defaultEffort) }
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
id: model.id,
|
|
125
|
+
name: model.name,
|
|
126
|
+
...model.description === void 0 ? {} : { description: model.description },
|
|
127
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
128
|
+
};
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
kind: "group",
|
|
132
|
+
group: {
|
|
133
|
+
id: provider.id,
|
|
134
|
+
name: provider.name,
|
|
135
|
+
models: entries
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return {
|
|
140
|
+
kind: "failure",
|
|
141
|
+
failure: {
|
|
142
|
+
id: provider.id,
|
|
143
|
+
name: provider.name,
|
|
144
|
+
message: error instanceof Error ? error.message : String(error)
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}));
|
|
149
|
+
return {
|
|
150
|
+
providers: catalog.filter((item) => item.kind === "group" && item.group.models.length > 0).map((item) => item.kind === "group" ? item.group : void 0).filter((group) => group !== void 0),
|
|
151
|
+
failures: catalog.filter((item) => item.kind === "failure").map((item) => item.kind === "failure" ? item.failure : void 0).filter((failure) => failure !== void 0),
|
|
152
|
+
subagentProviders: subagentProvidersOf(ctx)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** 已注册的子代理 provider 名(llm 服务缺失时同样返回,卡片仍可渲染)。 */
|
|
156
|
+
function subagentProvidersOf(ctx) {
|
|
157
|
+
const subagents = ctx.get("subagents");
|
|
158
|
+
return subagents === void 0 ? [] : [...subagents.list()];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/config-api.ts
|
|
163
|
+
const ROUTE_CONFIG = "/dsh-plus/subagent-model/config";
|
|
164
|
+
const ROUTE_CATALOG = "/dsh-plus/subagent-model/catalog";
|
|
165
|
+
const MAX_BODY_BYTES = 256 * 1024;
|
|
166
|
+
function sendJson(res, status, body) {
|
|
167
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
168
|
+
res.end(JSON.stringify(body));
|
|
169
|
+
}
|
|
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
|
+
async function handleCatalog(ctx, res) {
|
|
229
|
+
sendJson(res, 200, await buildModelCatalog(ctx));
|
|
230
|
+
}
|
|
231
|
+
/** 注册配置读写与目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
|
|
232
|
+
function registerConfigApi(ctx, service) {
|
|
233
|
+
const logger = ctx.logger("subagent-model");
|
|
234
|
+
const guard = (handler) => {
|
|
235
|
+
return async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
await handler(req, res);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
logger.warn(`config api ${req.method ?? "?"} ${req.url ?? "?"} failed: ${message}`);
|
|
241
|
+
if (!res.headersSent) sendJson(res, 400, { error: message });
|
|
242
|
+
else res.end();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
ctx.webServer.register({
|
|
247
|
+
kind: "exact",
|
|
248
|
+
path: ROUTE_CONFIG,
|
|
249
|
+
handler: guard((req, res) => handleConfig(ctx, service, req, res))
|
|
250
|
+
});
|
|
251
|
+
ctx.webServer.register({
|
|
252
|
+
kind: "exact",
|
|
253
|
+
path: ROUTE_CATALOG,
|
|
254
|
+
handler: guard(async (req, res) => {
|
|
255
|
+
if (req.method !== "GET") {
|
|
256
|
+
sendJson(res, 405, { error: "method not allowed" });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await handleCatalog(ctx, res);
|
|
260
|
+
})
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/delegation.ts
|
|
266
|
+
const WRAPPED = Symbol("dsh-plus-subagent-model.wrapped");
|
|
267
|
+
function entryFor(current, name$1) {
|
|
268
|
+
if (name$1 === void 0) return void 0;
|
|
269
|
+
const entry = current().entries[name$1];
|
|
270
|
+
return entry === void 0 ? void 0 : resolveEntry(entry);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 包装 `ctx.subagents.start` / `startContinuable`:按 provider 名命中配置条目,
|
|
274
|
+
* 向委托请求注入 agentOptions(显式工具行配置优先,插件只补空缺)。
|
|
275
|
+
* 未命中条目时完全直通(原生继承主代理行为)。
|
|
276
|
+
* 返回注销函数(恢复原方法),随调用方 fiber 释放。
|
|
277
|
+
*/
|
|
278
|
+
function installDelegationHook(ctx, current) {
|
|
279
|
+
const subagents = ctx.get("subagents");
|
|
280
|
+
if (subagents === void 0) {
|
|
281
|
+
ctx.logger("subagent-model").warn("subagents 服务不可用,跳过委托挂钩");
|
|
282
|
+
return () => {};
|
|
283
|
+
}
|
|
284
|
+
const marked = subagents;
|
|
285
|
+
if (marked[WRAPPED]) {
|
|
286
|
+
ctx.logger("subagent-model").warn("subagents 服务已被本插件包装,跳过重复挂钩");
|
|
287
|
+
return () => {};
|
|
288
|
+
}
|
|
289
|
+
const originalStart = subagents.start.bind(subagents);
|
|
290
|
+
const originalContinuable = subagents.startContinuable.bind(subagents);
|
|
291
|
+
subagents.start = async (name$1, request) => {
|
|
292
|
+
const injected = entryFor(current, name$1);
|
|
293
|
+
if (injected === void 0) return originalStart(name$1, request);
|
|
294
|
+
return originalStart(name$1, {
|
|
295
|
+
...request,
|
|
296
|
+
agentOptions: mergeAgentOptions(injected, request.agentOptions)
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
subagents.startContinuable = async (spec) => {
|
|
300
|
+
const injected = entryFor(current, spec.provider);
|
|
301
|
+
if (injected === void 0) return originalContinuable(spec);
|
|
302
|
+
return originalContinuable({
|
|
303
|
+
...spec,
|
|
304
|
+
request: {
|
|
305
|
+
...spec.request,
|
|
306
|
+
agentOptions: mergeAgentOptions(injected, spec.request.agentOptions)
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
marked[WRAPPED] = true;
|
|
311
|
+
return ctx.effect(() => () => {
|
|
312
|
+
subagents.start = originalStart;
|
|
313
|
+
subagents.startContinuable = originalContinuable;
|
|
314
|
+
delete marked[WRAPPED];
|
|
315
|
+
}, "subagent-model: delegation hook");
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 读取一次 LLM 请求的发起 agent 上本插件携带的 effort 私有标记:
|
|
319
|
+
* 仅对子代理(session header origin==='subagent')生效,主代理不受影响。
|
|
320
|
+
* 纯函数(agent 为最小结构面),便于单测。
|
|
321
|
+
*/
|
|
322
|
+
function childEffortOf(agent) {
|
|
323
|
+
if (agent.session.header.origin !== "subagent") return void 0;
|
|
324
|
+
return agent.options.reasoningEffort;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* 根 ctx 监听 `agent/request` 瀑布:对子代理应用条目配置的思考程度。
|
|
328
|
+
* provider/model 无需在此处理(路由直接读 options)。
|
|
329
|
+
* 根级监听覆盖 HMR 重载后已存在子代理的后续请求。
|
|
330
|
+
*/
|
|
331
|
+
function installChildRoute(ctx) {
|
|
332
|
+
ctx.on("agent/request", async (payload, next) => {
|
|
333
|
+
const resolved = await next();
|
|
334
|
+
const effort = childEffortOf(payload.agent);
|
|
335
|
+
if (effort === void 0) return resolved;
|
|
336
|
+
return applyEffort(resolved, effort);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/service.ts
|
|
342
|
+
var SubagentModelService = class extends Service {
|
|
343
|
+
static [Context.inject] = ["subagents", "llm"];
|
|
344
|
+
current;
|
|
345
|
+
constructor(ctx, config) {
|
|
346
|
+
super(ctx, "subagentModel");
|
|
347
|
+
this.current = () => config;
|
|
348
|
+
installSettingsSection(ctx, SETTINGS_NS, Config, config, {
|
|
349
|
+
setSource: (source) => {
|
|
350
|
+
this.current = source;
|
|
351
|
+
},
|
|
352
|
+
onChange: () => {}
|
|
353
|
+
});
|
|
354
|
+
installDelegationHook(ctx, () => this.current());
|
|
355
|
+
installChildRoute(ctx);
|
|
356
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
357
|
+
registerConfigApi(webCtx, this);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */
|
|
361
|
+
currentConfig() {
|
|
362
|
+
return this.current();
|
|
363
|
+
}
|
|
364
|
+
/** 已注册的子代理 provider 名(配置卡片据此合成行)。 */
|
|
365
|
+
subagentProviders() {
|
|
366
|
+
const subagents = this.ctx.get("subagents");
|
|
367
|
+
return subagents === void 0 ? [] : [...subagents.list()];
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/index.ts
|
|
373
|
+
const name = "dsh-plus-subagent-model";
|
|
374
|
+
const inject = ["subagents", "llm"];
|
|
375
|
+
function apply(ctx, config) {
|
|
376
|
+
ctx.plugin(SubagentModelService, config);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
//#endregion
|
|
380
|
+
export { Config, SubagentModelService, apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-plus/subagent-model",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh-plus service+ui plugin: 为 subagent/subagent_fork 等子代理按提供商配置模型与思考程度(或继承主代理)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"lib",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"dsh": {
|
|
24
|
+
"client": {
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
27
|
+
"@deepseek-ai/dsh-client-locale"
|
|
28
|
+
],
|
|
29
|
+
"platform": "web"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
34
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.6",
|
|
35
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
39
|
+
"@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",
|
|
45
|
+
"@types/react": "~18.3.1",
|
|
46
|
+
"react": "^18.2.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/A-G-guy/dsh-plugins.git",
|
|
55
|
+
"directory": "packages/subagent-model"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"watch": "tsdown --watch"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提供商/模型/思考档位目录:配置卡片的下拉数据源。
|
|
3
|
+
* 镜像官方 api-proxy `buildModelCatalog`(session.models RPC)的构建方式:
|
|
4
|
+
* `ctx.llm.listProviders()` → `listModels()` → `resolveModelInfo()`,
|
|
5
|
+
* 附 `ctx.subagents.list()` 的已注册子代理 provider 名(卡片按此行合成空行)。
|
|
6
|
+
* 目录仅作建议性数据源,不参与运行期路由。
|
|
7
|
+
* @module subagent-model/catalog
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
import type { LlmReasoningEffortInfo } from '@deepseek-ai/dsh-llm'
|
|
11
|
+
|
|
12
|
+
export interface CatalogEffort {
|
|
13
|
+
id: string
|
|
14
|
+
name: string
|
|
15
|
+
description?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CatalogModel {
|
|
19
|
+
id: string
|
|
20
|
+
name: string
|
|
21
|
+
description?: string
|
|
22
|
+
reasoning?: {
|
|
23
|
+
defaultEffort?: string
|
|
24
|
+
efforts: CatalogEffort[]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CatalogProvider {
|
|
29
|
+
id: string
|
|
30
|
+
name: string
|
|
31
|
+
models: CatalogModel[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CatalogFailure {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
message: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ModelCatalog {
|
|
41
|
+
providers: CatalogProvider[]
|
|
42
|
+
failures: CatalogFailure[]
|
|
43
|
+
/** 已注册的子代理 provider 名(spawn/fork/…),卡片据此合成配置行。 */
|
|
44
|
+
subagentProviders: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toEffort(effort: LlmReasoningEffortInfo): CatalogEffort {
|
|
48
|
+
return {
|
|
49
|
+
id: String(effort.id),
|
|
50
|
+
name: effort.name,
|
|
51
|
+
...effort.description === undefined ? {} : { description: effort.description },
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 构建目录。单个提供商失败只进入 failures,不拖垮其余组;
|
|
57
|
+
* 无模型的组被丢弃(与官方 buildModelCatalog 同语义)。
|
|
58
|
+
*/
|
|
59
|
+
export async function buildModelCatalog(ctx: Context): Promise<ModelCatalog> {
|
|
60
|
+
const llm = ctx.get('llm')
|
|
61
|
+
if (llm === undefined) {
|
|
62
|
+
return { providers: [], failures: [], subagentProviders: subagentProvidersOf(ctx) }
|
|
63
|
+
}
|
|
64
|
+
const catalog = await Promise.all(llm.listProviders().map(async (provider) => {
|
|
65
|
+
try {
|
|
66
|
+
const models = await llm.listModels(provider.id)
|
|
67
|
+
const entries = await Promise.all(models.map(async (model) => {
|
|
68
|
+
const resolved = await llm.resolveModelInfo(provider.id, model.id)
|
|
69
|
+
const reasoning = resolved.reasoning === undefined ? undefined : {
|
|
70
|
+
efforts: resolved.reasoning.efforts.map(toEffort),
|
|
71
|
+
...resolved.reasoning.defaultEffort === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { defaultEffort: String(resolved.reasoning.defaultEffort) },
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: model.id,
|
|
77
|
+
name: model.name,
|
|
78
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
79
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
80
|
+
}
|
|
81
|
+
}))
|
|
82
|
+
return {
|
|
83
|
+
kind: 'group' as const,
|
|
84
|
+
group: { id: provider.id, name: provider.name, models: entries },
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'failure' as const,
|
|
89
|
+
failure: {
|
|
90
|
+
id: provider.id,
|
|
91
|
+
name: provider.name,
|
|
92
|
+
message: error instanceof Error ? error.message : String(error),
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
return {
|
|
98
|
+
providers: catalog
|
|
99
|
+
.filter((item) => item.kind === 'group' && item.group.models.length > 0)
|
|
100
|
+
.map((item) => item.kind === 'group' ? item.group : undefined)
|
|
101
|
+
.filter((group): group is CatalogProvider => group !== undefined),
|
|
102
|
+
failures: catalog
|
|
103
|
+
.filter((item) => item.kind === 'failure')
|
|
104
|
+
.map((item) => item.kind === 'failure' ? item.failure : undefined)
|
|
105
|
+
.filter((failure): failure is CatalogFailure => failure !== undefined),
|
|
106
|
+
subagentProviders: subagentProvidersOf(ctx),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 已注册的子代理 provider 名(llm 服务缺失时同样返回,卡片仍可渲染)。 */
|
|
111
|
+
function subagentProvidersOf(ctx: Context): string[] {
|
|
112
|
+
const subagents = ctx.get('subagents')
|
|
113
|
+
return subagents === undefined ? [] : [...subagents.list()]
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片数据通道:同源 fetch 调自建 webServer 路由。
|
|
3
|
+
* (官方 settings.* RPC 白名单硬编码不含第三方 namespace,见 config-api.ts 注释。)
|
|
4
|
+
* @module subagent-model/client/api
|
|
5
|
+
*/
|
|
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
|
+
export interface CatalogEffort {
|
|
27
|
+
id: string
|
|
28
|
+
name: string
|
|
29
|
+
description?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CatalogModel {
|
|
33
|
+
id: string
|
|
34
|
+
name: string
|
|
35
|
+
description?: string
|
|
36
|
+
reasoning?: {
|
|
37
|
+
defaultEffort?: string
|
|
38
|
+
efforts: CatalogEffort[]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CatalogProvider {
|
|
43
|
+
id: string
|
|
44
|
+
name: string
|
|
45
|
+
models: CatalogModel[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ModelCatalog {
|
|
49
|
+
providers: CatalogProvider[]
|
|
50
|
+
failures: { id: string; name: string; message: string }[]
|
|
51
|
+
subagentProviders: string[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ROUTE_CONFIG = '/dsh-plus/subagent-model/config'
|
|
55
|
+
const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
|
|
56
|
+
|
|
57
|
+
async function parse<T>(res: Response): Promise<T> {
|
|
58
|
+
const body = (await res.json()) as T & { error?: string }
|
|
59
|
+
if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`)
|
|
60
|
+
return body
|
|
61
|
+
}
|
|
62
|
+
|
|
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
|
+
export async function fetchCatalog(): Promise<ModelCatalog> {
|
|
79
|
+
return parse<ModelCatalog>(await fetch(ROUTE_CATALOG, { credentials: 'same-origin' }))
|
|
80
|
+
}
|