@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/lib/client.js +62638 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +110 -2
- package/lib/style.css +1639 -0
- package/package.json +30 -9
- package/src/catalog.ts +127 -0
- package/src/client/api.ts +45 -0
- package/src/client/card.tsx +332 -0
- package/src/client/client.ts +79 -0
- package/src/client/draft.ts +69 -0
- package/src/client/i18n.ts +65 -0
- package/src/client/styles.ts +30 -0
- package/src/config-api.ts +53 -0
- package/src/index.ts +4 -2
- package/src/service.ts +8 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/subagent-model",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "dsh-plus service plugin: 为 subagent/subagent_fork
|
|
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.
|
|
22
|
-
"@deepseek-ai/dsh-
|
|
23
|
-
"@deepseek-ai/dsh-
|
|
24
|
-
"@deepseek-ai/
|
|
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-
|
|
29
|
-
"@deepseek-ai/dsh-
|
|
30
|
-
"@deepseek-ai/
|
|
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
|
+
}
|