@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
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置 HTTP 通道:webui 插件配置卡片的后端。
|
|
3
|
+
* 背景:官方 apiproxy 的 settings.* RPC 对 namespace 有硬编码白名单,第三方
|
|
4
|
+
* namespace 一律 settings-not-exposed;故卡片数据走自建 webServer 路由,
|
|
5
|
+
* 读写仍经 ctx.settings 用户层(持久化 $DSH_HOME/settings.yaml)。
|
|
6
|
+
* 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
|
|
7
|
+
* @module subagent-model/config-api
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
10
|
+
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
|
|
13
|
+
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
|
+
|
|
19
|
+
const ROUTE_CONFIG = '/dsh-plus/subagent-model/config'
|
|
20
|
+
const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
|
|
21
|
+
const MAX_BODY_BYTES = 256 * 1024
|
|
22
|
+
|
|
23
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
24
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
25
|
+
res.end(JSON.stringify(body))
|
|
26
|
+
}
|
|
27
|
+
|
|
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
|
+
async function handleCatalog(ctx: Context, res: ServerResponse): Promise<void> {
|
|
96
|
+
const catalog: ModelCatalog = await buildModelCatalog(ctx)
|
|
97
|
+
sendJson(res, 200, catalog)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 注册配置读写与目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
|
|
101
|
+
export function registerConfigApi(ctx: Context, service: SubagentModelService): void {
|
|
102
|
+
const logger = ctx.logger('subagent-model')
|
|
103
|
+
const guard = (
|
|
104
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>,
|
|
105
|
+
): ((req: IncomingMessage, res: ServerResponse) => Promise<void>) => {
|
|
106
|
+
return async (req, res) => {
|
|
107
|
+
try {
|
|
108
|
+
await handler(req, res)
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
111
|
+
logger.warn(`config api ${req.method ?? '?'} ${req.url ?? '?'} failed: ${message}`)
|
|
112
|
+
if (!res.headersSent) sendJson(res, 400, { error: message })
|
|
113
|
+
else res.end()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
ctx.webServer.register({
|
|
118
|
+
kind: 'exact',
|
|
119
|
+
path: ROUTE_CONFIG,
|
|
120
|
+
handler: guard((req, res) => handleConfig(ctx, service, req, res)),
|
|
121
|
+
})
|
|
122
|
+
ctx.webServer.register({
|
|
123
|
+
kind: 'exact',
|
|
124
|
+
path: ROUTE_CATALOG,
|
|
125
|
+
handler: guard(async (req, res) => {
|
|
126
|
+
if (req.method !== 'GET') {
|
|
127
|
+
sendJson(res, 405, { error: 'method not allowed' })
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
await handleCatalog(ctx, res)
|
|
131
|
+
}),
|
|
132
|
+
})
|
|
133
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置单一事实源:cordis 行级 Config(组合默认值,dev/prod patch 层可覆盖)
|
|
3
|
+
* 与 settings namespace(用户层,经 dsh-settings-file 持久化到 $DSH_HOME/settings.yaml)
|
|
4
|
+
* 共用同一 schemastery schema。条目按【子代理 provider 名】键控
|
|
5
|
+
* (standard preset 事实映射:subagent↔spawn、subagent_fork↔fork)。
|
|
6
|
+
*
|
|
7
|
+
* 运行时语义(详见 delegation.ts):
|
|
8
|
+
* - provider/model 显式时注入子代理 `agentOptions`(覆盖"继承主代理"快照);
|
|
9
|
+
* - reasoningEffort 显式时经私有字段随 agentOptions 抵达子代理,再由
|
|
10
|
+
* `agent/request` 瀑布监听器应用(dsh 原生 AgentOptions 不含 effort 字段)。
|
|
11
|
+
*
|
|
12
|
+
* 类型说明:z.dict 的推断类型引用 cosmokit 的 Dict,直接导出会触发
|
|
13
|
+
* dts 的 TS2742(不可具名引用);按官方 dsh-permission-presets 模式,
|
|
14
|
+
* 显式声明输入/输出接口并注解 schema(输出为规范型,输入为可空宽松型)。
|
|
15
|
+
* @module subagent-model/config
|
|
16
|
+
*/
|
|
17
|
+
import z from '@deepseek-ai/schemastery'
|
|
18
|
+
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
19
|
+
|
|
20
|
+
/** settings 命名空间;webui 配置卡片与插件运行期读取同一份。 */
|
|
21
|
+
export const SETTINGS_NS = settingsNamespace('dsh-plus-subagent-model')
|
|
22
|
+
|
|
23
|
+
/** 思考程度哨兵值:继承主代理(不注入 effort 字段)。 */
|
|
24
|
+
export const EFFORT_INHERIT = 'inherit'
|
|
25
|
+
/** 思考程度哨兵值:跟随模型默认(显式剥离任何继承/推导出的 effort)。 */
|
|
26
|
+
export const EFFORT_DEFAULT = 'default'
|
|
27
|
+
|
|
28
|
+
/** 一条条目的规范输出形态。 */
|
|
29
|
+
export interface EntryConfig {
|
|
30
|
+
enabled: boolean
|
|
31
|
+
provider: string
|
|
32
|
+
model: string
|
|
33
|
+
reasoningEffort: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 配置的规范输出形态。 */
|
|
37
|
+
export interface SubagentModelConfig {
|
|
38
|
+
enabled: boolean
|
|
39
|
+
entries: Record<string, EntryConfig>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 配置的宽松输入形态(schema 校验前的用户数据)。 */
|
|
43
|
+
export interface SubagentModelConfigInput {
|
|
44
|
+
enabled?: boolean | null
|
|
45
|
+
entries?: Record<string, {
|
|
46
|
+
enabled?: boolean | null
|
|
47
|
+
provider?: string | null
|
|
48
|
+
model?: string | null
|
|
49
|
+
reasoningEffort?: string | null
|
|
50
|
+
}> | null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const EntrySchema = z.object({
|
|
54
|
+
enabled: z.boolean().description('该子代理 provider 行的开关').default(false),
|
|
55
|
+
provider: z.string().description('LLM 提供商 id;留空 = 继承主代理').default(''),
|
|
56
|
+
model: z.string().description('模型 id;留空 = 继承主代理').default(''),
|
|
57
|
+
reasoningEffort: z.string()
|
|
58
|
+
.description(`思考程度:${EFFORT_INHERIT}(继承主代理)/ ${EFFORT_DEFAULT}(跟随模型默认)/ 提供商目录档位 id`)
|
|
59
|
+
.default(EFFORT_INHERIT),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export const Config: z<SubagentModelConfigInput, SubagentModelConfig> = z.object({
|
|
63
|
+
enabled: z.boolean().description('总开关').default(false),
|
|
64
|
+
entries: z.dict(EntrySchema).description('按子代理 provider 名(spawn/fork/…)配置').default({}),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** 注入子代理 AgentOptions 的结果对象:只含需要覆盖的字段。 */
|
|
68
|
+
export interface InjectedOptions {
|
|
69
|
+
provider?: string
|
|
70
|
+
model?: string
|
|
71
|
+
/** 私有标记:随 agentOptions 抵达子代理 options,由 agent/request 监听器消费。 */
|
|
72
|
+
reasoningEffort?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 解析一条条目为要注入的 agentOptions 片段。
|
|
77
|
+
* 未启用或全继承时返回 undefined(完全保持 dsh 原生行为)。
|
|
78
|
+
*/
|
|
79
|
+
export function resolveEntry(entry: EntryConfig): InjectedOptions | undefined {
|
|
80
|
+
if (!entry.enabled) return undefined
|
|
81
|
+
const out: InjectedOptions = {}
|
|
82
|
+
if (entry.provider.length > 0) out.provider = entry.provider
|
|
83
|
+
if (entry.model.length > 0) out.model = entry.model
|
|
84
|
+
if (entry.reasoningEffort !== EFFORT_INHERIT) out.reasoningEffort = entry.reasoningEffort
|
|
85
|
+
return out.provider !== undefined || out.model !== undefined || out.reasoningEffort !== undefined
|
|
86
|
+
? out
|
|
87
|
+
: undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 合并注入片段与工具行已有的 agentOptions:部署级显式配置优先,
|
|
92
|
+
* 插件只补空缺(standard preset 的工具行无 agentOptions,插件即生效)。
|
|
93
|
+
*/
|
|
94
|
+
export function mergeAgentOptions(
|
|
95
|
+
injected: InjectedOptions,
|
|
96
|
+
existing: InjectedOptions | undefined,
|
|
97
|
+
): InjectedOptions {
|
|
98
|
+
return { ...injected, ...existing }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 请求配置的形状(与 dsh agent/request 瀑布产物的相关字段)。 */
|
|
102
|
+
export interface RequestConfig {
|
|
103
|
+
provider: string
|
|
104
|
+
model: string
|
|
105
|
+
reasoningEffort?: string
|
|
106
|
+
maxTokens?: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 把条目的 effort 决策应用到一次 LLM 请求配置上(纯函数):
|
|
111
|
+
* - undefined:直通(未配置 effort,保持原生推导);
|
|
112
|
+
* - EFFORT_DEFAULT:剥离 reasoningEffort(跟随模型默认,覆盖 fork 的 seed 继承);
|
|
113
|
+
* - 其余:显式覆盖。
|
|
114
|
+
*/
|
|
115
|
+
export function applyEffort<T extends RequestConfig>(resolved: T, effort: string | undefined): T {
|
|
116
|
+
if (effort === undefined) return resolved
|
|
117
|
+
if (effort === EFFORT_DEFAULT) {
|
|
118
|
+
const { reasoningEffort: _dropped, ...rest } = resolved
|
|
119
|
+
return rest as T
|
|
120
|
+
}
|
|
121
|
+
return { ...resolved, reasoningEffort: effort } as T
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 条目校验的输入面(卡片提交的可能为宽松形态)。 */
|
|
125
|
+
export interface EntryValidationInput {
|
|
126
|
+
provider?: string | null
|
|
127
|
+
model?: string | null
|
|
128
|
+
reasoningEffort?: string | null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 校验一条用户提交的条目;返回错误消息,合法时返回 null。 */
|
|
132
|
+
export function validateEntry(entry: EntryValidationInput): string | null {
|
|
133
|
+
const model = entry.model ?? ''
|
|
134
|
+
const provider = entry.provider ?? ''
|
|
135
|
+
const effort = entry.reasoningEffort ?? ''
|
|
136
|
+
if (model.length > 0 && provider.length === 0) {
|
|
137
|
+
return 'model 不能脱离 provider 单独配置(请先选择提供商或改回继承)'
|
|
138
|
+
}
|
|
139
|
+
if (effort.length === 0) {
|
|
140
|
+
return 'reasoningEffort 不能为空(inherit / default / 档位 id)'
|
|
141
|
+
}
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
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
|
+
}
|
|
170
|
+
}
|
|
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 }
|
|
199
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 运行时挂钩:子代理模型路由的注入点。
|
|
3
|
+
*
|
|
4
|
+
* 机制依据(官方 dsh-subagent / dsh-agent-loop):
|
|
5
|
+
* - 子代理的模型由【创建时】的 AgentOptions 决定:`resolveChildAgentOptions(parent,
|
|
6
|
+
* request.agentOptions, depth)` = 父 options 快照 + 显式 agentOptions(后者胜出)。
|
|
7
|
+
* 子代理不经 api-proxy 的 create/resume setup,不安装模型 selection,
|
|
8
|
+
* `agent/request` 瀑布默认无人覆盖 —— 因此 provider/model 只需注入
|
|
9
|
+
* `request.agentOptions`,路由即命中。
|
|
10
|
+
* - reasoningEffort 不在 AgentOptions 读取路径上(AgentLoop 只读 provider/model/maxTokens,
|
|
11
|
+
* 子代理 buildRequest 的 effort 仅来自自身 session 折叠的 request/header)。
|
|
12
|
+
* 故 effort 以私有字段随 agentOptions 抵达子代理 options,再由根 ctx 的
|
|
13
|
+
* `agent/request` 瀑布监听器应用(origin==='subagent' 过滤)。
|
|
14
|
+
* - 冷恢复(continuable resume):descriptor 持久化 agentProvider/agentModel
|
|
15
|
+
* (来自 request.agentOptions),恢复后模型保持;effort 经 session log 的
|
|
16
|
+
* request/header 保留 —— 与热路径注入结果一致。
|
|
17
|
+
*
|
|
18
|
+
* 两个挂钩均幂等(Symbol 标记 + dispose 恢复),HMR 重载安全。
|
|
19
|
+
* @module subagent-model/delegation
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
22
|
+
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
|
|
23
|
+
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
|
24
|
+
import type { ContinuableStartSpec, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
|
|
25
|
+
|
|
26
|
+
import { applyEffort, mergeAgentOptions, resolveEntry, type RequestConfig, type SubagentModelConfig } from './config.ts'
|
|
27
|
+
|
|
28
|
+
const WRAPPED = Symbol('dsh-plus-subagent-model.wrapped')
|
|
29
|
+
|
|
30
|
+
/** 子代理服务的最小运行期面(仅包装用到的两个入口)。 */
|
|
31
|
+
interface SubagentsLike {
|
|
32
|
+
start(name: string, request: SubagentStartRequest): Promise<unknown>
|
|
33
|
+
startContinuable(spec: ContinuableStartSpec): Promise<unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function entryFor(current: () => SubagentModelConfig, name: string | undefined) {
|
|
37
|
+
if (name === undefined) return undefined
|
|
38
|
+
const entry = current().entries[name]
|
|
39
|
+
return entry === undefined ? undefined : resolveEntry(entry)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 包装 `ctx.subagents.start` / `startContinuable`:按 provider 名命中配置条目,
|
|
44
|
+
* 向委托请求注入 agentOptions(显式工具行配置优先,插件只补空缺)。
|
|
45
|
+
* 未命中条目时完全直通(原生继承主代理行为)。
|
|
46
|
+
* 返回注销函数(恢复原方法),随调用方 fiber 释放。
|
|
47
|
+
*/
|
|
48
|
+
export function installDelegationHook(
|
|
49
|
+
ctx: Context,
|
|
50
|
+
current: () => SubagentModelConfig,
|
|
51
|
+
): () => void {
|
|
52
|
+
const subagents = ctx.get('subagents') as SubagentsLike | undefined
|
|
53
|
+
if (subagents === undefined) {
|
|
54
|
+
ctx.logger('subagent-model').warn('subagents 服务不可用,跳过委托挂钩')
|
|
55
|
+
return () => {}
|
|
56
|
+
}
|
|
57
|
+
const marked = subagents as SubagentsLike & { [WRAPPED]?: boolean }
|
|
58
|
+
if (marked[WRAPPED]) {
|
|
59
|
+
ctx.logger('subagent-model').warn('subagents 服务已被本插件包装,跳过重复挂钩')
|
|
60
|
+
return () => {}
|
|
61
|
+
}
|
|
62
|
+
const originalStart = subagents.start.bind(subagents)
|
|
63
|
+
const originalContinuable = subagents.startContinuable.bind(subagents)
|
|
64
|
+
subagents.start = async (name, request) => {
|
|
65
|
+
const injected = entryFor(current, name)
|
|
66
|
+
if (injected === undefined) return originalStart(name, request)
|
|
67
|
+
return originalStart(name, {
|
|
68
|
+
...request,
|
|
69
|
+
agentOptions: mergeAgentOptions(injected, request.agentOptions),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
subagents.startContinuable = async (spec) => {
|
|
73
|
+
const injected = entryFor(current, spec.provider)
|
|
74
|
+
if (injected === undefined) return originalContinuable(spec)
|
|
75
|
+
return originalContinuable({
|
|
76
|
+
...spec,
|
|
77
|
+
request: {
|
|
78
|
+
...spec.request,
|
|
79
|
+
agentOptions: mergeAgentOptions(injected, spec.request.agentOptions),
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
marked[WRAPPED] = true
|
|
84
|
+
return ctx.effect(
|
|
85
|
+
() => () => {
|
|
86
|
+
subagents.start = originalStart
|
|
87
|
+
subagents.startContinuable = originalContinuable
|
|
88
|
+
delete marked[WRAPPED]
|
|
89
|
+
},
|
|
90
|
+
'subagent-model: delegation hook',
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 读取一次 LLM 请求的发起 agent 上本插件携带的 effort 私有标记:
|
|
96
|
+
* 仅对子代理(session header origin==='subagent')生效,主代理不受影响。
|
|
97
|
+
* 纯函数(agent 为最小结构面),便于单测。
|
|
98
|
+
*/
|
|
99
|
+
export function childEffortOf(agent: {
|
|
100
|
+
session: { header: { origin?: 'subagent' } }
|
|
101
|
+
options: AgentOptions & { reasoningEffort?: string }
|
|
102
|
+
}): string | undefined {
|
|
103
|
+
if (agent.session.header.origin !== 'subagent') return undefined
|
|
104
|
+
return agent.options.reasoningEffort
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 根 ctx 监听 `agent/request` 瀑布:对子代理应用条目配置的思考程度。
|
|
109
|
+
* provider/model 无需在此处理(路由直接读 options)。
|
|
110
|
+
* 根级监听覆盖 HMR 重载后已存在子代理的后续请求。
|
|
111
|
+
*/
|
|
112
|
+
export function installChildRoute(ctx: Context): void {
|
|
113
|
+
ctx.on('agent/request', async (payload, next) => {
|
|
114
|
+
const resolved = await next()
|
|
115
|
+
const effort = childEffortOf(payload.agent)
|
|
116
|
+
if (effort === undefined) return resolved
|
|
117
|
+
return applyEffort(resolved as RequestConfig, effort) as LlmCallConfig
|
|
118
|
+
})
|
|
119
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 插件:子代理独立模型配置(subagent / subagent_fork 等)。
|
|
3
|
+
* 为子代理按 provider 名配置 LLM 提供商、模型与思考程度(reasoningEffort),
|
|
4
|
+
* 或选择从主代理继承;只负责模型选定,其余参数(maxTokens/persona/toolFilter/
|
|
5
|
+
* maxDepth 等)保持 dsh 原生行为。配置 UI 位于 webui 设置-插件-插件配置,
|
|
6
|
+
* 持久化到 $DSH_HOME/settings.yaml 并热生效。
|
|
7
|
+
* @module @dsh-plus/subagent-model
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
|
|
11
|
+
import { Config, type SubagentModelConfig } from './config.ts'
|
|
12
|
+
import { SubagentModelService } from './service.ts'
|
|
13
|
+
|
|
14
|
+
export const name = 'dsh-plus-subagent-model'
|
|
15
|
+
|
|
16
|
+
export const inject = ['subagents', 'llm'] as const
|
|
17
|
+
|
|
18
|
+
export { Config }
|
|
19
|
+
|
|
20
|
+
export type { EntryConfig, InjectedOptions, SubagentModelConfig } from './config.ts'
|
|
21
|
+
export { SubagentModelService } from './service.ts'
|
|
22
|
+
|
|
23
|
+
export function apply(ctx: Context, config: SubagentModelConfig): void {
|
|
24
|
+
ctx.plugin(SubagentModelService, config)
|
|
25
|
+
}
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubagentModelService:子代理模型配置中枢。
|
|
3
|
+
* - 配置:installSettingsSection 接入 settings 用户层($DSH_HOME/settings.yaml,热生效),
|
|
4
|
+
* 无 settings provider 时退化为 cordis 行级 config。
|
|
5
|
+
* - 委托挂钩:包装 ctx.subagents.start/startContinuable,按 provider 名注入
|
|
6
|
+
* agentOptions(provider/model/effort 私有标记),未命中条目完全直通。
|
|
7
|
+
* - 子代理路由:根 ctx 的 agent/request 瀑布监听,把条目 effort 应用到子代理请求。
|
|
8
|
+
* - 配置通道:ctx.webServer 注册 config/catalog 同源路由(官方 settings RPC
|
|
9
|
+
* 白名单不含第三方 namespace)。
|
|
10
|
+
* @module @dsh-plus/subagent-model
|
|
11
|
+
*/
|
|
12
|
+
import { Context, Service } from '@deepseek-ai/cordis'
|
|
13
|
+
import { installSettingsSection } from '@deepseek-ai/dsh-settings'
|
|
14
|
+
|
|
15
|
+
import { registerConfigApi } from './config-api.ts'
|
|
16
|
+
import { Config, SETTINGS_NS, type SubagentModelConfig } from './config.ts'
|
|
17
|
+
import { installChildRoute, installDelegationHook } from './delegation.ts'
|
|
18
|
+
|
|
19
|
+
declare module '@deepseek-ai/cordis' {
|
|
20
|
+
interface Context {
|
|
21
|
+
subagentModel: SubagentModelService
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SubagentModelService extends Service {
|
|
26
|
+
static [Context.inject] = ['subagents', 'llm']
|
|
27
|
+
|
|
28
|
+
private current: () => SubagentModelConfig
|
|
29
|
+
|
|
30
|
+
constructor(ctx: Context, config: SubagentModelConfig) {
|
|
31
|
+
super(ctx, 'subagentModel')
|
|
32
|
+
this.current = () => config
|
|
33
|
+
installSettingsSection(ctx, SETTINGS_NS, Config, config, {
|
|
34
|
+
setSource: (source) => {
|
|
35
|
+
this.current = source
|
|
36
|
+
},
|
|
37
|
+
onChange: () => {},
|
|
38
|
+
})
|
|
39
|
+
installDelegationHook(ctx, () => this.current())
|
|
40
|
+
installChildRoute(ctx)
|
|
41
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
42
|
+
registerConfigApi(webCtx, this)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */
|
|
47
|
+
currentConfig(): SubagentModelConfig {
|
|
48
|
+
return this.current()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 已注册的子代理 provider 名(配置卡片据此合成行)。 */
|
|
52
|
+
subagentProviders(): string[] {
|
|
53
|
+
const subagents = this.ctx.get('subagents')
|
|
54
|
+
return subagents === undefined ? [] : [...subagents.list()]
|
|
55
|
+
}
|
|
56
|
+
}
|