@dsh-plus/subagent-model 0.2.0 → 0.3.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 配置卡片的可编辑草稿模型:ConfigValue ↔ Draft 转换与提交补丁(纯函数,
3
+ * 与渲染分离以便单测)。行集合 = 目录返回的已注册子代理 provider ∪ 已配置条目;
4
+ * 未配置的新 provider 行以默认空值落盘(自文档化)。
5
+ * @module subagent-model/client/draft
6
+ */
7
+ import type { ModelCatalog } from './api.ts'
8
+
9
+ /** settings 命名空间的解析值(无 secret 字段)。 */
10
+ export interface ConfigValue {
11
+ enabled: boolean
12
+ entries: Record<string, WireEntry>
13
+ }
14
+
15
+ export interface WireEntry {
16
+ enabled: boolean
17
+ provider: string
18
+ model: string
19
+ reasoningEffort: string
20
+ }
21
+
22
+ export interface DraftRow {
23
+ enabled: boolean
24
+ provider: string
25
+ model: string
26
+ reasoningEffort: string
27
+ }
28
+
29
+ export interface Draft {
30
+ enabled: boolean
31
+ rows: Record<string, DraftRow>
32
+ }
33
+
34
+ export const EMPTY_ROW: DraftRow = {
35
+ enabled: false,
36
+ provider: '',
37
+ model: '',
38
+ reasoningEffort: 'inherit',
39
+ }
40
+
41
+ /** 行集 = 目录 subagentProviders ∪ 已配置条目;目录行缺省为空行。 */
42
+ export function draftFrom(value: ConfigValue, catalog: ModelCatalog | null): Draft {
43
+ const rows: Record<string, DraftRow> = {}
44
+ const names = new Set<string>()
45
+ for (const name of catalog?.subagentProviders ?? []) names.add(name)
46
+ for (const name of Object.keys(value.entries).sort()) names.add(name)
47
+ for (const name of names) {
48
+ const entry = value.entries[name]
49
+ rows[name] =
50
+ entry === undefined
51
+ ? { ...EMPTY_ROW }
52
+ : {
53
+ enabled: entry.enabled,
54
+ provider: entry.provider,
55
+ model: entry.model,
56
+ reasoningEffort: entry.reasoningEffort,
57
+ }
58
+ }
59
+ return { enabled: value.enabled, rows }
60
+ }
61
+
62
+ export function toPatch(draft: Draft): Record<string, unknown> {
63
+ // entries 按键排序物化:dirty 比较走 JSON.stringify,而草稿的播种路径
64
+ // (先 entries 后补目录空行)与 draftFrom(先目录后 entries)键序不同,
65
+ // 不排序会出现「未改动却永久 dirty」的假阳性。
66
+ const entries: Record<string, DraftRow> = {}
67
+ for (const name of Object.keys(draft.rows).sort()) entries[name] = draft.rows[name]
68
+ return { enabled: draft.enabled, entries }
69
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
3
+ * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
4
+ * @module subagent-model/client/i18n
5
+ */
6
+ import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
7
+
8
+ export const NS = 'dsh-plus-subagent-model'
9
+
10
+ const ownZh = {
11
+ title: '子代理模型配置',
12
+ description: '为 subagent / subagent_fork 等子代理单独指定提供商、模型与思考程度,或继承主代理。',
13
+ enabled: '启用插件',
14
+ rowEnabled: '启用该行',
15
+ provider: '提供商',
16
+ model: '模型',
17
+ effort: '思考程度',
18
+ inheritProvider: '继承主代理',
19
+ inheritModel: '继承主代理',
20
+ effortInherit: '继承主代理',
21
+ effortDefault: '跟随模型默认',
22
+ providerHint: '留空 = 子代理沿用主代理的提供商与模型。',
23
+ modelHint: '留空 = 沿用主代理的模型(需与所选提供商匹配)。',
24
+ effortHint: 'inherit = 不干预(fork 会随会话继承);default = 显式使用模型默认档位。',
25
+ rowHint:
26
+ '按子代理 provider 名匹配;standard preset 中 spawn ↔ subagent 工具、fork ↔ subagent_fork 工具。',
27
+ rowDesc: '未注册的 provider 行不生效,可保留待用。',
28
+ noRows: '当前未发现子代理 provider;配置将在 provider 注册后生效。',
29
+ catalogError: '提供商目录加载失败:',
30
+ catalogRetry: '重试',
31
+ catalogLoading: '目录加载中…',
32
+ unknownValue: '目录外',
33
+ invalidModel: '模型不能脱离提供商单独配置。',
34
+ }
35
+
36
+ const ownEn = {
37
+ title: 'Subagent model config',
38
+ description:
39
+ 'Pick a provider, model, and thinking effort for subagent / subagent_fork children, or inherit from the main agent.',
40
+ enabled: 'Enable plugin',
41
+ rowEnabled: 'Enable row',
42
+ provider: 'Provider',
43
+ model: 'Model',
44
+ effort: 'Effort',
45
+ inheritProvider: 'Inherit from main agent',
46
+ inheritModel: 'Inherit from main agent',
47
+ effortInherit: 'Inherit from main agent',
48
+ effortDefault: 'Follow model default',
49
+ providerHint: 'Empty = the child follows the main agent\u2019s provider and model.',
50
+ modelHint: 'Empty = follow the main agent\u2019s model (must match the chosen provider).',
51
+ effortHint:
52
+ 'inherit = do not touch (forks inherit via session seed); default = explicitly use the model\u2019s default level.',
53
+ rowHint:
54
+ 'Matched by subagent provider name; in the standard preset spawn \u2194 subagent tool, fork \u2194 subagent_fork tool.',
55
+ rowDesc: 'Rows for unregistered providers are inert; keep them for later.',
56
+ noRows: 'No subagent provider registered yet; config takes effect once one appears.',
57
+ catalogError: 'Failed to load provider catalog: ',
58
+ catalogRetry: 'Retry',
59
+ catalogLoading: 'Loading catalog\u2026',
60
+ unknownValue: 'outside catalog',
61
+ invalidModel: 'Model cannot be set without a provider.',
62
+ }
63
+
64
+ export const zh: Record<string, string> = mergeDict(commonZh, ownZh)
65
+ export const en: Record<string, string> = mergeDict(commonEn, ownEn)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('dsm'),
3
+ * 本文件只补插件特有规则(provider 行块 / banner / 目录空态)。
4
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
5
+ * @module subagent-model/client/styles
6
+ */
7
+ import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
8
+
9
+ export const PLUGIN_ID = '@dsh-plus/subagent-model'
10
+
11
+ const extraCss = `
12
+ .dsm-row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;margin:12px 0 0;padding:4px 12px 10px}
13
+ .dsm-rowHead{align-items:center;gap:8px;padding:8px 0 2px;display:flex}
14
+ .dsm-rowName{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:600;line-height:1.5}
15
+ .dsm-rowHint{color:var(--dsw-alias-label-tertiary);margin:0 0 4px;font-size:12px;line-height:1.5}
16
+ .dsm-banner{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);justify-content:space-between;align-items:center;gap:8px;border-radius:8px;margin-top:12px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}
17
+ .dsm-bannerRetry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}
18
+ .dsm-empty{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
19
+ @media (max-width:767px){
20
+ .dsm-row{padding:2px 8px 8px}
21
+ .dsm-rowHead{flex-wrap:wrap}
22
+ }
23
+ `
24
+
25
+ export const cardCssAll = cardCss('dsm', extraCss)
26
+
27
+ /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
28
+ export function injectStyle(): HTMLStyleElement | null {
29
+ return injectCardStyle(PLUGIN_ID, cardCssAll)
30
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 自定义端点 HTTP 通道:配置读写已迁移到官方 settings RPC(rc7 起第三方
3
+ * 命名空间全量开放),这里只保留官方传输没有的「模型目录」端点。
4
+ * 仅监听 dsh web 同源(webserver 默认 127.0.0.1),与 GUI 其余面同等暴露面。
5
+ * @module subagent-model/config-api
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from 'node:http'
8
+
9
+ import type { Context } from '@deepseek-ai/cordis'
10
+
11
+ import { buildModelCatalog, type ModelCatalog } from './catalog.ts'
12
+
13
+ const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
14
+
15
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
16
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
17
+ res.end(JSON.stringify(body))
18
+ }
19
+
20
+ async function handleCatalog(ctx: Context, res: ServerResponse): Promise<void> {
21
+ const catalog: ModelCatalog = await buildModelCatalog(ctx)
22
+ sendJson(res, 200, catalog)
23
+ }
24
+
25
+ /** 注册目录路由;返回是否注册成功(webServer 缺失时由调用方保证不调用)。 */
26
+ export function registerCatalogApi(ctx: Context): void {
27
+ const logger = ctx.logger('subagent-model')
28
+ const guard = (
29
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>,
30
+ ): ((req: IncomingMessage, res: ServerResponse) => Promise<void>) => {
31
+ return async (req, res) => {
32
+ try {
33
+ await handler(req, res)
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error)
36
+ logger.warn(`catalog api ${req.method ?? '?'} ${req.url ?? '?'} failed: ${message}`)
37
+ if (!res.headersSent) sendJson(res, 400, { error: message })
38
+ else res.end()
39
+ }
40
+ }
41
+ }
42
+ ctx.webServer.register({
43
+ kind: 'exact',
44
+ path: ROUTE_CATALOG,
45
+ handler: guard(async (req, res) => {
46
+ if (req.method !== 'GET') {
47
+ sendJson(res, 405, { error: 'method not allowed' })
48
+ return
49
+ }
50
+ await handleCatalog(ctx, res)
51
+ }),
52
+ })
53
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * 或选择从主代理继承;`default` 条目可让 spawn/fork 共享同一路由。
5
5
  * 主代理显式选择的路由(官方 subagent-model-selection 的 model 字段)优先,
6
6
  * 本插件只补"未选择时的强制默认"。
7
- * 配置经 settings 用户层($DSH_HOME/settings.yaml)持久化并热生效。
7
+ * 配置经 settings 用户层($DSH_HOME/settings.yaml)持久化并热生效;
8
+ * 配置 UI 位于 webui 设置-插件-插件配置(settings.plugin.item 卡片,
9
+ * 模型目录经 host webServer 同源端点下发)。
8
10
  * @module @dsh-plus/subagent-model
9
11
  */
10
12
  import type { Context } from '@deepseek-ai/cordis'
@@ -14,7 +16,7 @@ import { SubagentModelService } from './service.ts'
14
16
 
15
17
  export const name = 'dsh-plus-subagent-model'
16
18
 
17
- export const inject = ['subagents'] as const
19
+ export const inject = ['subagents', 'llm'] as const
18
20
 
19
21
  export type {
20
22
  EntryConfig,
package/src/service.ts CHANGED
@@ -5,11 +5,14 @@
5
5
  * 走 validateEntries 规则(model 不能脱离 provider 等),非法即拒。
6
6
  * - 委托挂钩:包装 ctx.subagents.start/startContinuable,按 provider 名
7
7
  * (未命中回落 default 条目)注入 agentOptions(provider/model/effort)。
8
+ * - 自定义端点:ctx.webServer 仅注册模型目录路由(配置读写已走官方
9
+ * remote.settings 直连,settings.plugin.item 卡片见 client 半)。
8
10
  * @module @dsh-plus/subagent-model
9
11
  */
10
12
  import { Context, Service } from '@deepseek-ai/cordis'
11
13
 
12
14
  import { Config, SETTINGS_NS, type SubagentModelConfig, validateEntries } from './config.ts'
15
+ import { registerCatalogApi } from './config-api.ts'
13
16
  import { installDelegationHook } from './delegation.ts'
14
17
 
15
18
  declare module '@deepseek-ai/cordis' {
@@ -19,7 +22,7 @@ declare module '@deepseek-ai/cordis' {
19
22
  }
20
23
 
21
24
  export class SubagentModelService extends Service {
22
- static [Context.inject] = ['subagents']
25
+ static [Context.inject] = ['subagents', 'llm']
23
26
 
24
27
  private current: () => SubagentModelConfig
25
28
 
@@ -41,6 +44,10 @@ export class SubagentModelService extends Service {
41
44
  })
42
45
  })
43
46
  installDelegationHook(ctx, () => this.current())
47
+ // 模型目录端点(卡片下拉数据源;仅监听 dsh web 同源)。
48
+ ctx.inject(['webServer'], (webCtx) => {
49
+ registerCatalogApi(webCtx)
50
+ })
44
51
  }
45
52
 
46
53
  /** 当前生效配置(settings 用户层解析结果或 cordis 行级 config)。 */