@dsh-plus/llm-pi 0.1.41 → 0.1.43
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 +1528 -767
- package/lib/index.d.ts +4 -8
- package/lib/index.js +943 -391
- package/package.json +35 -36
- package/src/catalog/browse.ts +297 -0
- package/src/catalog/builtin.ts +183 -35
- package/src/client/api.ts +96 -33
- package/src/client/card.tsx +183 -133
- package/src/client/client.ts +4 -4
- package/src/client/constants.ts +25 -5
- package/src/client/draft.ts +34 -13
- package/src/client/fields.tsx +18 -3
- package/src/client/i18n.ts +153 -49
- package/src/client/root-fields.tsx +64 -52
- package/src/client/route-fit.ts +117 -0
- package/src/client/styles.ts +40 -10
- package/src/client/views/catalog-browser.tsx +376 -0
- package/src/client/views/catalog-row.tsx +147 -0
- package/src/client/views/compat.tsx +31 -3
- package/src/client/views/models.tsx +189 -193
- package/src/client/views/provider-fields.tsx +2 -2
- package/src/client/views/providers.tsx +51 -11
- package/src/compat.ts +43 -19
- package/src/config-api.ts +65 -37
- package/src/config.ts +20 -28
- package/src/inherit.ts +15 -31
- package/src/kit-meta.ts +156 -0
- package/src/ns.ts +2 -2
- package/src/{compat-gates.ts → official-surface.ts} +60 -16
- package/src/profiles.ts +52 -13
- package/src/resolve-dsh.ts +291 -72
- package/src/service.ts +209 -90
- package/src/catalog/models-dev.ts +0 -261
package/src/client/api.ts
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* card.tsx
|
|
5
|
-
* 展示(运行期诊断,非配置数据)。
|
|
2
|
+
* 自定义端点通道:只剩浏览器半拿不到的两样东西——运行期套件事实(生效版本、
|
|
3
|
+
* 协议集合、目录规模、诊断)与 pi-ai 内置目录搜索(配置读写走 ctx.remote.settings
|
|
4
|
+
* 直连,见 card.tsx / shared 的 scope.ts)。
|
|
6
5
|
* @module llm-pi/client/api
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import type { CompatValue } from './constants.ts'
|
|
9
|
+
|
|
10
|
+
/** 运行期套件事实(服务端 kitInfo() 的镜像)。 */
|
|
11
|
+
export interface WireKitInfo {
|
|
12
|
+
source: string
|
|
13
|
+
root?: string
|
|
14
|
+
versions: { piAi?: string; piAiAdapter?: string; dsh?: string }
|
|
15
|
+
verifiedRange: string
|
|
16
|
+
versionNotice?: string
|
|
17
|
+
protocols: string[]
|
|
18
|
+
protocolSource: string
|
|
19
|
+
catalog: { providers: number; models: number; generatedAt?: number }
|
|
20
|
+
compatSource: string
|
|
21
|
+
diagnostics: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 内置 provider 条目。 */
|
|
25
|
+
export interface WireProviderEntry {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
baseUrl?: string
|
|
29
|
+
modelCount: number
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
export interface WireProvider {
|
|
@@ -54,9 +71,6 @@ export interface WireModel {
|
|
|
54
71
|
/** settings 命名空间的解析值(llm-pi 无 secret 字段,value 即完整配置)。 */
|
|
55
72
|
export interface ConfigValue {
|
|
56
73
|
enabled: boolean
|
|
57
|
-
catalogUrl: string
|
|
58
|
-
catalogRefreshHours: number
|
|
59
|
-
catalogProxy: string
|
|
60
74
|
providers: Record<string, WireProvider>
|
|
61
75
|
}
|
|
62
76
|
|
|
@@ -65,18 +79,59 @@ export type ConfigPatch = ConfigValue
|
|
|
65
79
|
|
|
66
80
|
/** compat 字段表(服务端从官方包推导后下发;浏览器半不手抄)。 */
|
|
67
81
|
export interface WireCompatTable {
|
|
68
|
-
fields: Record<string, Record<string,
|
|
82
|
+
fields: Record<string, Record<string, CompatValue>>
|
|
69
83
|
source: string
|
|
70
84
|
problem?: string
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
/**
|
|
74
|
-
export interface
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
/** 内置目录的单个模型(服务端 catalog/browse.ts 的镜像)。 */
|
|
88
|
+
export interface WireModelInfo {
|
|
89
|
+
provider: string
|
|
90
|
+
id: string
|
|
91
|
+
path: string
|
|
92
|
+
name: string
|
|
93
|
+
api: string
|
|
94
|
+
baseUrl: string
|
|
95
|
+
contextWindow: number
|
|
96
|
+
maxTokens: number
|
|
97
|
+
input: string[]
|
|
98
|
+
reasoning: boolean
|
|
99
|
+
thinkingLevelMap?: Record<string, string | null>
|
|
100
|
+
compat?: Record<string, unknown>
|
|
101
|
+
/** 目录给出但未单列的其余字段(cost / inputLimits 等),原样展示。 */
|
|
102
|
+
rest: Record<string, unknown>
|
|
103
|
+
/** 当前生效协议集合能否服务该模型。 */
|
|
104
|
+
servable: boolean
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `GET /catalog` 响应:运行期事实 + provider/协议索引 + compat 字段表。 */
|
|
108
|
+
export interface CatalogMeta {
|
|
109
|
+
kit: WireKitInfo
|
|
110
|
+
providers: WireProviderEntry[]
|
|
111
|
+
/** `{provider: {modelId: api}}`:推断 route 生效协议用。 */
|
|
112
|
+
apis: Record<string, Record<string, string>>
|
|
113
|
+
compat: WireCompatTable
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** `GET /catalog/models` 响应。 */
|
|
117
|
+
export interface CatalogPage {
|
|
118
|
+
total: number
|
|
119
|
+
servableHidden: number
|
|
120
|
+
offset: number
|
|
121
|
+
limit: number
|
|
122
|
+
items: WireModelInfo[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 目录搜索条件(与 server 端同名参数一一对应)。 */
|
|
126
|
+
export interface CatalogQuery {
|
|
127
|
+
q?: string
|
|
128
|
+
provider?: string
|
|
129
|
+
api?: string
|
|
130
|
+
reasoning?: boolean
|
|
131
|
+
image?: boolean
|
|
132
|
+
servableOnly?: boolean
|
|
133
|
+
offset?: number
|
|
134
|
+
limit?: number
|
|
80
135
|
}
|
|
81
136
|
|
|
82
137
|
const ROUTE_CATALOG = '/dsh-plus/llm-pi/catalog'
|
|
@@ -87,23 +142,31 @@ async function parse<T>(res: Response): Promise<T> {
|
|
|
87
142
|
return body
|
|
88
143
|
}
|
|
89
144
|
|
|
90
|
-
/**
|
|
91
|
-
export async function
|
|
92
|
-
|
|
93
|
-
source: 'builtin' | 'models-dev',
|
|
94
|
-
): Promise<CatalogResult> {
|
|
95
|
-
const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`
|
|
96
|
-
return parse<CatalogResult>(await fetch(url, { credentials: 'same-origin' }))
|
|
145
|
+
/** 运行期事实 + 目录索引(卡片挂载时一次)。 */
|
|
146
|
+
export async function fetchCatalogMeta(): Promise<CatalogMeta> {
|
|
147
|
+
return parse<CatalogMeta>(await fetch(ROUTE_CATALOG, { credentials: 'same-origin' }))
|
|
97
148
|
}
|
|
98
149
|
|
|
99
|
-
/**
|
|
100
|
-
export async function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
/** 目录搜索(模糊 q + provider/api/能力/分页)。 */
|
|
151
|
+
export async function searchCatalog(
|
|
152
|
+
query: CatalogQuery,
|
|
153
|
+
signal?: AbortSignal,
|
|
154
|
+
): Promise<CatalogPage> {
|
|
155
|
+
const params = new URLSearchParams()
|
|
156
|
+
if (query.q !== undefined && query.q.length > 0) params.set('q', query.q)
|
|
157
|
+
if (query.provider !== undefined && query.provider.length > 0)
|
|
158
|
+
params.set('provider', query.provider)
|
|
159
|
+
if (query.api !== undefined && query.api.length > 0) params.set('api', query.api)
|
|
160
|
+
if (query.reasoning === true) params.set('reasoning', '1')
|
|
161
|
+
if (query.image === true) params.set('image', '1')
|
|
162
|
+
if (query.servableOnly === false) params.set('servable', '0')
|
|
163
|
+
if (query.offset !== undefined) params.set('offset', String(query.offset))
|
|
164
|
+
if (query.limit !== undefined) params.set('limit', String(query.limit))
|
|
165
|
+
const suffix = params.toString()
|
|
166
|
+
return parse<CatalogPage>(
|
|
167
|
+
await fetch(`${ROUTE_CATALOG}/models${suffix === '' ? '' : `?${suffix}`}`, {
|
|
106
168
|
credentials: 'same-origin',
|
|
169
|
+
...(signal === undefined ? {} : { signal }),
|
|
107
170
|
}),
|
|
108
171
|
)
|
|
109
172
|
}
|
package/src/client/card.tsx
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「LLM 路由」配置卡片:经 injectPluginConfigCard
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* 「LLM 路由」配置卡片:经 injectPluginConfigCard 注册(插件页
|
|
3
|
+
* plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
|
|
4
|
+
*
|
|
5
|
+
* 页面结构(自上而下,除根字段外**默认全部收起**,避免长列表划不到头):
|
|
6
|
+
* 1. 根字段:启用开关 + 运行期状态行(生效版本/安装树/目录规模/协议/compat 来源/诊断);
|
|
7
|
+
* 2. 「内置模型目录」浏览器(折叠小节,默认收起):搜索/筛选/复制路径 id/
|
|
8
|
+
* 一键把 `{id, extends}` 加到目标 route(添加后自动展开该 route 并回显);
|
|
9
|
+
* 3. Provider 路由列表(每 route 收起;route 内模型行同样默认收起)。
|
|
10
|
+
*
|
|
11
|
+
* 数据来源:配置读写走官方 remote.settings(scope/api);运行期事实与目录搜索
|
|
12
|
+
* 走本插件 `/catalog` 端点(浏览器半读不到 pi-ai 包)。草稿/保存与目录索引各由
|
|
13
|
+
* 本文件的 hook 承担,全部状态(浏览器开合、目标 route、route 展开态)在卡片持有,
|
|
14
|
+
* 子组件受控。
|
|
9
15
|
* @module llm-pi/client/card
|
|
10
16
|
*/
|
|
11
17
|
|
|
@@ -14,25 +20,27 @@ import {
|
|
|
14
20
|
CardChrome,
|
|
15
21
|
CardLoading,
|
|
16
22
|
type CardStatusState,
|
|
17
|
-
cardVariantFor,
|
|
18
23
|
IDLE_STATUS,
|
|
19
24
|
type NamespaceSettingsApi,
|
|
20
25
|
type PluginConfigViewProps,
|
|
21
26
|
type Scope,
|
|
22
27
|
} from '@dsh-plus/shared/client'
|
|
23
28
|
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
24
|
-
import { type
|
|
25
|
-
import { installCompatFields } from './constants.ts'
|
|
29
|
+
import { type CatalogMeta, type ConfigValue, fetchCatalogMeta, type WireModelInfo } from './api.ts'
|
|
30
|
+
import { installCompatFields, installProtocols } from './constants.ts'
|
|
26
31
|
import {
|
|
32
|
+
addModelEntry,
|
|
27
33
|
type Draft,
|
|
28
34
|
draftFromValue,
|
|
29
35
|
emptyProviderDraft,
|
|
30
|
-
numTextOk,
|
|
31
36
|
type ProviderDraft,
|
|
32
37
|
toPatch,
|
|
33
38
|
} from './draft.ts'
|
|
39
|
+
import { CollapseSection } from './fields.tsx'
|
|
34
40
|
import type { Translate } from './i18n.ts'
|
|
35
41
|
import { RootFields } from './root-fields.tsx'
|
|
42
|
+
import { type ApiIndex, routeApiFacts, targetRoutes } from './route-fit.ts'
|
|
43
|
+
import { CatalogBrowser } from './views/catalog-browser.tsx'
|
|
36
44
|
import { ProvidersSection } from './views/providers.tsx'
|
|
37
45
|
|
|
38
46
|
export interface CardProps extends PluginConfigViewProps {
|
|
@@ -41,29 +49,94 @@ export interface CardProps extends PluginConfigViewProps {
|
|
|
41
49
|
api: NamespaceSettingsApi
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
/**
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
setModelsDevStatus(next: WireModelsDevStatus): void
|
|
49
|
-
} {
|
|
50
|
-
const [kitSource, setKitSource] = useState<string | null>(null)
|
|
51
|
-
const [modelsDevStatus, setModelsDevStatus] = useState<WireModelsDevStatus | null>(null)
|
|
52
|
+
/** 运行期事实 + 目录索引:卡片挂载拉一次(同时安装协议集合与 compat 字段表)。 */
|
|
53
|
+
function useCatalogMeta(): { meta: CatalogMeta | null; error: string } {
|
|
54
|
+
const [meta, setMeta] = useState<CatalogMeta | null>(null)
|
|
55
|
+
const [error, setError] = useState('')
|
|
52
56
|
useEffect(() => {
|
|
53
57
|
let alive = true
|
|
54
|
-
|
|
58
|
+
fetchCatalogMeta()
|
|
55
59
|
.then((result) => {
|
|
56
60
|
if (!alive) return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
installProtocols(result.kit.protocols)
|
|
62
|
+
installCompatFields(result.compat.fields)
|
|
63
|
+
setMeta(result)
|
|
64
|
+
})
|
|
65
|
+
.catch((reason: unknown) => {
|
|
66
|
+
if (!alive) return
|
|
67
|
+
setError(reason instanceof Error ? reason.message : String(reason))
|
|
60
68
|
})
|
|
61
|
-
.catch(() => {})
|
|
62
69
|
return () => {
|
|
63
70
|
alive = false
|
|
64
71
|
}
|
|
65
72
|
}, [])
|
|
66
|
-
return {
|
|
73
|
+
return { meta, error }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 草稿与保存:首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑
|
|
78
|
+
* (与官方 staged 表单一致)。dirty 由"提交形状"双向比较得出。
|
|
79
|
+
*/
|
|
80
|
+
function useCardDraft(options: {
|
|
81
|
+
value: ConfigValue | undefined
|
|
82
|
+
scope: Scope
|
|
83
|
+
api: NamespaceSettingsApi
|
|
84
|
+
t: Translate
|
|
85
|
+
}) {
|
|
86
|
+
const { value, scope, api, t } = options
|
|
87
|
+
const [draft, setDraft] = useState<Draft | null>(null)
|
|
88
|
+
const [epoch, setEpoch] = useState(0)
|
|
89
|
+
const [saving, setSaving] = useState(false)
|
|
90
|
+
const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (value === undefined || draft !== null) return
|
|
94
|
+
setDraft(draftFromValue(value))
|
|
95
|
+
}, [value, draft])
|
|
96
|
+
|
|
97
|
+
const dirty = useMemo(
|
|
98
|
+
() =>
|
|
99
|
+
value !== undefined &&
|
|
100
|
+
draft !== null &&
|
|
101
|
+
JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))),
|
|
102
|
+
[value, draft],
|
|
103
|
+
)
|
|
104
|
+
const onSave = (): void => {
|
|
105
|
+
if (draft === null) return
|
|
106
|
+
setSaving(true)
|
|
107
|
+
const revision = scope.getSnapshot().revision
|
|
108
|
+
api
|
|
109
|
+
.update(toPatch(draft) as unknown as Record<string, unknown>, revision)
|
|
110
|
+
.then(async () => {
|
|
111
|
+
await scope.load()
|
|
112
|
+
const next = scope.getSnapshot().value as ConfigValue | undefined
|
|
113
|
+
if (next !== undefined) setDraft(draftFromValue(next))
|
|
114
|
+
setEpoch((current) => current + 1)
|
|
115
|
+
setStatus({ kind: 'ok', text: t('saveOk') })
|
|
116
|
+
})
|
|
117
|
+
.catch((error: unknown) => {
|
|
118
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
119
|
+
setStatus({ kind: 'error', text: `${t('saveFailed')}${message}` })
|
|
120
|
+
})
|
|
121
|
+
.finally(() => setSaving(false))
|
|
122
|
+
}
|
|
123
|
+
const onDiscard = (): void => {
|
|
124
|
+
if (value === undefined) return
|
|
125
|
+
setDraft(draftFromValue(value))
|
|
126
|
+
setEpoch((current) => current + 1)
|
|
127
|
+
setStatus(IDLE_STATUS)
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
draft,
|
|
131
|
+
setDraft,
|
|
132
|
+
dirty,
|
|
133
|
+
saving,
|
|
134
|
+
status,
|
|
135
|
+
idle: () => setStatus(IDLE_STATUS),
|
|
136
|
+
epoch,
|
|
137
|
+
onSave,
|
|
138
|
+
onDiscard,
|
|
139
|
+
}
|
|
67
140
|
}
|
|
68
141
|
|
|
69
142
|
/** 路由级草稿改写:改字段 / 新增 route / 删除 route(返回新草稿,不改入参)。 */
|
|
@@ -82,26 +155,17 @@ function withoutRoute(draft: Draft, route: string): Draft {
|
|
|
82
155
|
return { ...draft, providers: next }
|
|
83
156
|
}
|
|
84
157
|
|
|
85
|
-
/**
|
|
158
|
+
/** 卡片动作:放弃、保存(模型目录已唯一来自 pi-ai,无可拉取项)。 */
|
|
86
159
|
function cardActions(options: {
|
|
87
160
|
t: Translate
|
|
88
161
|
dirty: boolean
|
|
89
|
-
invalid: boolean
|
|
90
162
|
saving: boolean
|
|
91
|
-
refreshing: boolean
|
|
92
163
|
disabled: boolean
|
|
93
|
-
onRefresh(): void
|
|
94
164
|
onDiscard(): void
|
|
95
165
|
onSave(): void
|
|
96
166
|
}): CardAction[] {
|
|
97
167
|
const { t } = options
|
|
98
168
|
return [
|
|
99
|
-
{
|
|
100
|
-
key: 'refresh',
|
|
101
|
-
label: t(options.refreshing ? 'refreshingCatalog' : 'refreshCatalog'),
|
|
102
|
-
disabled: options.disabled || options.refreshing,
|
|
103
|
-
onClick: options.onRefresh,
|
|
104
|
-
},
|
|
105
169
|
{
|
|
106
170
|
key: 'discard',
|
|
107
171
|
label: t('discard'),
|
|
@@ -112,12 +176,18 @@ function cardActions(options: {
|
|
|
112
176
|
key: 'save',
|
|
113
177
|
label: t(options.saving ? 'saving' : 'save'),
|
|
114
178
|
variant: 'primary',
|
|
115
|
-
disabled: !options.dirty || options.
|
|
179
|
+
disabled: !options.dirty || options.saving || options.disabled,
|
|
116
180
|
onClick: options.onSave,
|
|
117
181
|
},
|
|
118
182
|
]
|
|
119
183
|
}
|
|
120
184
|
|
|
185
|
+
/** 打开浏览器时的目标 route 选择:优先当前目标,否则第一个候选。 */
|
|
186
|
+
function pickTarget(targets: readonly string[], preferred: string): string {
|
|
187
|
+
if (preferred !== '' && targets.includes(preferred)) return preferred
|
|
188
|
+
return targets[0] ?? ''
|
|
189
|
+
}
|
|
190
|
+
|
|
121
191
|
export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
122
192
|
const { t, scope, api } = props
|
|
123
193
|
const snapshot = useSyncExternalStore(
|
|
@@ -125,95 +195,48 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
125
195
|
() => scope.getSnapshot(),
|
|
126
196
|
)
|
|
127
197
|
const value = snapshot.value as ConfigValue | undefined
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const [
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (value === undefined || draft !== null) return
|
|
140
|
-
setDraft(draftFromValue(value))
|
|
141
|
-
}, [value, draft])
|
|
142
|
-
|
|
143
|
-
const dirty = useMemo(
|
|
144
|
-
() =>
|
|
145
|
-
value !== undefined &&
|
|
146
|
-
draft !== null &&
|
|
147
|
-
JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))),
|
|
148
|
-
[value, draft],
|
|
149
|
-
)
|
|
150
|
-
const invalid = useMemo(() => {
|
|
151
|
-
if (draft === null) return false
|
|
152
|
-
return (
|
|
153
|
-
!numTextOk(draft.catalogRefreshHours) ||
|
|
154
|
-
Object.values(draft.providers).some((provider) =>
|
|
155
|
-
provider.models.some((model) => model.id.trim() === ''),
|
|
156
|
-
)
|
|
157
|
-
)
|
|
158
|
-
}, [draft])
|
|
198
|
+
const { draft, setDraft, dirty, saving, status, idle, epoch, onSave, onDiscard } = useCardDraft({
|
|
199
|
+
value,
|
|
200
|
+
scope,
|
|
201
|
+
api,
|
|
202
|
+
t,
|
|
203
|
+
})
|
|
204
|
+
const [browserOpen, setBrowserOpen] = useState(false)
|
|
205
|
+
const [browserTarget, setBrowserTarget] = useState('')
|
|
206
|
+
const [openRoutes, setOpenRoutes] = useState<Record<string, boolean>>({})
|
|
207
|
+
const { meta, error: metaError } = useCatalogMeta()
|
|
159
208
|
|
|
160
209
|
// 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
|
|
161
210
|
if (props.view === 'summary') return t('summaryLine')
|
|
162
211
|
|
|
163
|
-
// 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
|
|
164
|
-
const variant = cardVariantFor(props.view)
|
|
165
|
-
|
|
166
212
|
if (value === undefined || draft === null) {
|
|
167
|
-
return <CardLoading prefix="lpc"
|
|
213
|
+
return <CardLoading prefix="lpc" text={t('loading')} />
|
|
168
214
|
}
|
|
169
215
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
216
|
+
const apis: ApiIndex = meta?.apis ?? {}
|
|
217
|
+
const targets = targetRoutes(draft.providers)
|
|
218
|
+
const target = pickTarget(targets, browserTarget)
|
|
219
|
+
const openBrowserFor = (route: string): void => {
|
|
220
|
+
setBrowserTarget(route)
|
|
221
|
+
setBrowserOpen(true)
|
|
173
222
|
}
|
|
174
223
|
const onAddRoute = (key: string): void => {
|
|
175
224
|
setDraft(withNewRoute(draft, key))
|
|
176
|
-
|
|
225
|
+
setOpenRoutes((prev) => ({ ...prev, [key]: true }))
|
|
226
|
+
setBrowserTarget((prev) => (prev === '' ? key : prev))
|
|
227
|
+
idle()
|
|
177
228
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const next = scope.getSnapshot().value as ConfigValue | undefined
|
|
190
|
-
if (next !== undefined) setDraft(draftFromValue(next))
|
|
191
|
-
setEpoch((value) => value + 1)
|
|
192
|
-
setStatus({ kind: 'ok', text: t('saveOk') })
|
|
193
|
-
})
|
|
194
|
-
.catch((error: unknown) => {
|
|
195
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
196
|
-
setStatus({ kind: 'error', text: `${t('saveFailed')}${message}` })
|
|
197
|
-
})
|
|
198
|
-
.finally(() => setSaving(false))
|
|
199
|
-
}
|
|
200
|
-
const onDiscard = (): void => {
|
|
201
|
-
setDraft(draftFromValue(value))
|
|
202
|
-
setEpoch((value) => value + 1)
|
|
203
|
-
setStatus(IDLE_STATUS)
|
|
204
|
-
}
|
|
205
|
-
const onRefreshCatalog = (): void => {
|
|
206
|
-
setRefreshing(true)
|
|
207
|
-
refreshCatalog()
|
|
208
|
-
.then((result) => {
|
|
209
|
-
setModelsDevStatus(result.status)
|
|
210
|
-
setStatus({ kind: 'ok', text: t('refreshOk') })
|
|
211
|
-
})
|
|
212
|
-
.catch((error: unknown) => {
|
|
213
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
214
|
-
setStatus({ kind: 'error', text: `${t('refreshFailed')}${message}` })
|
|
215
|
-
})
|
|
216
|
-
.finally(() => setRefreshing(false))
|
|
229
|
+
/**
|
|
230
|
+
* 目录一键添加:把 `{id, extends: 'provider/model'}` 写进目标 route 草稿
|
|
231
|
+
* (不预填 name/容量——继承才能跟随 pi-ai 目录升级),并展开该 route 给出反馈。
|
|
232
|
+
*/
|
|
233
|
+
const onAddModel = (route: string, model: WireModelInfo): boolean => {
|
|
234
|
+
const result = addModelEntry(draft, route, model)
|
|
235
|
+
if (!result.added) return false
|
|
236
|
+
setDraft(result.draft)
|
|
237
|
+
setOpenRoutes((prev) => ({ ...prev, [route]: true }))
|
|
238
|
+
idle()
|
|
239
|
+
return true
|
|
217
240
|
}
|
|
218
241
|
|
|
219
242
|
const disabled = !snapshot.writable
|
|
@@ -222,45 +245,72 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
222
245
|
prefix="lpc"
|
|
223
246
|
title={t('title')}
|
|
224
247
|
description={t('description')}
|
|
225
|
-
open={open}
|
|
226
|
-
onToggle={setOpen}
|
|
227
|
-
variant={variant}
|
|
228
248
|
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
229
249
|
dirty={dirty}
|
|
230
250
|
dirtyLabel={t('unsaved')}
|
|
231
251
|
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
232
252
|
status={status}
|
|
233
|
-
actions={cardActions({
|
|
234
|
-
t,
|
|
235
|
-
dirty,
|
|
236
|
-
invalid,
|
|
237
|
-
saving,
|
|
238
|
-
refreshing,
|
|
239
|
-
disabled,
|
|
240
|
-
onRefresh: onRefreshCatalog,
|
|
241
|
-
onDiscard,
|
|
242
|
-
onSave,
|
|
243
|
-
})}
|
|
253
|
+
actions={cardActions({ t, dirty, saving, disabled, onDiscard, onSave })}
|
|
244
254
|
>
|
|
245
255
|
<RootFields
|
|
246
256
|
t={t}
|
|
247
257
|
draft={draft}
|
|
248
258
|
disabled={disabled}
|
|
249
|
-
|
|
250
|
-
|
|
259
|
+
kit={meta?.kit ?? null}
|
|
260
|
+
kitError={metaError}
|
|
251
261
|
onEdit={(patch) => {
|
|
252
262
|
setDraft({ ...draft, ...patch })
|
|
253
|
-
|
|
263
|
+
idle()
|
|
254
264
|
}}
|
|
255
265
|
/>
|
|
266
|
+
<CollapseSection
|
|
267
|
+
id="lpc-catalog-browser"
|
|
268
|
+
title={t('browserGroup')}
|
|
269
|
+
defaultOpen={false}
|
|
270
|
+
open={browserOpen}
|
|
271
|
+
onToggle={() => setBrowserOpen((open) => !open)}
|
|
272
|
+
meta={meta === null ? '' : `${meta.kit.catalog.models}`}
|
|
273
|
+
>
|
|
274
|
+
<CatalogBrowser
|
|
275
|
+
t={t}
|
|
276
|
+
targets={targets}
|
|
277
|
+
apis={apis}
|
|
278
|
+
providers={meta?.providers ?? []}
|
|
279
|
+
target={target}
|
|
280
|
+
onTargetChange={setBrowserTarget}
|
|
281
|
+
providerOf={(route) => draft.providers[route]}
|
|
282
|
+
routeApiOf={(route) => {
|
|
283
|
+
const provider = draft.providers[route]
|
|
284
|
+
return provider === undefined ? undefined : routeApiFacts(provider, apis).api
|
|
285
|
+
}}
|
|
286
|
+
onAdd={onAddModel}
|
|
287
|
+
disabled={disabled}
|
|
288
|
+
{...(metaError === '' ? {} : { metaError })}
|
|
289
|
+
/>
|
|
290
|
+
</CollapseSection>
|
|
256
291
|
<ProvidersSection
|
|
257
292
|
providers={draft.providers}
|
|
293
|
+
openRoutes={openRoutes}
|
|
258
294
|
epoch={epoch}
|
|
259
295
|
disabled={disabled}
|
|
260
296
|
t={t}
|
|
261
297
|
onAddRoute={onAddRoute}
|
|
262
|
-
onRemoveRoute={
|
|
263
|
-
|
|
298
|
+
onRemoveRoute={(route) => {
|
|
299
|
+
setDraft(withoutRoute(draft, route))
|
|
300
|
+
idle()
|
|
301
|
+
}}
|
|
302
|
+
onPatchProvider={(route, patch) => {
|
|
303
|
+
setDraft(withProviderPatch(draft, route, patch))
|
|
304
|
+
idle()
|
|
305
|
+
}}
|
|
306
|
+
onToggleRoute={(route, open) => setOpenRoutes((prev) => ({ ...prev, [route]: open }))}
|
|
307
|
+
onCollapseAll={() => setOpenRoutes({})}
|
|
308
|
+
onExpandAll={() =>
|
|
309
|
+
setOpenRoutes(
|
|
310
|
+
Object.fromEntries(Object.keys(draft.providers).map((route) => [route, true])),
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
onAddFromCatalog={openBrowserFor}
|
|
264
314
|
/>
|
|
265
315
|
</CardChrome>
|
|
266
316
|
)
|
package/src/client/client.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 浏览器半入口:注册 locale 字典 + 注册「LLM 路由」卡片
|
|
3
|
-
* (injectPluginConfigCard
|
|
4
|
-
*
|
|
3
|
+
* (injectPluginConfigCard 两槽位:插件页 plugins.row.config /
|
|
4
|
+
* plugins.bundle.config)。
|
|
5
5
|
* 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
|
|
6
6
|
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
7
7
|
*
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* scope 与基础控件走 @dsh-plus/shared/client 套件(收编自本插件原实现)。
|
|
10
10
|
* 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
|
|
11
11
|
* 已移除;0.1.7 起 settingsScope 服务亦删除(configForms 取代)——插件卡片自始
|
|
12
|
-
* 直连 remote.settings,见 @dsh-plus/shared/client scope.ts
|
|
13
|
-
*
|
|
12
|
+
* 直连 remote.settings,见 @dsh-plus/shared/client scope.ts);自定义端点只剩
|
|
13
|
+
* `/catalog`(api.ts:运行期套件事实 + pi-ai 内置目录搜索,浏览器半读不到 pi-ai 包)。
|
|
14
14
|
* 卡片 key/rowId 约定见 @dsh-plus/shared/client/config-slots。
|
|
15
15
|
* @module @dsh-plus/llm-pi/client
|
|
16
16
|
*/
|
package/src/client/constants.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts
|
|
2
|
+
* 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts 逐字对齐。
|
|
3
3
|
* 浏览器半不能 import 服务端模块(tsdown 只打包 client 侧入口),
|
|
4
4
|
* 改动服务端这些常量时必须同步本文件。
|
|
5
|
+
*
|
|
6
|
+
* 两个集合改为**服务端下发**(与服务端校验同源,浏览器半不再手抄):
|
|
7
|
+
* - 线协议:取自已装官方适配器的 supportedProtocols()(`installProtocols`);
|
|
8
|
+
* - compat 字段表:取自官方 bundle + Config schema 推导(`installCompatFields`)。
|
|
5
9
|
* @module llm-pi/client/constants
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
|
-
/**
|
|
9
|
-
export const
|
|
12
|
+
/** 协议兜底集合(来源:config.ts FALLBACK_PROTOCOLS);服务端下发前/失败时使用。 */
|
|
13
|
+
export const FALLBACK_PROTOCOLS = [
|
|
10
14
|
'openai-completions',
|
|
11
15
|
'openai-responses',
|
|
12
16
|
'anthropic-messages',
|
|
@@ -29,15 +33,31 @@ export const BUDGET_KEYS = ['minimal', 'low', 'medium', 'high'] as const
|
|
|
29
33
|
|
|
30
34
|
export type CompatValue = 'boolean' | 'integer' | 'number' | 'object' | readonly string[]
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 生效协议集合:由服务端 /catalog 下发(服务端与官方适配器同源推导),
|
|
38
|
+
* 未拿到前用 FALLBACK_PROTOCOLS。
|
|
39
|
+
*/
|
|
40
|
+
let protocols: string[] = [...FALLBACK_PROTOCOLS]
|
|
41
|
+
|
|
42
|
+
/** 安装服务端下发的协议集合(card 挂载时调用)。 */
|
|
43
|
+
export function installProtocols(list: readonly string[]): void {
|
|
44
|
+
if (list.length > 0) protocols = [...list]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 当前可选协议(顺序即官方表顺序)。 */
|
|
48
|
+
export function protocolOptions(): readonly string[] {
|
|
49
|
+
return protocols
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
/**
|
|
33
53
|
* 逐协议 compat 字段表:**服务端推导结果经 /catalog 下发**,浏览器半不再手抄
|
|
34
|
-
* (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见
|
|
54
|
+
* (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 official-surface.ts)。
|
|
35
55
|
* 未拿到服务端表时用 EMPTY(UI 只渲染提示行),不放内置副本兜底——宁可少画
|
|
36
56
|
* 控件,也不给出可能与官方不符的字段集。
|
|
37
57
|
*/
|
|
38
58
|
let compatTable: Record<string, Record<string, CompatValue>> = {}
|
|
39
59
|
|
|
40
|
-
/** 安装服务端下发的字段表(card
|
|
60
|
+
/** 安装服务端下发的字段表(card 挂载时调用)。 */
|
|
41
61
|
export function installCompatFields(table: Record<string, Record<string, CompatValue>>): void {
|
|
42
62
|
compatTable = table
|
|
43
63
|
}
|