@dsh-plus/subagent-model 0.2.1 → 0.3.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-plus/subagent-model",
3
- "version": "0.2.1",
4
- "description": "dsh-plus service plugin: 为 subagent/subagent_fork 等子代理按 provider 配置模型与思考程度(或继承主代理)",
3
+ "version": "0.3.1",
4
+ "description": "dsh-plus service+ui plugin: 为 subagent/subagent_fork 等子代理按提供商配置模型与思考程度(或继承主代理)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -10,6 +10,7 @@
10
10
  "types": "./lib/index.d.ts",
11
11
  "default": "./lib/index.js"
12
12
  },
13
+ "./client": "./lib/client.js",
13
14
  "./src/*": "./src/*",
14
15
  "./package.json": "./package.json"
15
16
  },
@@ -17,21 +18,41 @@
17
18
  "lib",
18
19
  "src"
19
20
  ],
21
+ "dependencies": {
22
+ "@dsh-plus/shared": "0.1.11"
23
+ },
20
24
  "peerDependencies": {
21
- "@deepseek-ai/cordis": "^4.0.1",
22
- "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
23
- "@deepseek-ai/dsh-subagent": "^0.1.3-alpha.2",
24
- "@deepseek-ai/schemastery": "^3.18.1"
25
+ "@deepseek-ai/cordis": "^4.0.2",
26
+ "@deepseek-ai/dsh-host-webserver": "*",
27
+ "@deepseek-ai/dsh-llm": "*",
28
+ "@deepseek-ai/dsh-settings": "*",
29
+ "@deepseek-ai/dsh-subagent": "*",
30
+ "@deepseek-ai/schemastery": "^3.18.2"
25
31
  },
26
32
  "devDependencies": {
27
33
  "@deepseek-ai/cordis": "4.0.2",
28
- "@deepseek-ai/dsh-settings": "0.1.3-alpha.2",
29
- "@deepseek-ai/dsh-subagent": "0.1.3-alpha.2",
30
- "@deepseek-ai/schemastery": "3.18.2"
34
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
35
+ "@deepseek-ai/dsh-llm": "0.1.5-alpha.1",
36
+ "@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
37
+ "@deepseek-ai/dsh-subagent": "0.1.5-alpha.1",
38
+ "@deepseek-ai/schemastery": "3.18.2",
39
+ "@types/node": "26.4.0",
40
+ "@types/react": "~18.3.31",
41
+ "react": "^18.2.0"
31
42
  },
32
43
  "publishConfig": {
33
44
  "access": "public"
34
45
  },
46
+ "dsh": {
47
+ "client": {
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-renderer",
50
+ "@deepseek-ai/dsh-client-locale",
51
+ "@deepseek-ai/dsh-api-remotes"
52
+ ],
53
+ "platform": "web"
54
+ }
55
+ },
35
56
  "license": "MIT",
36
57
  "repository": {
37
58
  "type": "git",
package/src/catalog.ts ADDED
@@ -0,0 +1,127 @@
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 {
63
+ providers: [],
64
+ failures: [],
65
+ subagentProviders: subagentProvidersOf(ctx),
66
+ }
67
+ }
68
+ const catalog = await Promise.all(
69
+ llm.listProviders().map(async (provider) => {
70
+ try {
71
+ const models = await llm.listModels(provider.id)
72
+ const entries = await Promise.all(
73
+ models.map(async (model) => {
74
+ const resolved = await llm.resolveModelInfo(provider.id, model.id)
75
+ const reasoning =
76
+ resolved.reasoning === undefined
77
+ ? undefined
78
+ : {
79
+ efforts: resolved.reasoning.efforts.map(toEffort),
80
+ ...(resolved.reasoning.defaultEffort === undefined
81
+ ? {}
82
+ : {
83
+ defaultEffort: String(resolved.reasoning.defaultEffort),
84
+ }),
85
+ }
86
+ return {
87
+ id: model.id,
88
+ name: model.name,
89
+ ...(model.description === undefined ? {} : { description: model.description }),
90
+ ...(reasoning === undefined ? {} : { reasoning }),
91
+ }
92
+ }),
93
+ )
94
+ return {
95
+ kind: 'group' as const,
96
+ group: { id: provider.id, name: provider.name, models: entries },
97
+ }
98
+ } catch (error) {
99
+ return {
100
+ kind: 'failure' as const,
101
+ failure: {
102
+ id: provider.id,
103
+ name: provider.name,
104
+ message: error instanceof Error ? error.message : String(error),
105
+ },
106
+ }
107
+ }
108
+ }),
109
+ )
110
+ return {
111
+ providers: catalog
112
+ .filter((item) => item.kind === 'group' && item.group.models.length > 0)
113
+ .map((item) => (item.kind === 'group' ? item.group : undefined))
114
+ .filter((group): group is CatalogProvider => group !== undefined),
115
+ failures: catalog
116
+ .filter((item) => item.kind === 'failure')
117
+ .map((item) => (item.kind === 'failure' ? item.failure : undefined))
118
+ .filter((failure): failure is CatalogFailure => failure !== undefined),
119
+ subagentProviders: subagentProvidersOf(ctx),
120
+ }
121
+ }
122
+
123
+ /** 已注册的子代理 provider 名(llm 服务缺失时同样返回,卡片仍可渲染)。 */
124
+ function subagentProvidersOf(ctx: Context): string[] {
125
+ const subagents = ctx.get('subagents')
126
+ return subagents === undefined ? [] : [...subagents.list()]
127
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 自定义端点通道:仅剩「模型目录」(配置读写已迁移到官方 settingsScope
3
+ * 传输,见 scope.ts 与 card.tsx)。
4
+ * @module subagent-model/client/api
5
+ */
6
+
7
+ export interface CatalogEffort {
8
+ id: string
9
+ name: string
10
+ description?: string
11
+ }
12
+
13
+ export interface CatalogModel {
14
+ id: string
15
+ name: string
16
+ description?: string
17
+ reasoning?: {
18
+ defaultEffort?: string
19
+ efforts: CatalogEffort[]
20
+ }
21
+ }
22
+
23
+ export interface CatalogProvider {
24
+ id: string
25
+ name: string
26
+ models: CatalogModel[]
27
+ }
28
+
29
+ export interface ModelCatalog {
30
+ providers: CatalogProvider[]
31
+ failures: { id: string; name: string; message: string }[]
32
+ subagentProviders: string[]
33
+ }
34
+
35
+ const ROUTE_CATALOG = '/dsh-plus/subagent-model/catalog'
36
+
37
+ async function parse<T>(res: Response): Promise<T> {
38
+ const body = (await res.json()) as T & { error?: string }
39
+ if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`)
40
+ return body
41
+ }
42
+
43
+ export async function fetchCatalog(): Promise<ModelCatalog> {
44
+ return parse<ModelCatalog>(await fetch(ROUTE_CATALOG, { credentials: 'same-origin' }))
45
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 「子代理模型配置」配置卡片:注册进 settings.plugin.item 插槽(官方插件配置页)。
3
+ * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/CheckRow/SelectField)。
4
+ * 配置读写走官方 settingsScope 传输:value 为 schema 解析后的命名空间值
5
+ * (enabled + entries),行集合 = 目录返回的已注册子代理 provider
6
+ * ∪ 已配置条目,保存时全量写回 entries(未配置的新 provider 行以默认空值
7
+ * 落盘,自文档化);「模型目录」为唯一保留的自定义端点。
8
+ * @module subagent-model/client/card
9
+ */
10
+
11
+ import {
12
+ CardChrome,
13
+ type CardStatusState,
14
+ CheckRow,
15
+ IDLE_STATUS,
16
+ type NamespaceSettingsApi,
17
+ type Scope,
18
+ SelectField,
19
+ type SelectOption,
20
+ } from '@dsh-plus/shared/client'
21
+ import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
22
+ import { type CatalogProvider, fetchCatalog, type ModelCatalog } from './api.ts'
23
+ import {
24
+ type ConfigValue,
25
+ type Draft,
26
+ type DraftRow,
27
+ draftFrom,
28
+ EMPTY_ROW,
29
+ toPatch,
30
+ } from './draft.ts'
31
+
32
+ export interface CardProps {
33
+ t(key: string): string
34
+ scope: Scope
35
+ api: NamespaceSettingsApi
36
+ }
37
+
38
+ const EFFORT_INHERIT = 'inherit'
39
+ const EFFORT_DEFAULT = 'default'
40
+
41
+ function unknownOption(value: string, label: string): SelectOption {
42
+ return { value, label: `${value}(${label})` }
43
+ }
44
+
45
+ function providerOptions(
46
+ catalog: ModelCatalog | null,
47
+ row: DraftRow,
48
+ t: (k: string) => string,
49
+ ): SelectOption[] {
50
+ const options: SelectOption[] = [{ value: '', label: t('inheritProvider') }]
51
+ for (const provider of catalog?.providers ?? [])
52
+ options.push({ value: provider.id, label: provider.name })
53
+ if (row.provider.length > 0 && !options.some((o) => o.value === row.provider)) {
54
+ options.push(unknownOption(row.provider, t('unknownValue')))
55
+ }
56
+ return options
57
+ }
58
+
59
+ function modelOptions(
60
+ catalog: ModelCatalog | null,
61
+ row: DraftRow,
62
+ t: (k: string) => string,
63
+ ): SelectOption[] {
64
+ const options: SelectOption[] = [{ value: '', label: t('inheritModel') }]
65
+ const provider = catalog?.providers.find((p) => p.id === row.provider)
66
+ for (const model of provider?.models ?? []) options.push({ value: model.id, label: model.name })
67
+ if (row.model.length > 0 && !options.some((o) => o.value === row.model)) {
68
+ options.push(unknownOption(row.model, t('unknownValue')))
69
+ }
70
+ return options
71
+ }
72
+
73
+ function effortOptions(
74
+ catalog: ModelCatalog | null,
75
+ row: DraftRow,
76
+ t: (k: string) => string,
77
+ ): SelectOption[] {
78
+ const options: SelectOption[] = [
79
+ { value: EFFORT_INHERIT, label: t('effortInherit') },
80
+ { value: EFFORT_DEFAULT, label: t('effortDefault') },
81
+ ]
82
+ const provider = catalog?.providers.find((p) => p.id === row.provider)
83
+ const model = provider?.models.find((m) => m.id === row.model)
84
+ for (const effort of model?.reasoning?.efforts ?? [])
85
+ options.push({ value: effort.id, label: effort.name })
86
+ if (row.reasoningEffort.length > 0 && !options.some((o) => o.value === row.reasoningEffort)) {
87
+ options.push(unknownOption(row.reasoningEffort, t('unknownValue')))
88
+ }
89
+ return options
90
+ }
91
+
92
+ interface RowBlockProps {
93
+ name: string
94
+ row: DraftRow
95
+ catalog: ModelCatalog | null
96
+ disabled: boolean
97
+ t(key: string): string
98
+ onEdit(row: DraftRow): void
99
+ }
100
+
101
+ function RowBlock(props: RowBlockProps): ReactElement {
102
+ const { name, row, catalog, disabled, t, onEdit } = props
103
+ const invalidModel = row.model.length > 0 && row.provider.length === 0
104
+ return (
105
+ <div className="dsm-row">
106
+ <div className="dsm-rowHead">
107
+ <span className="dsm-rowName">{name}</span>
108
+ <CheckRow
109
+ prefix="dsm"
110
+ id={`dsm-row-${name}`}
111
+ label={t('rowEnabled')}
112
+ checked={row.enabled}
113
+ disabled={disabled}
114
+ onEdit={(v) => onEdit({ ...row, enabled: v })}
115
+ />
116
+ </div>
117
+ <p className="dsm-rowHint">{t('rowHint')}</p>
118
+ <SelectField
119
+ prefix="dsm"
120
+ id={`dsm-provider-${name}`}
121
+ label={t('provider')}
122
+ hint={t('providerHint')}
123
+ value={row.provider}
124
+ options={providerOptions(catalog, row, t)}
125
+ disabled={disabled}
126
+ onEdit={(v) => onEdit({ ...row, provider: v })}
127
+ />
128
+ <SelectField
129
+ prefix="dsm"
130
+ id={`dsm-model-${name}`}
131
+ label={t('model')}
132
+ hint={t('modelHint')}
133
+ value={row.model}
134
+ options={modelOptions(catalog, row, t)}
135
+ disabled={disabled || row.provider.length === 0}
136
+ invalid={invalidModel}
137
+ invalidLabel={t('invalidModel')}
138
+ onEdit={(v) => onEdit({ ...row, model: v })}
139
+ />
140
+ <SelectField
141
+ prefix="dsm"
142
+ id={`dsm-effort-${name}`}
143
+ label={t('effort')}
144
+ hint={t('effortHint')}
145
+ value={row.reasoningEffort}
146
+ options={effortOptions(catalog, row, t)}
147
+ disabled={disabled}
148
+ onEdit={(v) => onEdit({ ...row, reasoningEffort: v })}
149
+ />
150
+ </div>
151
+ )
152
+ }
153
+
154
+ export function SubagentModelCard(props: CardProps): ReactElement | null {
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
161
+ const [open, setOpen] = useState(false)
162
+ const [draft, setDraft] = useState<Draft | null>(null)
163
+ const [catalog, setCatalog] = useState<ModelCatalog | null>(null)
164
+ const [catalogFailed, setCatalogFailed] = useState(false)
165
+ const [saving, setSaving] = useState(false)
166
+ const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
167
+
168
+ // 播种草稿(行集 = 已配置条目);catalog 到达后只补缺失的 provider 空行,
169
+ // 不覆盖在途编辑;后续 Host 更新同样不覆盖。
170
+ useEffect(() => {
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])
185
+
186
+ useEffect(() => {
187
+ if (!open || catalog !== null) return
188
+ let alive = true
189
+ fetchCatalog()
190
+ .then((loaded) => {
191
+ if (!alive) return
192
+ setCatalog(loaded)
193
+ setCatalogFailed(false)
194
+ })
195
+ .catch(() => {
196
+ if (alive) setCatalogFailed(true)
197
+ })
198
+ return () => {
199
+ alive = false
200
+ }
201
+ }, [open, catalog])
202
+
203
+ const dirty = useMemo(
204
+ () =>
205
+ value !== undefined &&
206
+ draft !== null &&
207
+ JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFrom(value, catalog))),
208
+ [value, draft, catalog],
209
+ )
210
+ const invalid = useMemo(
211
+ () =>
212
+ draft !== null &&
213
+ Object.values(draft.rows).some((row) => row.model.length > 0 && row.provider.length === 0),
214
+ [draft],
215
+ )
216
+
217
+ if (value === undefined || draft === null) {
218
+ return (
219
+ <li className="dsm-card">
220
+ <p className="dsm-empty">{t('loading')}</p>
221
+ </li>
222
+ )
223
+ }
224
+ const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
225
+ setDraft({ ...draft, [key]: editValue })
226
+ setStatus(IDLE_STATUS)
227
+ }
228
+ const editRow = (name: string, row: DraftRow): void => {
229
+ setDraft({ ...draft, rows: { ...draft.rows, [name]: row } })
230
+ setStatus(IDLE_STATUS)
231
+ }
232
+ const onSave = (): void => {
233
+ setSaving(true)
234
+ const revision = scope.getSnapshot().revision
235
+ api
236
+ .update(toPatch(draft), revision)
237
+ .then(async () => {
238
+ await scope.load()
239
+ const next = scope.getSnapshot().value as ConfigValue | undefined
240
+ if (next !== undefined) setDraft(draftFrom(next, catalog))
241
+ setStatus(IDLE_STATUS)
242
+ })
243
+ .catch((error: unknown) => {
244
+ setStatus({
245
+ kind: 'error',
246
+ text: `${t('saveFailed')}${error instanceof Error ? error.message : ''}`,
247
+ })
248
+ })
249
+ .finally(() => setSaving(false))
250
+ }
251
+ const onRetryCatalog = (): void => {
252
+ setCatalog(null)
253
+ setCatalogFailed(false)
254
+ }
255
+
256
+ const disabled = !snapshot.writable
257
+ const rowNames = Object.keys(draft.rows).sort()
258
+ const providerCount = (catalog?.providers ?? []).length
259
+ return (
260
+ <CardChrome
261
+ prefix="dsm"
262
+ title={t('title')}
263
+ description={t('description')}
264
+ open={open}
265
+ onToggle={setOpen}
266
+ statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
267
+ dirty={dirty}
268
+ dirtyLabel={t('unsaved')}
269
+ readOnlyNotice={disabled ? t('readOnly') : undefined}
270
+ status={status}
271
+ actions={[
272
+ {
273
+ key: 'discard',
274
+ label: t('discard'),
275
+ disabled: !dirty || saving,
276
+ onClick: () => {
277
+ setDraft(draftFrom(value, catalog))
278
+ setStatus(IDLE_STATUS)
279
+ },
280
+ },
281
+ {
282
+ key: 'save',
283
+ label: t(saving ? 'saving' : 'save'),
284
+ variant: 'primary',
285
+ disabled: !dirty || invalid || saving || disabled,
286
+ onClick: onSave,
287
+ },
288
+ ]}
289
+ >
290
+ <CheckRow
291
+ prefix="dsm"
292
+ id="dsm-enabled"
293
+ label={t('enabled')}
294
+ checked={draft.enabled}
295
+ disabled={disabled}
296
+ onEdit={(v) => edit('enabled', v)}
297
+ />
298
+ {catalogFailed ? (
299
+ <div className="dsm-banner" role="status">
300
+ <span>{t('catalogError')}</span>
301
+ <button type="button" className="dsm-bannerRetry" onClick={onRetryCatalog}>
302
+ {t('catalogRetry')}
303
+ </button>
304
+ </div>
305
+ ) : null}
306
+ {catalog === null && !catalogFailed ? (
307
+ <p className="dsm-empty" role="status">
308
+ {t('catalogLoading')}
309
+ </p>
310
+ ) : null}
311
+ {rowNames.length === 0 ? (
312
+ <p className="dsm-empty" role="status">
313
+ {t('noRows')}
314
+ </p>
315
+ ) : null}
316
+ {rowNames.map((name) => (
317
+ <RowBlock
318
+ key={name}
319
+ name={name}
320
+ row={draft.rows[name] ?? { ...EMPTY_ROW }}
321
+ catalog={catalog}
322
+ disabled={disabled}
323
+ t={t}
324
+ onEdit={(row) => editRow(name, row)}
325
+ />
326
+ ))}
327
+ <p className="dsm-hint dsm-rowHint">{providerCount > 0 ? t('rowDesc') : ''}</p>
328
+ </CardChrome>
329
+ )
330
+ }
331
+
332
+ export type { CatalogProvider }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 浏览器半入口:注册 locale 字典 + 向 settings.plugin.item 插槽注册「子代理模型配置」卡片。
3
+ * 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
4
+ * (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
5
+ *
6
+ * 类型说明:浏览器半只用到 slots/locale/remote 的很窄一面,
7
+ * scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
8
+ * 0.1.2-alpha.1 起配置读写统一走 ctx.remote.settings 直连
9
+ * (connection.api.settings RPC 面已移除;不复用 settingsScope 服务——
10
+ * 非 loopback 页面下它固定 memory 模式无数据);自定义端点仅剩「模型目录」(api.ts)。
11
+ * 插槽为 keyed 槽位:key 必须是卡片编辑的 settings 命名空间
12
+ * (即服务端 SETTINGS_NS 注册的同一字面量,见 ../ns.ts),
13
+ * 官方配置页按此 key 与 Host 已注册命名空间配对分发。
14
+ * @module @dsh-plus/subagent-model/client
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis'
17
+
18
+ import {
19
+ createNamespaceApi,
20
+ createSettingsScope,
21
+ type SettingsRemoteFace,
22
+ } from '@dsh-plus/shared/client'
23
+ import { SETTINGS_NS } from '../ns.ts'
24
+ import { SubagentModelCard } from './card.tsx'
25
+ import { en, NS, zh } from './i18n.ts'
26
+ import { injectStyle } from './styles.ts'
27
+
28
+ export const name = 'dsh-plus-subagent-model'
29
+
30
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
31
+ export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
32
+
33
+ interface SlotsLike {
34
+ inject(key: string, callback: () => unknown): unknown
35
+ register(options: Record<string, unknown>, component: unknown): () => void
36
+ }
37
+
38
+ interface LocaleLike {
39
+ register(ns: string, dict: { zh: Record<string, string>; en: Record<string, string> }): () => void
40
+ bind(ns: string): (key: string) => string
41
+ }
42
+
43
+ interface RemoteLike {
44
+ settings: SettingsRemoteFace
45
+ $on(event: string, listener: (payload?: unknown) => void): () => void
46
+ }
47
+
48
+ interface ClientContext {
49
+ slots: SlotsLike
50
+ locale: LocaleLike
51
+ get(key: 'remote'): RemoteLike
52
+ on(event: string, listener: () => void): () => void
53
+ effect(execute: () => () => void, label?: string): unknown
54
+ }
55
+
56
+ export function apply(ctx: Context): void {
57
+ const c = ctx as unknown as ClientContext
58
+ const tag = injectStyle()
59
+ c.effect(
60
+ () => () => {
61
+ tag?.remove()
62
+ },
63
+ 'subagent-model: style',
64
+ )
65
+ c.effect(() => c.locale.register(NS, { zh, en }), 'subagent-model: locale')
66
+ const scope = createSettingsScope(c, SETTINGS_NS, 'subagent-model: settings scope')
67
+ const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
68
+ c.slots.inject('settings.plugin.item', () =>
69
+ c.slots.register(
70
+ {
71
+ name: 'settings.plugin.item',
72
+ key: SETTINGS_NS,
73
+ locale: NS,
74
+ inject: () => ({ t: c.locale.bind(NS), scope, api }),
75
+ },
76
+ SubagentModelCard,
77
+ ),
78
+ )
79
+ }