@dsh-plus/llm-pi 0.1.42 → 0.1.44

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.
@@ -69,9 +69,6 @@ export interface ProviderDraft {
69
69
 
70
70
  export interface Draft {
71
71
  enabled: boolean
72
- catalogUrl: string
73
- catalogRefreshHours: string
74
- catalogProxy: string
75
72
  providers: Record<string, ProviderDraft>
76
73
  }
77
74
 
@@ -355,9 +352,6 @@ export function emptyModelDraft(): ModelDraft {
355
352
  export function draftFromValue(value: ConfigValue): Draft {
356
353
  return {
357
354
  enabled: value.enabled,
358
- catalogUrl: value.catalogUrl,
359
- catalogRefreshHours: String(value.catalogRefreshHours),
360
- catalogProxy: value.catalogProxy,
361
355
  providers: Object.fromEntries(
362
356
  Object.entries(value.providers).map(([route, provider]) => [
363
357
  route,
@@ -369,17 +363,44 @@ export function draftFromValue(value: ConfigValue): Draft {
369
363
 
370
364
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
371
365
  export function toPatch(draft: Draft): ConfigPatch {
372
- const refreshHours = toNum(draft.catalogRefreshHours)
373
366
  return {
374
367
  enabled: draft.enabled,
375
- catalogUrl: draft.catalogUrl.trim(),
376
- // 保存动作在文本非法时不可用(card.tsx 的 invalid 门控),故写入路径必为合法数字;
377
- // dirty 比较路径可能拿到非法文本的 undefined(JSON.stringify 时同键被丢弃),
378
- // 此处仅把"保存前已校验"的运行期事实带到类型层,取值不变。
379
- catalogRefreshHours: refreshHours as unknown as number,
380
- catalogProxy: draft.catalogProxy.trim(),
381
368
  providers: Object.fromEntries(
382
369
  Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]),
383
370
  ),
384
371
  }
385
372
  }
373
+
374
+ /** 目录一键添加的结果:added=false 表示该 route 已有同 id 条目(原样返回草稿)。 */
375
+ export interface AddModelResult {
376
+ draft: Draft
377
+ added: boolean
378
+ }
379
+
380
+ /**
381
+ * 把一个内置目录模型加进目标 route:只写 `id` + `extends`(**不预填**
382
+ * name/contextWindow/maxTokens——继承才能跟随 pi-ai 目录升级,预填会把值冻住)。
383
+ * 同 id 已存在时原样返回(added=false),不产生重复条目。
384
+ */
385
+ export function addModelEntry(
386
+ draft: Draft,
387
+ route: string,
388
+ model: { id: string; path: string },
389
+ ): AddModelResult {
390
+ const provider = draft.providers[route]
391
+ if (provider === undefined) return { draft, added: false }
392
+ if (provider.models.some((entry) => entry.id.trim() === model.id)) {
393
+ return { draft, added: false }
394
+ }
395
+ const entry: ModelDraft = { ...emptyModelDraft(), id: model.id, extends: model.path }
396
+ return {
397
+ draft: {
398
+ ...draft,
399
+ providers: {
400
+ ...draft.providers,
401
+ [route]: { ...provider, models: [...provider.models, entry] },
402
+ },
403
+ },
404
+ added: true,
405
+ }
406
+ }
@@ -13,18 +13,30 @@ import type { HeaderPair } from './draft.ts'
13
13
  /**
14
14
  * 可折叠小节:复杂项(compat/retryPolicy 等)默认折叠,避免卡片过长;
15
15
  * 基础项(baseURL 等)保持展开。标题按钮切换展开态。
16
+ * 传 `open` 即为**受控**(由外部状态决定展开,如卡片级"全部收起"与
17
+ * 「从内置目录添加」联动展开目标 route);不传则内部自管。
16
18
  */
17
19
  export interface CollapseSectionProps {
18
20
  id: string
19
21
  title: string
20
- /** 默认展开态;复杂项传 false(默认折叠)。 */
22
+ /** 默认展开态;复杂项传 false(默认折叠)。受控模式下仅作初值语义的说明。 */
21
23
  defaultOpen: boolean
24
+ /** 受控展开态;传了就完全由外部决定。 */
25
+ open?: boolean
26
+ onToggle?(): void
27
+ /** 标题右侧的计数/状态副标题(如 compat「已设 3/19」)。 */
28
+ meta?: string
22
29
  disabled?: boolean
23
30
  children: ReactNode
24
31
  }
25
32
 
26
33
  export function CollapseSection(props: CollapseSectionProps): ReactElement {
27
- const [open, setOpen] = useState(props.defaultOpen)
34
+ const [innerOpen, setInnerOpen] = useState(props.defaultOpen)
35
+ const open = props.open ?? innerOpen
36
+ const toggle = (): void => {
37
+ if (props.onToggle !== undefined) props.onToggle()
38
+ else setInnerOpen((value) => !value)
39
+ }
28
40
  return (
29
41
  <div className="lpc-collapse">
30
42
  <button
@@ -32,10 +44,13 @@ export function CollapseSection(props: CollapseSectionProps): ReactElement {
32
44
  className="lpc-collapseHead"
33
45
  id={props.id}
34
46
  aria-expanded={open}
35
- onClick={() => setOpen(!open)}
47
+ onClick={toggle}
36
48
  >
37
49
  <ChevronDownIcon className={`lpc-chevron${open ? ' lpc-chevronOpen' : ''}`} />
38
50
  <span className="lpc-collapseTitle">{props.title}</span>
51
+ {props.meta !== undefined && props.meta !== '' ? (
52
+ <span className="lpc-collapseMeta">{props.meta}</span>
53
+ ) : null}
39
54
  </button>
40
55
  {open ? <div className="lpc-collapseBody">{props.children}</div> : null}
41
56
  </div>
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
3
3
  * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
4
+ * 服务端推导出来的诊断/提示(compat problem、版本区间提示、降级诊断)按原文展示,
5
+ * 不做二次翻译——它们是事实描述,且与日志同一份文本。
4
6
  * @module llm-pi/client/i18n
5
7
  */
6
8
  import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
@@ -9,29 +11,74 @@ export const NS = 'dsh-plus-llm-pi'
9
11
 
10
12
  const ownZh = {
11
13
  title: 'LLM 路由(llm-pi)',
12
- description: '自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。',
13
- summaryLine: '自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。',
14
+ description:
15
+ '自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。',
16
+ summaryLine:
17
+ '自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。',
14
18
  enabled: '启用插件',
15
- catalogUrl: 'models.dev 目录数据端点',
16
- catalogUrlHint: '快照数据源;一般无需修改。',
17
- catalogRefreshHours: '目录刷新间隔(小时)',
18
- catalogRefreshHoursHint: '0 = 不自动拉取(可手动拉取或读已有缓存);>0 = 每 N 小时自动拉取。',
19
- catalogProxy: '拉取代理地址',
20
- catalogProxyHint: 'HTTP 代理(如 http://127.0.0.1:7890);留空直连。',
21
- kitSource: '模块来源',
22
- modelsDevStatus: 'models.dev 快照',
23
- modelsDevEmpty: '未拉取(无缓存数据;可手动拉取)',
24
- modelsDevStatusLine: '已加载',
25
- modelsDevError: '加载失败:',
26
- refreshCatalog: '手动拉取',
27
- refreshingCatalog: '拉取中…',
28
- refreshOk: '目录已拉取。',
29
- refreshFailed: '拉取失败:',
19
+ runtimeGroup: '运行期套件',
20
+ runtimeSource: '来源',
21
+ runtimeRoot: '安装树',
22
+ runtimeVersions: '生效版本',
23
+ runtimeCatalog: '内置模型目录',
24
+ runtimeProtocols: '可服务协议',
25
+ runtimeCompat: 'compat 门控表',
26
+ runtimeDiagnostics: '诊断',
27
+ runtimeVersionUnknown: '未知',
28
+ runtimeCatalogUnit: '个 provider /',
29
+ runtimeCatalogModels: '个模型',
30
+ runtimeCatalogGenerated: '数据生成于',
31
+ runtimeNotice: '提示',
32
+ runtimeDerived: '现场推导',
33
+ runtimeFallback: '回退快照',
34
+ browserGroup: '内置模型目录',
35
+ browserHint:
36
+ '浏览 pi-ai 内置目录(随已装 dsh 的 pi-ai 版本自动更新):模糊搜索、按供应商/协议/能力筛选;复制路径 id,或一键把「模型 id + 继承源」加到某个 route。',
37
+ browserSearch: '模糊搜索',
38
+ browserSearchPlaceholder: '模型 id / 显示名 / provider,多词用空格',
39
+ browserProvider: '供应商',
40
+ browserApi: '协议',
41
+ browserAll: '全部',
42
+ browserOnlyServable: '仅本插件可服务',
43
+ browserReasoning: '推理',
44
+ browserImage: '图片输入',
45
+ browserTarget: '目标 route',
46
+ browserNoTarget:
47
+ '还没有可添加的 route:先在上方新增一个 route(adapter: deepseek 的路由不适用 pi-ai 继承)。',
48
+ browserMatched: '命中',
49
+ browserHiddenServable: '另有协议不受支持的模型',
50
+ browserHiddenUnit: '个已隐藏',
51
+ browserLoadMore: '加载更多',
52
+ browserLoading: '目录加载中…',
53
+ browserEmpty: '没有命中的模型。',
54
+ browserFailed: '目录加载失败:',
55
+ browserAdd: '添加',
56
+ browserAdded: '已添加',
57
+ browserCopy: '复制路径 id',
58
+ browserCopied: '已复制',
59
+ browserDetails: '参数明细',
60
+ browserCompat: 'compat',
61
+ browserExtra: '其余参数',
62
+ browserThinkMap: '思考档位',
63
+ browserInputText: '文本',
64
+ browserInputImage: '图片',
65
+ browserContextShort: '上下文',
66
+ browserMaxShort: '输出',
67
+ browserAddedTo: '已添加到 route',
68
+ browserTargetProtocol: '当前协议',
69
+ browserNoProtocol: '协议待定',
70
+ blockUnsupported: '该协议本插件无法服务(不可添加)',
71
+ blockDuplicate: '该 route 已有同 id 模型',
72
+ blockConflict: '与目标 route 的协议不一致',
73
+ blockMixed: '目标 route 已混用多个协议,请先修正',
74
+ blockNoRoute: '目标 route 不存在',
30
75
  advancedGroup: '高级设置(retryPolicy / compat)',
31
76
  extraFields: '其余字段(JSON)',
32
77
  extraFieldsHint:
33
78
  '表单未覆盖的 wire 字段原样往返:如 adapter=deepseek 的 thinking / reasoningEffort、Files API 与图片配额等。对象整体合并进该 route(模型级同 key 覆盖 route 级),非法 JSON 不会提交。',
34
79
  providersGroup: 'Provider 路由',
80
+ providersCollapseAll: '全部收起',
81
+ providersExpandAll: '全部展开',
35
82
  addRoute: '新增 route',
36
83
  addRoutePlaceholder: '新 route 键名(如 my-llm)',
37
84
  routeEmpty: 'route 键名不能为空。',
@@ -39,7 +86,8 @@ const ownZh = {
39
86
  deleteRoute: '删除 route',
40
87
  providerFields: '基础字段',
41
88
  extends: '继承内置 provider',
42
- extendsHint: '如 openai、anthropic;提供 api/baseURL 默认值与模型查找源。',
89
+ extendsHint:
90
+ '如 openai、anthropic(候选见「内置模型目录」);提供 api/baseURL 默认值与模型查找源。',
43
91
  displayName: '显示名',
44
92
  api: '线协议',
45
93
  baseURL: '端点 URL',
@@ -70,55 +118,104 @@ const ownZh = {
70
118
  compatGroup: 'Compat 覆盖',
71
119
  compatApiHint: 'api 未设置时暂按 openai-completions 字段组渲染;保存时后端按实际协议校验。',
72
120
  compatUnset: '未设置',
121
+ compatSetCount: '已设',
122
+ compatFilter: '过滤字段',
73
123
  modelsGroup: '模型目录',
74
124
  addModel: '添加模型',
75
- deleteModel: '删除',
125
+ addFromCatalog: '从内置目录添加',
126
+ modelsCollapseAll: '全部收起',
127
+ modelsExpandAll: '全部展开',
128
+ modelsFilter: '过滤模型',
129
+ modelsFilterPlaceholder: '按 id / 继承源筛选',
130
+ modelsHidden: '已按筛选隐藏',
76
131
  modelRow: '模型',
132
+ modelUntitled: '(未填 id)',
133
+ deleteModel: '删除',
77
134
  modelId: '模型 id',
78
135
  modelIdHint: '发送给 provider 的标识,必填。',
79
136
  modelIdRequired: '模型 id 必填。',
80
137
  modelExtends: '继承源',
81
- modelExtendsHint: '"provider/model" 或裸 model id(候选来自下方目录)。',
138
+ modelExtendsHint: '"provider/model" 或裸 model id;候选见「内置模型目录」。',
82
139
  modelName: '显示名',
83
140
  contextWindow: '上下文容量',
84
141
  maxTokens: '输出上限',
85
142
  input: '输入模态',
86
143
  reasoningEfforts: 'reasoningEfforts',
87
144
  nonReasoning: '非推理模型',
88
- catalogSource: '候选来源',
89
- catalogProvider: '候选 provider',
90
- catalogLoading: '目录加载中…',
91
- catalogFailed: '目录加载失败。',
92
145
  saveOk: '已保存。',
93
146
  saveFailed: '保存失败:',
94
147
  } as const
95
148
 
96
149
  const ownEn = {
97
150
  title: 'LLM routes (llm-pi)',
98
- description: 'Custom LLM routes: protocol, compat, model catalog and models.dev fallback.',
99
- summaryLine: 'Custom LLM routes: protocol, compat, model catalog and models.dev fallback.',
151
+ description:
152
+ 'Custom LLM routes: protocol, compat, model inheritance; the model catalog comes solely from the pi-ai built-in catalog (following the installed dsh).',
153
+ summaryLine:
154
+ 'Custom LLM routes: protocol, compat, model inheritance; the model catalog comes solely from the pi-ai built-in catalog.',
100
155
  enabled: 'Enable plugin',
101
- catalogUrl: 'models.dev catalog endpoint',
102
- catalogUrlHint: 'Snapshot data source; usually no change needed.',
103
- catalogRefreshHours: 'Catalog refresh (hours)',
104
- catalogRefreshHoursHint:
105
- '0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.',
106
- catalogProxy: 'Fetch proxy',
107
- catalogProxyHint: 'HTTP proxy (e.g. http://127.0.0.1:7890); leave empty for direct.',
108
- kitSource: 'Module source',
109
- modelsDevStatus: 'models.dev snapshot',
110
- modelsDevEmpty: 'Not fetched (no cached data; use manual refresh)',
111
- modelsDevStatusLine: 'Loaded',
112
- modelsDevError: 'Load failed: ',
113
- refreshCatalog: 'Refresh now',
114
- refreshingCatalog: 'Refreshing…',
115
- refreshOk: 'Catalog refreshed.',
116
- refreshFailed: 'Refresh failed: ',
156
+ runtimeGroup: 'Runtime kit',
157
+ runtimeSource: 'Source',
158
+ runtimeRoot: 'Install tree',
159
+ runtimeVersions: 'Effective versions',
160
+ runtimeCatalog: 'Built-in catalog',
161
+ runtimeProtocols: 'Servable protocols',
162
+ runtimeCompat: 'compat gates',
163
+ runtimeDiagnostics: 'Diagnostics',
164
+ runtimeVersionUnknown: 'unknown',
165
+ runtimeCatalogUnit: 'providers /',
166
+ runtimeCatalogModels: 'models',
167
+ runtimeCatalogGenerated: 'data generated at',
168
+ runtimeNotice: 'Notice',
169
+ runtimeDerived: 'derived live',
170
+ runtimeFallback: 'fallback snapshot',
171
+ browserGroup: 'Built-in model catalog',
172
+ browserHint:
173
+ 'Browse the pi-ai built-in catalog (follows the installed dsh): fuzzy search, filter by provider/protocol/capability; copy the path id, or add "model id + inheritance source" to a route in one click.',
174
+ browserSearch: 'Fuzzy search',
175
+ browserSearchPlaceholder: 'model id / display name / provider; space-separated terms',
176
+ browserProvider: 'Provider',
177
+ browserApi: 'Protocol',
178
+ browserAll: 'All',
179
+ browserOnlyServable: 'Servable only',
180
+ browserReasoning: 'Reasoning',
181
+ browserImage: 'Image input',
182
+ browserTarget: 'Target route',
183
+ browserNoTarget:
184
+ 'No route to add to yet: create one above (adapter: deepseek routes do not use pi-ai inheritance).',
185
+ browserMatched: 'Matched',
186
+ browserHiddenServable: 'Unsupported-protocol models',
187
+ browserHiddenUnit: 'hidden',
188
+ browserLoadMore: 'Load more',
189
+ browserLoading: 'Loading catalog…',
190
+ browserEmpty: 'No matching model.',
191
+ browserFailed: 'Catalog request failed: ',
192
+ browserAdd: 'Add',
193
+ browserAdded: 'Added',
194
+ browserCopy: 'Copy path id',
195
+ browserCopied: 'Copied',
196
+ browserDetails: 'Parameters',
197
+ browserCompat: 'compat',
198
+ browserExtra: 'Other parameters',
199
+ browserThinkMap: 'Thinking levels',
200
+ browserInputText: 'text',
201
+ browserInputImage: 'image',
202
+ browserContextShort: 'context',
203
+ browserMaxShort: 'output',
204
+ browserAddedTo: 'Added to route',
205
+ browserTargetProtocol: 'protocol',
206
+ browserNoProtocol: 'protocol undecided',
207
+ blockUnsupported: 'This protocol is not servable by this plugin',
208
+ blockDuplicate: 'This route already has a model with the same id',
209
+ blockConflict: 'Protocol differs from the target route',
210
+ blockMixed: 'The target route already mixes protocols — fix it first',
211
+ blockNoRoute: 'Target route does not exist',
117
212
  advancedGroup: 'Advanced (retryPolicy / compat)',
118
213
  extraFields: 'Other fields (JSON)',
119
214
  extraFieldsHint:
120
215
  'Wire fields the form does not cover, round-tripped verbatim: e.g. thinking / reasoningEffort and the Files API & image quotas for adapter=deepseek. The object merges into this route (model-level keys win); invalid JSON is not submitted.',
121
216
  providersGroup: 'Provider routes',
217
+ providersCollapseAll: 'Collapse all',
218
+ providersExpandAll: 'Expand all',
122
219
  addRoute: 'Add route',
123
220
  addRoutePlaceholder: 'New route key (e.g. my-llm)',
124
221
  routeEmpty: 'Route key must not be empty.',
@@ -126,7 +223,8 @@ const ownEn = {
126
223
  deleteRoute: 'Delete route',
127
224
  providerFields: 'Basic fields',
128
225
  extends: 'Inherit built-in provider',
129
- extendsHint: 'e.g. openai, anthropic; provides api/baseURL defaults and the model lookup source.',
226
+ extendsHint:
227
+ 'e.g. openai, anthropic (see the built-in catalog below); provides api/baseURL defaults and the model lookup source.',
130
228
  displayName: 'Display name',
131
229
  api: 'Wire protocol',
132
230
  baseURL: 'Base URL',
@@ -158,25 +256,31 @@ const ownEn = {
158
256
  compatApiHint:
159
257
  'When api is unset, fields render per openai-completions; the backend validates per the effective protocol.',
160
258
  compatUnset: 'Unset',
259
+ compatSetCount: 'set',
260
+ compatFilter: 'Filter fields',
161
261
  modelsGroup: 'Model catalog',
162
262
  addModel: 'Add model',
163
- deleteModel: 'Delete',
263
+ addFromCatalog: 'Add from catalog',
264
+ modelsCollapseAll: 'Collapse all',
265
+ modelsExpandAll: 'Expand all',
266
+ modelsFilter: 'Filter models',
267
+ modelsFilterPlaceholder: 'filter by id / inheritance source',
268
+ modelsHidden: 'hidden by filter',
164
269
  modelRow: 'Model',
270
+ modelUntitled: '(no id yet)',
271
+ deleteModel: 'Delete',
165
272
  modelId: 'Model id',
166
273
  modelIdHint: 'The identifier sent to the provider; required.',
167
274
  modelIdRequired: 'Model id is required.',
168
275
  modelExtends: 'Inherits from',
169
- modelExtendsHint: '"provider/model" or a bare model id (candidates from the catalog below).',
276
+ modelExtendsHint:
277
+ '"provider/model" or a bare model id; candidates in the built-in catalog above.',
170
278
  modelName: 'Display name',
171
279
  contextWindow: 'Context window',
172
280
  maxTokens: 'Max tokens',
173
281
  input: 'Input modalities',
174
282
  reasoningEfforts: 'reasoningEfforts',
175
283
  nonReasoning: 'Non-reasoning model',
176
- catalogSource: 'Candidate source',
177
- catalogProvider: 'Candidate provider',
178
- catalogLoading: 'Loading catalog…',
179
- catalogFailed: 'Failed to load catalog.',
180
284
  saveOk: 'Saved.',
181
285
  saveFailed: 'Save failed: ',
182
286
  } as const
@@ -1,36 +1,54 @@
1
1
  /**
2
- * 「LLM 路由」卡片的根字段组:总开关、目录端点/刷新间隔/代理,以及运行期诊断行
3
- * (kitSource 与 models-dev 快照状态,来自模型目录端点而非配置数据)。
2
+ * 卡片根字段组:总开关 + 运行期状态行。
3
+ *
4
+ * 状态行回答的是"我现在到底在跑哪一份 pi-ai"——插件按文件路径动态加载已装 dsh 树
5
+ * 里的套件(见 resolve-dsh.ts),manifest 上写的版本**不是**生效版本。这里显示
6
+ * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及逐项降级
7
+ * 诊断;版本超出验证区间时给出提示(提示而非阻断)。
4
8
  * @module llm-pi/client/root-fields
5
9
  */
6
10
 
7
- import { CheckRow, TextField } from '@dsh-plus/shared/client'
11
+ import { CheckRow } from '@dsh-plus/shared/client'
8
12
  import type { ReactElement } from 'react'
9
13
 
10
- import type { WireModelsDevStatus } from './api.ts'
11
- import { type Draft, numTextOk } from './draft.ts'
14
+ import type { WireKitInfo } from './api.ts'
15
+ import type { Draft } from './draft.ts'
12
16
  import type { Translate } from './i18n.ts'
13
17
 
14
- /** models-dev 状态行文案(缺席/错误/未拉取/正常四态)。 */
15
- function modelsDevText(status: WireModelsDevStatus | null, t: Translate): string {
16
- if (status === null) return t('modelsDevEmpty')
17
- if (status.error !== null) return `${t('modelsDevError')}${status.error}`
18
- if (status.fetchedAt === null) return t('modelsDevEmpty')
19
- return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt}`
18
+ /** 时间戳 → 本地日期(秒级精度足够)。 */
19
+ function formatTime(ms: number | undefined, fallback: string): string {
20
+ if (ms === undefined || !Number.isFinite(ms)) return fallback
21
+ return new Date(ms).toLocaleString()
22
+ }
23
+
24
+ /** 生效版本三连(缺失显示"未知")。 */
25
+ function versionsText(kit: WireKitInfo, t: Translate): string {
26
+ const unknown = t('runtimeVersionUnknown')
27
+ return [
28
+ `pi-ai ${kit.versions.piAi ?? unknown}`,
29
+ `dsh-llm-pi-ai ${kit.versions.piAiAdapter ?? unknown}`,
30
+ ...(kit.versions.dsh === undefined ? [] : [`dsh ${kit.versions.dsh}`]),
31
+ ].join(' · ')
32
+ }
33
+
34
+ /** 来源标签:现场推导 / 回退快照。 */
35
+ function sourceLabel(t: Translate, source: string): string {
36
+ return source === 'official' ? t('runtimeDerived') : t('runtimeFallback')
20
37
  }
21
38
 
22
39
  export interface RootFieldsProps {
23
40
  t: Translate
24
41
  draft: Draft
25
42
  disabled: boolean
26
- kitSource: string | null
27
- modelsDevStatus: WireModelsDevStatus | null
43
+ kit: WireKitInfo | null
44
+ /** 目录索引拉取失败原因(失败时状态行显式提示,其余字段照常可编辑)。 */
45
+ kitError: string
28
46
  /** 根字段编辑(合并式写入并清空状态行)。 */
29
47
  onEdit(patch: Partial<Draft>): void
30
48
  }
31
49
 
32
50
  export function RootFields(props: RootFieldsProps): ReactElement {
33
- const { t, draft, disabled, onEdit } = props
51
+ const { t, draft, disabled, kit, onEdit } = props
34
52
  return (
35
53
  <>
36
54
  <CheckRow
@@ -41,44 +59,38 @@ export function RootFields(props: RootFieldsProps): ReactElement {
41
59
  disabled={disabled}
42
60
  onEdit={(value) => onEdit({ enabled: value })}
43
61
  />
44
- <TextField
45
- prefix="lpc"
46
- id="lpc-catalogUrl"
47
- label={t('catalogUrl')}
48
- hint={t('catalogUrlHint')}
49
- value={draft.catalogUrl}
50
- disabled={disabled}
51
- onEdit={(value) => onEdit({ catalogUrl: value })}
52
- />
53
- <TextField
54
- prefix="lpc"
55
- id="lpc-catalogRefresh"
56
- label={t('catalogRefreshHours')}
57
- hint={t('catalogRefreshHoursHint')}
58
- value={draft.catalogRefreshHours}
59
- numeric
60
- disabled={disabled}
61
- invalid={!numTextOk(draft.catalogRefreshHours)}
62
- invalidLabel={t('invalidNumber')}
63
- onEdit={(value) => onEdit({ catalogRefreshHours: value })}
64
- />
65
- <TextField
66
- prefix="lpc"
67
- id="lpc-catalogProxy"
68
- label={t('catalogProxy')}
69
- hint={t('catalogProxyHint')}
70
- value={draft.catalogProxy}
71
- disabled={disabled}
72
- onEdit={(value) => onEdit({ catalogProxy: value })}
73
- />
74
- <p className="lpc-statusRow">
75
- {t('kitSource')}:{props.kitSource ?? ''}
76
- </p>
77
- <div className="lpc-statusRow">
78
- <span>
79
- {t('modelsDevStatus')}:{modelsDevText(props.modelsDevStatus, t)}
80
- </span>
81
- </div>
62
+ <p className="lpc-groupLabel">{t('runtimeGroup')}</p>
63
+ {props.kitError !== '' ? (
64
+ <p className="lpc-invalid">{`${t('browserFailed')}${props.kitError}`}</p>
65
+ ) : null}
66
+ {kit === null ? (
67
+ <p className="lpc-hint">{t('browserLoading')}</p>
68
+ ) : (
69
+ <>
70
+ <p className="lpc-statusRow">
71
+ {`${t('runtimeSource')}:${kit.source}${kit.root === undefined ? '' : ` · ${t('runtimeRoot')} ${kit.root}`}`}
72
+ </p>
73
+ <p className="lpc-statusRow">{`${t('runtimeVersions')}:${versionsText(kit, t)}`}</p>
74
+ <p className="lpc-statusRow">
75
+ {`${t('runtimeCatalog')}:${kit.catalog.providers} ${t('runtimeCatalogUnit')} ${kit.catalog.models} ${t('runtimeCatalogModels')} · ${t('runtimeCatalogGenerated')} ${formatTime(kit.catalog.generatedAt, t('runtimeVersionUnknown'))}`}
76
+ </p>
77
+ <p className="lpc-statusRow">
78
+ {`${t('runtimeProtocols')}:${kit.protocols.join(' / ')}(${sourceLabel(t, kit.protocolSource)})`}
79
+ </p>
80
+ <p className="lpc-statusRow">
81
+ {`${t('runtimeCompat')}:${sourceLabel(t, kit.compatSource)}`}
82
+ </p>
83
+ {kit.versionNotice !== undefined ? (
84
+ <p className="lpc-invalid">{`${t('runtimeNotice')}:${kit.versionNotice}`}</p>
85
+ ) : null}
86
+ {kit.diagnostics.length > 0 ? (
87
+ <>
88
+ <p className="lpc-catDetailLabel">{t('runtimeDiagnostics')}</p>
89
+ <pre className="lpc-catJson">{kit.diagnostics.join('\n')}</pre>
90
+ </>
91
+ ) : null}
92
+ </>
93
+ )}
82
94
  </>
83
95
  )
84
96
  }