@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 CHANGED
@@ -4,50 +4,18 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
10
  //#region ../shared/src/client/card.tsx
11
11
  const IDLE_STATUS = {
12
12
  kind: "idle",
13
13
  text: ""
14
14
  };
15
- /** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
16
- function CardFoot(props) {
17
- const p = props.prefix;
18
- const section = props.variant === "section";
19
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20
- className: section ? `${p}-sectionFooter` : `${p}-footer`,
21
- children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22
- className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
23
- role: "status",
24
- children: props.status.text
25
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26
- type: "button",
27
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
28
- disabled: action.disabled === true,
29
- onClick: action.onClick,
30
- children: action.label
31
- }, action.key))]
32
- });
33
- }
34
- /** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
35
- function CardBody(props) {
36
- const p = props.prefix;
37
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
38
- props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
39
- className: `${p}-readOnly`,
40
- role: "status",
41
- children: props.readOnlyNotice
42
- }) : null,
43
- props.children,
44
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
45
- ] });
46
- }
47
15
  function CardChrome(props) {
48
16
  const p = props.prefix;
49
17
  const badge = props.statusBadge;
50
- if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
18
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
51
19
  className: `${p}-section`,
52
20
  "aria-label": props.title,
53
21
  children: [
@@ -72,50 +40,37 @@ window.__ModuleLoader__.load({
72
40
  className: `${p}-sectionDesc`,
73
41
  children: props.description
74
42
  }),
75
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
43
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
76
44
  className: `${p}-sectionBody`,
77
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
45
+ children: [
46
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
47
+ className: `${p}-readOnly`,
48
+ role: "status",
49
+ children: props.readOnlyNotice
50
+ }) : null,
51
+ props.children,
52
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53
+ className: `${p}-sectionFooter`,
54
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
55
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
56
+ role: "status",
57
+ children: props.status.text
58
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
59
+ type: "button",
60
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
61
+ disabled: action.disabled === true,
62
+ onClick: action.onClick,
63
+ children: action.label
64
+ }, action.key))]
65
+ })
66
+ ]
78
67
  })
79
68
  ]
80
69
  });
81
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
82
- className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
83
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
84
- type: "button",
85
- className: `${p}-header`,
86
- "aria-expanded": props.open,
87
- "aria-label": props.title,
88
- onClick: () => props.onToggle(!props.open),
89
- children: [
90
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
91
- className: `${p}-headText`,
92
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
93
- className: `${p}-name`,
94
- children: props.title
95
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
96
- className: `${p}-description`,
97
- children: props.description
98
- })]
99
- }),
100
- badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
101
- className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
102
- children: badge.text
103
- }) : null,
104
- props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
105
- className: `${p}-pending`,
106
- children: props.dirtyLabel
107
- }) : null,
108
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
109
- ]
110
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
111
- className: `${p}-body`,
112
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
113
- }) : null]
114
- });
115
70
  }
116
71
  /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
117
72
  function CardLoading(props) {
118
- if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
73
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
119
74
  className: `${props.prefix}-section`,
120
75
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
121
76
  className: `${props.prefix}-readOnly`,
@@ -123,41 +78,44 @@ window.__ModuleLoader__.load({
123
78
  children: props.text
124
79
  })
125
80
  });
126
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
127
- className: `${props.prefix}-card`,
128
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
129
- className: `${props.prefix}-readOnly`,
130
- role: "status",
131
- children: props.text
132
- })
133
- });
134
81
  }
135
82
  //#endregion
136
- //#region ../shared/src/client/config-slots.ts
137
- /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
138
- const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
83
+ //#region ../shared/src/client/clipboard.ts
139
84
  /**
140
- * 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
141
- * 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
142
- * 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
85
+ * 剪贴板复制助手(浏览器半共享件):`navigator.clipboard` 优先,
86
+ * 非安全上下文(如非 localhost 直连的 HTTP 页面)无该 API 时退 `execCommand`。
87
+ * @module @dsh-plus/shared/client/clipboard
143
88
  */
144
- function cardVariantFor(view) {
145
- return view === "page" ? "section" : "card";
89
+ /** 复制文本到剪贴板;返回是否成功(失败静默,调用方自行决定是否提示)。 */
90
+ async function copyText(text) {
91
+ try {
92
+ await navigator.clipboard.writeText(text);
93
+ return true;
94
+ } catch {
95
+ const area = document.createElement("textarea");
96
+ area.value = text;
97
+ area.style.position = "fixed";
98
+ area.style.opacity = "0";
99
+ document.body.appendChild(area);
100
+ area.select();
101
+ const ok = document.execCommand("copy");
102
+ area.remove();
103
+ return ok;
104
+ }
146
105
  }
106
+ //#endregion
107
+ //#region ../shared/src/client/config-slots.ts
108
+ /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
109
+ const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
147
110
  /**
148
- * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
149
- * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
111
+ * 把一张配置卡片注册进插件页的两个配置槽位。
112
+ * 两个槽位都由 0.2.1-alpha.1 的插件页声明,任一缺席即报错——不做版本探测。
150
113
  */
151
114
  function injectPluginConfigCard(slots, reg) {
152
115
  const base = {
153
116
  locale: reg.ns,
154
117
  inject: reg.inject
155
118
  };
156
- slots.inject("settings.plugin.item", () => slots.register({
157
- name: "settings.plugin.item",
158
- key: reg.ns,
159
- ...base
160
- }, reg.component));
161
119
  slots.inject("plugins.row.config", () => slots.register({
162
120
  name: "plugins.row.config",
163
121
  key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
@@ -182,41 +140,6 @@ window.__ModuleLoader__.load({
182
140
  */
183
141
  //#endregion
184
142
  //#region ../shared/src/client/fields.tsx
185
- function TextField$1(props) {
186
- const p = props.prefix ?? "dshp";
187
- const invalid = props.invalid === true;
188
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
189
- className: `${p}-field`,
190
- children: [
191
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
192
- className: `${p}-head`,
193
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
194
- className: `${p}-label`,
195
- htmlFor: props.id,
196
- children: props.label
197
- }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
198
- className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
199
- children: props.badge.text
200
- }) : null]
201
- }),
202
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
203
- id: props.id,
204
- className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
205
- type: props.password === true ? "password" : "text",
206
- inputMode: props.numeric === true ? "numeric" : void 0,
207
- autoComplete: props.password === true ? "off" : void 0,
208
- "aria-invalid": invalid || void 0,
209
- value: props.value,
210
- disabled: props.disabled === true,
211
- onChange: (event) => props.onEdit(event.target.value)
212
- }),
213
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
214
- className: invalid ? `${p}-invalid` : `${p}-hint`,
215
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
216
- })
217
- ]
218
- });
219
- }
220
143
  /**
221
144
  * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
222
145
  * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
@@ -378,36 +301,33 @@ window.__ModuleLoader__.load({
378
301
  //#endregion
379
302
  //#region ../shared/src/client/styles.ts
380
303
  /**
381
- * 配置卡片样式工厂(各插件 client 的公共收编版)。
382
- * 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
383
- * 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
304
+ * 配置分节样式工厂(各插件 client 的公共收编版)。
305
+ *
306
+ * 目标宿主 dsh 0.2.1-alpha.1:插件页把配置视图放进已有页面级内边距的容器里
307
+ * (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
308
+ * 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
309
+ * 横向零内边距、页脚非 sticky),度量对齐官方设置页
310
+ * (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
311
+ *
312
+ * 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
384
313
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
385
- * - footer 按钮等宽换行、状态文字独占一行;
386
- * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
387
- * - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
388
- * 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
389
- * 缩窄移动端可用宽度(卡片套卡片)。
314
+ * - 页脚按钮等宽换行、状态文字独占一行。
390
315
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
391
316
  * @module @dsh-plus/shared/client/styles
392
317
  */
393
- /** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
318
+ /** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
394
319
  function cardCss(prefix, extra = "") {
395
320
  return `
396
- .${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
397
- .${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
398
- .${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
399
- .${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
400
- .${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
401
- .${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
402
- .${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
403
- .${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
404
- .${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
405
- .${prefix}-chevronOpen{transform:rotate(180deg)}
321
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
322
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
323
+ .${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
324
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
325
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
326
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
327
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
406
328
  .${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
407
- .${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
408
329
  .${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
409
330
  .${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
410
- .${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
411
331
  .${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
412
332
  .${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
413
333
  .${prefix}-head{align-items:center;gap:8px;display:flex}
@@ -433,19 +353,9 @@ window.__ModuleLoader__.load({
433
353
  .${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
434
354
  .${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
435
355
  .${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
436
- /* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
437
- 横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
438
- .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
439
- .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
440
- .${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
441
- .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
442
- .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
443
- .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
444
- .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
445
356
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
446
357
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
447
358
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
448
- .${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
449
359
  .${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
450
360
  .${prefix}-statusError{color:var(--dsw-alias-label-error)}
451
361
  .${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
@@ -455,16 +365,10 @@ window.__ModuleLoader__.load({
455
365
  .${prefix}-btn:disabled{opacity:.4;cursor:default}
456
366
  .${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
457
367
  @media (max-width:767px){
458
- .${prefix}-body{margin:0 10px}
459
- .${prefix}-header{padding:12px 12px}
460
368
  .${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
461
369
  .${prefix}-checkRow{min-height:44px;padding:8px 0}
462
- .${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
463
370
  .${prefix}-status{flex-basis:100%;flex:none}
464
371
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
465
- .${prefix}-statusBadge{display:none}
466
- .${prefix}-sectionBadge{display:inline-block}
467
- .${prefix}-sectionFooter{position:static}
468
372
  }
469
373
  @media (pointer:coarse){
470
374
  .${prefix}-btn{min-height:44px}
@@ -495,8 +399,8 @@ ${extra}
495
399
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
496
400
  * 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 对命名空间做编译期
497
401
  * 语法校验,见 config.ts);浏览器半把它作为配置卡片的注册键
498
- * (legacy settings.plugin.item keyed 槽位按「卡片编辑的 settings 命名空间」
499
- * 分发;0.1.6-alpha.2 起改挂插件页 row/bundle config,见 shared/config-slots),
402
+ * (插件页配置卡片挂在 row/bundle config 槽位上,与命名空间解耦,
403
+ * 见 shared/config-slots),
500
404
  * 同一字面量两处共用,防止漂移。
501
405
  * @module llm-pi/ns
502
406
  */
@@ -509,28 +413,41 @@ ${extra}
509
413
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
510
414
  return body;
511
415
  }
512
- /** 目录查询:provider 为空时只返回该源的 provider 列表。 */
513
- async function fetchCatalog(provider, source) {
514
- const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`;
515
- return parse(await fetch(url, { credentials: "same-origin" }));
516
- }
517
- /** 手动拉取 models.dev 目录:POST /catalog/refresh → 最新快照状态。 */
518
- async function refreshCatalog() {
519
- return parse(await fetch(`${ROUTE_CATALOG}/refresh`, {
520
- method: "POST",
521
- credentials: "same-origin"
416
+ /** 运行期事实 + 目录索引(卡片挂载时一次)。 */
417
+ async function fetchCatalogMeta() {
418
+ return parse(await fetch(ROUTE_CATALOG, { credentials: "same-origin" }));
419
+ }
420
+ /** 目录搜索(模糊 q + provider/api/能力/分页)。 */
421
+ async function searchCatalog(query, signal) {
422
+ const params = new URLSearchParams();
423
+ if (query.q !== void 0 && query.q.length > 0) params.set("q", query.q);
424
+ if (query.provider !== void 0 && query.provider.length > 0) params.set("provider", query.provider);
425
+ if (query.api !== void 0 && query.api.length > 0) params.set("api", query.api);
426
+ if (query.reasoning === true) params.set("reasoning", "1");
427
+ if (query.image === true) params.set("image", "1");
428
+ if (query.servableOnly === false) params.set("servable", "0");
429
+ if (query.offset !== void 0) params.set("offset", String(query.offset));
430
+ if (query.limit !== void 0) params.set("limit", String(query.limit));
431
+ const suffix = params.toString();
432
+ return parse(await fetch(`${ROUTE_CATALOG}/models${suffix === "" ? "" : `?${suffix}`}`, {
433
+ credentials: "same-origin",
434
+ ...signal === void 0 ? {} : { signal }
522
435
  }));
523
436
  }
524
437
  //#endregion
525
438
  //#region src/client/constants.ts
526
439
  /**
527
- * 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts、compat.ts 逐字对齐。
440
+ * 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts 逐字对齐。
528
441
  * 浏览器半不能 import 服务端模块(tsdown 只打包 client 侧入口),
529
442
  * 改动服务端这些常量时必须同步本文件。
443
+ *
444
+ * 两个集合改为**服务端下发**(与服务端校验同源,浏览器半不再手抄):
445
+ * - 线协议:取自已装官方适配器的 supportedProtocols()(`installProtocols`);
446
+ * - compat 字段表:取自官方 bundle + Config schema 推导(`installCompatFields`)。
530
447
  * @module llm-pi/client/constants
531
448
  */
532
- /** 协议枚举(来源:config.ts PROTOCOL_IDS)。 */
533
- const PROTOCOL_IDS = [
449
+ /** 协议兜底集合(来源:config.ts FALLBACK_PROTOCOLS);服务端下发前/失败时使用。 */
450
+ const FALLBACK_PROTOCOLS = [
534
451
  "openai-completions",
535
452
  "openai-responses",
536
453
  "anthropic-messages"
@@ -568,13 +485,26 @@ ${extra}
568
485
  "high"
569
486
  ];
570
487
  /**
488
+ * 生效协议集合:由服务端 /catalog 下发(服务端与官方适配器同源推导),
489
+ * 未拿到前用 FALLBACK_PROTOCOLS。
490
+ */
491
+ let protocols = [...FALLBACK_PROTOCOLS];
492
+ /** 安装服务端下发的协议集合(card 挂载时调用)。 */
493
+ function installProtocols(list) {
494
+ if (list.length > 0) protocols = [...list];
495
+ }
496
+ /** 当前可选协议(顺序即官方表顺序)。 */
497
+ function protocolOptions() {
498
+ return protocols;
499
+ }
500
+ /**
571
501
  * 逐协议 compat 字段表:**服务端推导结果经 /catalog 下发**,浏览器半不再手抄
572
- * (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 compat-gates.ts)。
502
+ * (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 official-surface.ts)。
573
503
  * 未拿到服务端表时用 EMPTY(UI 只渲染提示行),不放内置副本兜底——宁可少画
574
504
  * 控件,也不给出可能与官方不符的字段集。
575
505
  */
576
506
  let compatTable = {};
577
- /** 安装服务端下发的字段表(card 挂载/刷新目录时调用)。 */
507
+ /** 安装服务端下发的字段表(card 挂载时调用)。 */
578
508
  function installCompatFields(table) {
579
509
  compatTable = table;
580
510
  }
@@ -600,9 +530,6 @@ ${extra}
600
530
  const value = Number(trimmed);
601
531
  return Number.isFinite(value) ? value : void 0;
602
532
  }
603
- function numTextOk(text) {
604
- return text.trim() !== "" && Number.isFinite(Number(text.trim()));
605
- }
606
533
  function headersToPairs(headers) {
607
534
  return Object.entries(headers ?? {}).map(([key, value]) => ({
608
535
  key,
@@ -862,95 +789,49 @@ ${extra}
862
789
  function draftFromValue(value) {
863
790
  return {
864
791
  enabled: value.enabled,
865
- catalogUrl: value.catalogUrl,
866
- catalogRefreshHours: String(value.catalogRefreshHours),
867
- catalogProxy: value.catalogProxy,
868
792
  providers: Object.fromEntries(Object.entries(value.providers).map(([route, provider]) => [route, providerDraftFromWire(provider)]))
869
793
  };
870
794
  }
871
795
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
872
796
  function toPatch(draft) {
873
- const refreshHours = toNum(draft.catalogRefreshHours);
874
797
  return {
875
798
  enabled: draft.enabled,
876
- catalogUrl: draft.catalogUrl.trim(),
877
- catalogRefreshHours: refreshHours,
878
- catalogProxy: draft.catalogProxy.trim(),
879
799
  providers: Object.fromEntries(Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]))
880
800
  };
881
801
  }
882
- //#endregion
883
- //#region src/client/root-fields.tsx
884
802
  /**
885
- * 「LLM 路由」卡片的根字段组:总开关、目录端点/刷新间隔/代理,以及运行期诊断行
886
- * (kitSource 与 models-dev 快照状态,来自模型目录端点而非配置数据)。
887
- * @module llm-pi/client/root-fields
803
+ * 把一个内置目录模型加进目标 route:只写 `id` + `extends`(**不预填**
804
+ * name/contextWindow/maxTokens——继承才能跟随 pi-ai 目录升级,预填会把值冻住)。
805
+ * 同 id 已存在时原样返回(added=false),不产生重复条目。
888
806
  */
889
- /** models-dev 状态行文案(缺席/错误/未拉取/正常四态)。 */
890
- function modelsDevText(status, t) {
891
- if (status === null) return t("modelsDevEmpty");
892
- if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
893
- if (status.fetchedAt === null) return t("modelsDevEmpty");
894
- return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt}`;
895
- }
896
- function RootFields(props) {
897
- const { t, draft, disabled, onEdit } = props;
898
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
899
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
900
- prefix: "lpc",
901
- id: "lpc-enabled",
902
- label: t("enabled"),
903
- checked: draft.enabled,
904
- disabled,
905
- onEdit: (value) => onEdit({ enabled: value })
906
- }),
907
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
908
- prefix: "lpc",
909
- id: "lpc-catalogUrl",
910
- label: t("catalogUrl"),
911
- hint: t("catalogUrlHint"),
912
- value: draft.catalogUrl,
913
- disabled,
914
- onEdit: (value) => onEdit({ catalogUrl: value })
915
- }),
916
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
917
- prefix: "lpc",
918
- id: "lpc-catalogRefresh",
919
- label: t("catalogRefreshHours"),
920
- hint: t("catalogRefreshHoursHint"),
921
- value: draft.catalogRefreshHours,
922
- numeric: true,
923
- disabled,
924
- invalid: !numTextOk(draft.catalogRefreshHours),
925
- invalidLabel: t("invalidNumber"),
926
- onEdit: (value) => onEdit({ catalogRefreshHours: value })
927
- }),
928
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
929
- prefix: "lpc",
930
- id: "lpc-catalogProxy",
931
- label: t("catalogProxy"),
932
- hint: t("catalogProxyHint"),
933
- value: draft.catalogProxy,
934
- disabled,
935
- onEdit: (value) => onEdit({ catalogProxy: value })
936
- }),
937
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
938
- className: "lpc-statusRow",
939
- children: [
940
- t("kitSource"),
941
- ":",
942
- props.kitSource ?? ""
943
- ]
944
- }),
945
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
946
- className: "lpc-statusRow",
947
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
948
- t("modelsDevStatus"),
949
- ":",
950
- modelsDevText(props.modelsDevStatus, t)
951
- ] })
952
- })
953
- ] });
807
+ function addModelEntry(draft, route, model) {
808
+ const provider = draft.providers[route];
809
+ if (provider === void 0) return {
810
+ draft,
811
+ added: false
812
+ };
813
+ if (provider.models.some((entry) => entry.id.trim() === model.id)) return {
814
+ draft,
815
+ added: false
816
+ };
817
+ const entry = {
818
+ ...emptyModelDraft(),
819
+ id: model.id,
820
+ extends: model.path
821
+ };
822
+ return {
823
+ draft: {
824
+ ...draft,
825
+ providers: {
826
+ ...draft.providers,
827
+ [route]: {
828
+ ...provider,
829
+ models: [...provider.models, entry]
830
+ }
831
+ }
832
+ },
833
+ added: true
834
+ };
954
835
  }
955
836
  //#endregion
956
837
  //#region src/client/fields.tsx
@@ -961,7 +842,12 @@ ${extra}
961
842
  * @module llm-pi/client/fields
962
843
  */
963
844
  function CollapseSection(props) {
964
- const [open, setOpen] = (0, react.useState)(props.defaultOpen);
845
+ const [innerOpen, setInnerOpen] = (0, react.useState)(props.defaultOpen);
846
+ const open = props.open ?? innerOpen;
847
+ const toggle = () => {
848
+ if (props.onToggle !== void 0) props.onToggle();
849
+ else setInnerOpen((value) => !value);
850
+ };
965
851
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
966
852
  className: "lpc-collapse",
967
853
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -969,11 +855,18 @@ ${extra}
969
855
  className: "lpc-collapseHead",
970
856
  id: props.id,
971
857
  "aria-expanded": open,
972
- onClick: () => setOpen(!open),
973
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
974
- className: "lpc-collapseTitle",
975
- children: props.title
976
- })]
858
+ onClick: toggle,
859
+ children: [
860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }),
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
862
+ className: "lpc-collapseTitle",
863
+ children: props.title
864
+ }),
865
+ props.meta !== void 0 && props.meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
866
+ className: "lpc-collapseMeta",
867
+ children: props.meta
868
+ }) : null
869
+ ]
977
870
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
978
871
  className: "lpc-collapseBody",
979
872
  children: props.children
@@ -1215,211 +1108,850 @@ ${extra}
1215
1108
  });
1216
1109
  }
1217
1110
  //#endregion
1218
- //#region src/client/views/compat.tsx
1219
- /** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
1220
- function pruneCompatForApi(compat, api) {
1221
- const group = api !== "" && compatFieldsOf(api).length > 0 ? api : COMPAT_FALLBACK_API;
1222
- const fields = new Set(compatFieldsOf(group));
1223
- const next = {};
1224
- for (const [key, value] of Object.entries(compat)) if (fields.has(key)) next[key] = value;
1225
- return next;
1111
+ //#region src/client/root-fields.tsx
1112
+ /**
1113
+ * 卡片根字段组:总开关 + 运行期状态行。
1114
+ *
1115
+ * 状态行回答的是"我现在到底在跑哪一份 pi-ai"——插件按文件路径动态加载已装 dsh 树
1116
+ * 里的套件(见 resolve-dsh.ts),manifest 上写的版本**不是**生效版本。这里显示
1117
+ * 生效版本、安装树、目录规模与数据日期、可服务协议、compat 表来源,以及逐项降级
1118
+ * 诊断;版本超出验证区间时给出提示(提示而非阻断)。
1119
+ * @module llm-pi/client/root-fields
1120
+ */
1121
+ /** 时间戳 → 本地日期(秒级精度足够)。 */
1122
+ function formatTime(ms, fallback) {
1123
+ if (ms === void 0 || !Number.isFinite(ms)) return fallback;
1124
+ return new Date(ms).toLocaleString();
1125
+ }
1126
+ /** 生效版本三连(缺失显示"未知")。 */
1127
+ function versionsText(kit, t) {
1128
+ const unknown = t("runtimeVersionUnknown");
1129
+ return [
1130
+ `pi-ai ${kit.versions.piAi ?? unknown}`,
1131
+ `dsh-llm-pi-ai ${kit.versions.piAiAdapter ?? unknown}`,
1132
+ ...kit.versions.dsh === void 0 ? [] : [`dsh ${kit.versions.dsh}`]
1133
+ ].join(" · ");
1226
1134
  }
1227
- function CompatEditor(props) {
1228
- const effective = props.api !== "" && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API;
1229
- const fields = compatFieldsOf(effective);
1230
- const setField = (field, value) => {
1231
- const next = { ...props.compat };
1232
- if (value === void 0) delete next[field];
1233
- else next[field] = value;
1234
- props.onEdit(next);
1235
- };
1236
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1237
- className: `lpc-field lpc-wide`,
1238
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
1239
- id: `${props.idPrefix}-collapse`,
1240
- title: props.t("compatGroup"),
1241
- defaultOpen: false,
1242
- children: [props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1243
- className: "lpc-hint",
1244
- children: props.t("compatApiHint")
1245
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1246
- className: "lpc-grid",
1247
- children: fields.map((field) => {
1248
- const spec = compatFieldSpec(effective, field);
1249
- if (spec === void 0) return null;
1250
- if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1251
- id: `${props.idPrefix}-${field}`,
1252
- label: field,
1253
- value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1254
- options: ["true", "false"],
1255
- unsetLabel: props.t("compatUnset"),
1256
- disabled: props.disabled === true,
1257
- onEdit: (value) => {
1258
- if (value === "") setField(field, void 0);
1259
- else setField(field, value === "true");
1260
- }
1261
- }, field);
1262
- if (spec === "object") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1263
- id: `${props.idPrefix}-${field}`,
1264
- label: field,
1265
- value: props.compat[field],
1266
- epoch: props.epoch,
1267
- disabled: props.disabled === true,
1268
- invalidText: props.t("invalidJson"),
1269
- onEdit: (value) => setField(field, value)
1270
- }, field);
1271
- if (spec === "integer" || spec === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IntegerField, {
1272
- id: `${props.idPrefix}-${field}`,
1273
- label: field,
1274
- value: props.compat[field],
1275
- epoch: props.epoch,
1276
- invalidLabel: props.t("invalidInteger"),
1277
- disabled: props.disabled === true,
1278
- onEdit: (value) => setField(field, value)
1279
- }, field);
1280
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1281
- id: `${props.idPrefix}-${field}`,
1282
- label: field,
1283
- value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1284
- options: spec,
1285
- unsetLabel: props.t("compatUnset"),
1286
- disabled: props.disabled === true,
1287
- onEdit: (value) => {
1288
- if (value === "") setField(field, void 0);
1289
- else setField(field, value);
1290
- }
1291
- }, field);
1292
- })
1293
- })]
1294
- })
1295
- });
1135
+ /** 来源标签:现场推导 / 回退快照。 */
1136
+ function sourceLabel(t, source) {
1137
+ return source === "official" ? t("runtimeDerived") : t("runtimeFallback");
1138
+ }
1139
+ function RootFields(props) {
1140
+ const { t, draft, disabled, kit, onEdit } = props;
1141
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1143
+ prefix: "lpc",
1144
+ id: "lpc-enabled",
1145
+ label: t("enabled"),
1146
+ checked: draft.enabled,
1147
+ disabled,
1148
+ onEdit: (value) => onEdit({ enabled: value })
1149
+ }),
1150
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1151
+ className: "lpc-groupLabel",
1152
+ children: t("runtimeGroup")
1153
+ }),
1154
+ props.kitError !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1155
+ className: "lpc-invalid",
1156
+ children: `${t("browserFailed")}${props.kitError}`
1157
+ }) : null,
1158
+ kit === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1159
+ className: "lpc-hint",
1160
+ children: t("browserLoading")
1161
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1163
+ className: "lpc-statusRow",
1164
+ children: `${t("runtimeSource")}:${kit.source}${kit.root === void 0 ? "" : ` · ${t("runtimeRoot")} ${kit.root}`}`
1165
+ }),
1166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1167
+ className: "lpc-statusRow",
1168
+ children: `${t("runtimeVersions")}:${versionsText(kit, t)}`
1169
+ }),
1170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1171
+ className: "lpc-statusRow",
1172
+ children: `${t("runtimeCatalog")}:${kit.catalog.providers} ${t("runtimeCatalogUnit")} ${kit.catalog.models} ${t("runtimeCatalogModels")} · ${t("runtimeCatalogGenerated")} ${formatTime(kit.catalog.generatedAt, t("runtimeVersionUnknown"))}`
1173
+ }),
1174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1175
+ className: "lpc-statusRow",
1176
+ children: `${t("runtimeProtocols")}:${kit.protocols.join(" / ")}(${sourceLabel(t, kit.protocolSource)})`
1177
+ }),
1178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1179
+ className: "lpc-statusRow",
1180
+ children: `${t("runtimeCompat")}:${sourceLabel(t, kit.compatSource)}`
1181
+ }),
1182
+ kit.versionNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1183
+ className: "lpc-invalid",
1184
+ children: `${t("runtimeNotice")}:${kit.versionNotice}`
1185
+ }) : null,
1186
+ kit.diagnostics.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1187
+ className: "lpc-catDetailLabel",
1188
+ children: t("runtimeDiagnostics")
1189
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1190
+ className: "lpc-catJson",
1191
+ children: kit.diagnostics.join("\n")
1192
+ })] }) : null
1193
+ ] })
1194
+ ] });
1296
1195
  }
1297
1196
  //#endregion
1298
- //#region src/client/views/models.tsx
1197
+ //#region src/client/route-fit.ts
1299
1198
  /**
1300
- * 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat),
1301
- * extends 输入框带 datalist 候选——候选来自 /catalog 端点:选定 source
1302
- * (builtin / models-dev)与 provider 后拉取其 models 列表。
1303
- * @module llm-pi/client/views/models
1199
+ * 解析条目指向的继承引用(与服务端 inherit.ts 同规则):
1200
+ * `"provider/model"` 显式引用;裸 `"model"` 随 route 级 extends 源;
1201
+ * 条目无 extends 时按同名模型挂在 route 级 extends 源下。
1202
+ * 两段任一为空(无引用可解析)时返回空串。
1304
1203
  */
1305
- function catalogNote(status, t) {
1306
- if (status === void 0) return "";
1307
- if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
1308
- return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? "-"}`;
1204
+ function entryReference(entry, routeExtends) {
1205
+ const raw = entry.extends.trim();
1206
+ if (raw === "") return {
1207
+ provider: routeExtends.trim(),
1208
+ model: entry.id.trim()
1209
+ };
1210
+ const slash = raw.indexOf("/");
1211
+ if (slash > 0) return {
1212
+ provider: raw.slice(0, slash),
1213
+ model: raw.slice(slash + 1)
1214
+ };
1215
+ return {
1216
+ provider: routeExtends.trim(),
1217
+ model: raw
1218
+ };
1309
1219
  }
1310
- function ModelsTable(props) {
1311
- const { t } = props;
1312
- const [source, setSource] = (0, react.useState)("builtin");
1313
- const [providerIds, setProviderIds] = (0, react.useState)([]);
1314
- const [provider, setProvider] = (0, react.useState)("");
1315
- const [candidateModels, setCandidateModels] = (0, react.useState)([]);
1316
- const [note, setNote] = (0, react.useState)("");
1317
- const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1318
- (0, react.useEffect)(() => {
1319
- let alive = true;
1320
- setNote(t("catalogLoading"));
1321
- (async () => {
1322
- try {
1323
- const list = await fetchCatalog("", source);
1324
- if (!alive) return;
1325
- setProviderIds(list.providers);
1326
- const preferred = list.providers.includes(props.defaultProvider) ? props.defaultProvider : list.providers[0] ?? "";
1327
- setProvider(preferred);
1328
- setNote(catalogNote(list.status, t));
1329
- if (preferred === "") return;
1330
- const result = await fetchCatalog(preferred, source);
1331
- if (alive) setCandidateModels(result.models);
1332
- } catch {
1333
- if (alive) setNote(t("catalogFailed"));
1334
- }
1335
- })();
1336
- return () => {
1337
- alive = false;
1338
- };
1339
- }, [source]);
1340
- const onProviderChange = (value) => {
1341
- setProvider(value);
1342
- if (value === "") {
1343
- setCandidateModels([]);
1344
- return;
1220
+ /**
1221
+ * 推断 route 当前实际生效的协议:
1222
+ * - route 级 `api` 显式给出即用它(物化时它压过继承值);
1223
+ * - 否则逐模型查**其引用指向的模型**的协议(注意是引用目标,不是条目 id:
1224
+ * 生产配置普遍用别名条目,如 `{id: 'glm-5.3', extends: 'zai/glm-5.3'}`):
1225
+ * 单一 → 该协议;多个 → mixed;引用解析不出来计 unresolved。
1226
+ */
1227
+ function routeApiFacts(provider, apis) {
1228
+ const explicit = provider.api.trim();
1229
+ if (explicit !== "") return {
1230
+ api: explicit,
1231
+ mixed: false,
1232
+ unresolved: 0
1233
+ };
1234
+ const found = /* @__PURE__ */ new Set();
1235
+ let unresolved = 0;
1236
+ for (const model of provider.models) {
1237
+ const ref = entryReference(model, provider.extends);
1238
+ if (ref.provider === "" || ref.model === "") {
1239
+ unresolved += 1;
1240
+ continue;
1345
1241
  }
1346
- fetchCatalog(value, source).then((result) => setCandidateModels(result.models)).catch(() => setNote(t("catalogFailed")));
1242
+ const api = apis[ref.provider]?.[ref.model];
1243
+ if (api === void 0) unresolved += 1;
1244
+ else found.add(api);
1245
+ }
1246
+ if (found.size === 1) return {
1247
+ api: [...found][0] ?? "",
1248
+ mixed: false,
1249
+ unresolved
1250
+ };
1251
+ return {
1252
+ mixed: found.size > 1,
1253
+ unresolved
1254
+ };
1255
+ }
1256
+ /** route 是否由 PiAiAdapter 服务(`adapter` 在卡片的 extra 里原样往返)。 */
1257
+ function isPiRoute(provider) {
1258
+ const adapter = provider.extra["adapter"];
1259
+ return adapter === void 0 || adapter === "pi";
1260
+ }
1261
+ /** 可作为「一键添加」目标的 route(deepseek 路由不适用 pi-ai 继承)。 */
1262
+ function targetRoutes(providers) {
1263
+ return Object.entries(providers).filter(([, provider]) => isPiRoute(provider)).map(([route]) => route).sort((left, right) => left.localeCompare(right));
1264
+ }
1265
+ /**
1266
+ * 判定某内置模型能否加进目标 route:
1267
+ * 协议不受支持 → unsupported;route 不存在 → no-route;同 id 已在 → duplicate;
1268
+ * route 已混用多个协议 → route-mixed(保存本来就会失败,先提示修好);
1269
+ * route 已有确定协议且与模型不同 → protocol-conflict(保存一定失败,提前拦)。
1270
+ */
1271
+ function addEligibility(options) {
1272
+ const { model, provider, apis } = options;
1273
+ if (!model.servable) return {
1274
+ ok: false,
1275
+ reason: "unsupported",
1276
+ detail: model.api
1277
+ };
1278
+ if (provider === void 0) return {
1279
+ ok: false,
1280
+ reason: "no-route"
1281
+ };
1282
+ if (provider.models.some((entry) => entry.id.trim() === model.id)) return {
1283
+ ok: false,
1284
+ reason: "duplicate"
1285
+ };
1286
+ const facts = routeApiFacts(provider, apis);
1287
+ if (facts.mixed) return {
1288
+ ok: false,
1289
+ reason: "route-mixed"
1347
1290
  };
1291
+ if (facts.api !== void 0 && facts.api !== model.api) return {
1292
+ ok: false,
1293
+ reason: "protocol-conflict",
1294
+ detail: facts.api
1295
+ };
1296
+ return { ok: true };
1297
+ }
1298
+ //#endregion
1299
+ //#region src/client/views/catalog-row.tsx
1300
+ /**
1301
+ * 目录浏览器的单行:路径 id(可复制)+ 名称 + 协议徽标 + 参数行 + 折叠的参数明细
1302
+ * + 「添加到 route」(不可添加时按钮文案即原因)。与浏览器主体拆开,
1303
+ * 便于各自演进(行内只依赖服务端给的模型事实)。
1304
+ * @module llm-pi/client/views/catalog-row
1305
+ */
1306
+ /** 计数展示(千分位;不引本地化依赖)。 */
1307
+ function count$1(value) {
1308
+ return value.toLocaleString();
1309
+ }
1310
+ /** 参数行的键值短展示:字符串化,长值截断。 */
1311
+ function shortValue(value) {
1312
+ const text = typeof value === "string" ? value : JSON.stringify(value);
1313
+ if (text === void 0) return "undefined";
1314
+ return text.length > 120 ? `${text.slice(0, 117)}…` : text;
1315
+ }
1316
+ /** 禁止添加的原因文案。 */
1317
+ function blockText(t, reason, detail) {
1318
+ if (reason === "unsupported") return `${t("blockUnsupported")}${detail === void 0 ? "" : `(${detail})`}`;
1319
+ if (reason === "duplicate") return t("blockDuplicate");
1320
+ if (reason === "protocol-conflict") return `${t("blockConflict")}${detail === void 0 || detail === "" ? "" : `(${detail})`}`;
1321
+ if (reason === "route-mixed") return t("blockMixed");
1322
+ return t("blockNoRoute");
1323
+ }
1324
+ /** 单行:路径 id(可复制)+ 名称 + 参数行 + 折叠的参数明细 + 添加按钮。 */
1325
+ function CatalogRow(props) {
1326
+ const { t, model } = props;
1327
+ const [details, setDetails] = (0, react.useState)(false);
1328
+ const [copied, setCopied] = (0, react.useState)(false);
1329
+ const timer = (0, react.useRef)(null);
1330
+ (0, react.useEffect)(() => () => {
1331
+ if (timer.current !== null) clearTimeout(timer.current);
1332
+ }, []);
1333
+ const blocked = props.eligibility.ok ? void 0 : props.eligibility;
1334
+ const compatKeys = Object.keys(model.compat ?? {});
1335
+ const restKeys = Object.keys(model.rest);
1348
1336
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1349
- className: "lpc-models",
1337
+ className: "lpc-catRow",
1350
1338
  children: [
1351
1339
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1352
- className: "lpc-modelHead",
1353
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1354
- className: "lpc-modelTitle",
1355
- children: t("modelsGroup")
1356
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1357
- type: "button",
1358
- className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1359
- disabled: props.disabled === true,
1360
- onClick: () => props.onModels([...props.models, emptyModelDraft()]),
1361
- children: t("addModel")
1362
- })]
1363
- }),
1364
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1365
- className: "lpc-catalogBar",
1340
+ className: "lpc-catHead",
1366
1341
  children: [
1367
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1368
- className: "lpc-catalogLabel",
1369
- htmlFor: `${listId}-source`,
1370
- children: t("catalogSource")
1371
- }),
1372
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1373
- id: `${listId}-source`,
1374
- className: "lpc-input lpc-select lpc-catalogSelect",
1375
- value: source,
1376
- disabled: props.disabled === true,
1377
- onChange: (event) => setSource(event.target.value),
1378
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1379
- value: "builtin",
1380
- children: "builtin"
1381
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1382
- value: "models-dev",
1383
- children: "models-dev"
1384
- })]
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1343
+ className: "lpc-catPath",
1344
+ children: model.path
1385
1345
  }),
1386
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1387
- className: "lpc-catalogLabel",
1388
- htmlFor: `${listId}-provider`,
1389
- children: t("catalogProvider")
1346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1347
+ type: "button",
1348
+ className: "lpc-iconBtn",
1349
+ title: copied ? t("browserCopied") : t("browserCopy"),
1350
+ "aria-label": t("browserCopy"),
1351
+ onClick: () => {
1352
+ copyText(model.path).then((ok) => {
1353
+ if (!ok) return;
1354
+ setCopied(true);
1355
+ if (timer.current !== null) clearTimeout(timer.current);
1356
+ timer.current = setTimeout(() => setCopied(false), 1500);
1357
+ });
1358
+ },
1359
+ children: copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCopyOutlineRegular, { size: 14 })
1360
+ }),
1361
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1362
+ className: "lpc-catName",
1363
+ children: model.name
1390
1364
  }),
1391
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1392
- id: `${listId}-provider`,
1393
- className: "lpc-input lpc-select lpc-catalogSelect",
1394
- value: provider,
1395
- disabled: props.disabled === true || providerIds.length === 0,
1396
- onChange: (event) => onProviderChange(event.target.value),
1397
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1398
- value: "",
1399
- children: "-"
1400
- }), providerIds.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1401
- value: id,
1402
- children: id
1403
- }, id))]
1365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1366
+ className: model.servable ? "lpc-badge lpc-badgeSet" : "lpc-badge lpc-badgeUnset",
1367
+ children: model.api
1368
+ }),
1369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1370
+ type: "button",
1371
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1372
+ disabled: props.disabled || blocked !== void 0,
1373
+ title: blocked === void 0 ? void 0 : blockText(t, blocked.reason, blocked.detail),
1374
+ onClick: props.onAdd,
1375
+ children: blocked === void 0 ? t("browserAdd") : blockText(t, blocked.reason, blocked.detail)
1404
1376
  })
1405
1377
  ]
1406
1378
  }),
1407
- note !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1379
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1380
+ className: "lpc-catParams",
1381
+ children: [
1382
+ `${t("browserContextShort")} ${count$1(model.contextWindow)} · ${t("browserMaxShort")} ${count$1(model.maxTokens)} · `,
1383
+ model.input.map((m) => m === "image" ? t("browserInputImage") : t("browserInputText")).join("/"),
1384
+ model.reasoning ? ` · ${t("browserReasoning")}` : "",
1385
+ compatKeys.length > 0 ? ` · compat ${compatKeys.length}` : ""
1386
+ ]
1387
+ }),
1388
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1389
+ type: "button",
1390
+ className: "lpc-catDetailsToggle",
1391
+ "aria-expanded": details,
1392
+ onClick: () => setDetails(!details),
1393
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${details ? " lpc-chevronOpen" : ""}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("browserDetails") })]
1394
+ }),
1395
+ details ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1396
+ className: "lpc-catDetails",
1397
+ children: [
1398
+ model.thinkingLevelMap !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1399
+ className: "lpc-catParams",
1400
+ children: `${t("browserThinkMap")}:${Object.entries(model.thinkingLevelMap).map(([level, wire]) => `${level}=${wire ?? "-"}`).join(" ")}`
1401
+ }) : null,
1402
+ compatKeys.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1403
+ className: "lpc-catDetailLabel",
1404
+ children: t("browserCompat")
1405
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1406
+ className: "lpc-catJson",
1407
+ children: JSON.stringify(model.compat, null, 2)
1408
+ })] }) : null,
1409
+ restKeys.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1410
+ className: "lpc-catDetailLabel",
1411
+ children: t("browserExtra")
1412
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1413
+ className: "lpc-catJson",
1414
+ children: restKeys.map((key) => `${key}: ${shortValue(model.rest[key])}`).join("\n")
1415
+ })] }) : null,
1416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1417
+ className: "lpc-catParams",
1418
+ children: `baseUrl:${model.baseUrl}`
1419
+ })
1420
+ ]
1421
+ }) : null
1422
+ ]
1423
+ });
1424
+ }
1425
+ //#endregion
1426
+ //#region src/client/views/catalog-browser.tsx
1427
+ /**
1428
+ * 内置模型目录浏览器:搜索(服务端模糊匹配)/筛选/分页 + 复制路径 id +
1429
+ * 一键把 `{id, extends: 'provider/model'}` 加到目标 route。
1430
+ *
1431
+ * 数据全部来自本插件的 `/catalog` 端点(浏览器半读不到 pi-ai 包):
1432
+ * - `apis`(provider → modelId → api)用于推断目标 route 的生效协议,
1433
+ * 在添加前拦下"协议不一致/重复 id/协议不受支持"这三类必然失败的添加;
1434
+ * - 模型行的参数直接渲染服务端给的事实(含官方新增字段,见 `rest`)。
1435
+ * 折叠由外层(card.tsx)控制,故本组件只管内容与交互。
1436
+ *
1437
+ * 结构:`useCatalogSearch`(拉取/分页状态)+ `BrowserToolbar`(搜索与筛选条)
1438
+ * + `CatalogRow`(明细行),`CatalogBrowser` 只做编排。
1439
+ * @module llm-pi/client/views/catalog-browser
1440
+ */
1441
+ /** 计数展示(千分位;不引本地化依赖)。 */
1442
+ function count(value) {
1443
+ return value.toLocaleString();
1444
+ }
1445
+ /** 一页条数(服务端同样有上限;"加载更多"按此翻页)。 */
1446
+ const PAGE_SIZE = 30;
1447
+ /** 搜索去抖毫秒(本地端点,短去抖即可)。 */
1448
+ const DEBOUNCE_MS = 180;
1449
+ const EMPTY_STATE = {
1450
+ items: [],
1451
+ total: 0,
1452
+ servableHidden: 0,
1453
+ loading: false,
1454
+ error: ""
1455
+ };
1456
+ /**
1457
+ * 目录搜索状态机:条件变化重新拉第一页(旧请求按 requestId 丢弃,避免竞态覆盖),
1458
+ * `loadMore` 追加下一页。
1459
+ */
1460
+ function useCatalogSearch(filters, enabled) {
1461
+ const [state, setState] = (0, react.useState)(EMPTY_STATE);
1462
+ const requestId = (0, react.useRef)(0);
1463
+ (0, react.useEffect)(() => {
1464
+ if (!enabled) return;
1465
+ const id = requestId.current + 1;
1466
+ requestId.current = id;
1467
+ setState((prev) => ({
1468
+ ...prev,
1469
+ loading: true,
1470
+ error: ""
1471
+ }));
1472
+ const controller = new AbortController();
1473
+ searchCatalog({
1474
+ ...filters,
1475
+ offset: 0,
1476
+ limit: PAGE_SIZE
1477
+ }, controller.signal).then((page) => {
1478
+ if (requestId.current !== id) return;
1479
+ setState({
1480
+ items: page.items,
1481
+ total: page.total,
1482
+ servableHidden: page.servableHidden,
1483
+ loading: false,
1484
+ error: ""
1485
+ });
1486
+ }).catch((error) => {
1487
+ if (requestId.current !== id) return;
1488
+ setState((prev) => ({
1489
+ ...prev,
1490
+ loading: false,
1491
+ error: error instanceof Error ? error.message : String(error)
1492
+ }));
1493
+ });
1494
+ return () => controller.abort();
1495
+ }, [filters, enabled]);
1496
+ const loadMore = () => {
1497
+ const id = requestId.current + 1;
1498
+ requestId.current = id;
1499
+ setState((prev) => ({
1500
+ ...prev,
1501
+ loading: true
1502
+ }));
1503
+ searchCatalog({
1504
+ ...filters,
1505
+ offset: state.items.length,
1506
+ limit: PAGE_SIZE
1507
+ }).then((page) => {
1508
+ if (requestId.current !== id) return;
1509
+ setState((prev) => ({
1510
+ ...prev,
1511
+ items: [...prev.items, ...page.items],
1512
+ total: page.total,
1513
+ servableHidden: page.servableHidden,
1514
+ loading: false
1515
+ }));
1516
+ }).catch((error) => {
1517
+ if (requestId.current !== id) return;
1518
+ setState((prev) => ({
1519
+ ...prev,
1520
+ loading: false,
1521
+ error: error instanceof Error ? error.message : String(error)
1522
+ }));
1523
+ });
1524
+ };
1525
+ return {
1526
+ ...state,
1527
+ loadMore
1528
+ };
1529
+ }
1530
+ /** 搜索框输入(本地态)→ 去抖后的查询串。 */
1531
+ function useDebouncedQuery(query) {
1532
+ const [debounced, setDebounced] = (0, react.useState)("");
1533
+ (0, react.useEffect)(() => {
1534
+ const timer = setTimeout(() => setDebounced(query), DEBOUNCE_MS);
1535
+ return () => clearTimeout(timer);
1536
+ }, [query]);
1537
+ return debounced;
1538
+ }
1539
+ /** 搜索/筛选/目标 route 三条工具条。 */
1540
+ function BrowserToolbar(props) {
1541
+ const { t } = props;
1542
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1544
+ className: "lpc-catBar",
1545
+ children: [
1546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1547
+ className: "lpc-input lpc-catSearch",
1548
+ type: "search",
1549
+ value: props.query,
1550
+ placeholder: t("browserSearchPlaceholder"),
1551
+ "aria-label": t("browserSearch"),
1552
+ disabled: props.disabled,
1553
+ onChange: (event) => props.onQuery(event.target.value)
1554
+ }),
1555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1556
+ className: "lpc-input lpc-select lpc-catSelect",
1557
+ value: props.provider,
1558
+ "aria-label": t("browserProvider"),
1559
+ disabled: props.disabled,
1560
+ onChange: (event) => props.onProvider(event.target.value),
1561
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1562
+ value: "",
1563
+ children: `${t("browserProvider")}:${t("browserAll")}`
1564
+ }), props.providers.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1565
+ value: entry.id,
1566
+ children: `${entry.id}(${entry.modelCount})`
1567
+ }, entry.id))]
1568
+ }),
1569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1570
+ className: "lpc-input lpc-select lpc-catSelect",
1571
+ value: props.api,
1572
+ "aria-label": t("browserApi"),
1573
+ disabled: props.disabled,
1574
+ onChange: (event) => props.onApi(event.target.value),
1575
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1576
+ value: "",
1577
+ children: `${t("browserApi")}:${t("browserAll")}`
1578
+ }), protocolOptions().map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1579
+ value: id,
1580
+ children: id
1581
+ }, id))]
1582
+ })
1583
+ ]
1584
+ }),
1585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1586
+ className: "lpc-catBar",
1587
+ children: [
1588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1589
+ prefix: "lpc",
1590
+ id: "lpc-cat-servable",
1591
+ label: t("browserOnlyServable"),
1592
+ checked: props.servableOnly,
1593
+ disabled: props.disabled,
1594
+ onEdit: props.onServableOnly
1595
+ }),
1596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1597
+ prefix: "lpc",
1598
+ id: "lpc-cat-reasoning",
1599
+ label: t("browserReasoning"),
1600
+ checked: props.reasoning,
1601
+ disabled: props.disabled,
1602
+ onEdit: props.onReasoning
1603
+ }),
1604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1605
+ prefix: "lpc",
1606
+ id: "lpc-cat-image",
1607
+ label: t("browserImage"),
1608
+ checked: props.image,
1609
+ disabled: props.disabled,
1610
+ onEdit: props.onImage
1611
+ })
1612
+ ]
1613
+ }),
1614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1615
+ className: "lpc-catBar",
1616
+ children: [
1617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1618
+ className: "lpc-catLabel",
1619
+ htmlFor: "lpc-cat-target",
1620
+ children: t("browserTarget")
1621
+ }),
1622
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1623
+ id: "lpc-cat-target",
1624
+ className: "lpc-input lpc-select lpc-catSelect",
1625
+ value: props.target,
1626
+ disabled: props.disabled || props.targets.length === 0,
1627
+ onChange: (event) => props.onTarget(event.target.value),
1628
+ children: [props.targets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1629
+ value: "",
1630
+ children: "-"
1631
+ }) : null, props.targets.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1632
+ value: route,
1633
+ children: route
1634
+ }, route))]
1635
+ }),
1636
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1637
+ className: "lpc-catNote",
1638
+ children: props.targetApi === void 0 ? t("browserNoProtocol") : `${t("browserTargetProtocol")}:${props.targetApi}`
1639
+ })
1640
+ ]
1641
+ })
1642
+ ] });
1643
+ }
1644
+ function CatalogBrowser(props) {
1645
+ const { t } = props;
1646
+ const [query, setQuery] = (0, react.useState)("");
1647
+ const debounced = useDebouncedQuery(query);
1648
+ const [provider, setProvider] = (0, react.useState)("");
1649
+ const [api, setApi] = (0, react.useState)("");
1650
+ const [reasoning, setReasoning] = (0, react.useState)(false);
1651
+ const [image, setImage] = (0, react.useState)(false);
1652
+ const [servableOnly, setServableOnly] = (0, react.useState)(true);
1653
+ const [note, setNote] = (0, react.useState)("");
1654
+ const { items, total, servableHidden, loading, error, loadMore } = useCatalogSearch((0, react.useMemo)(() => ({
1655
+ q: debounced,
1656
+ provider,
1657
+ api,
1658
+ reasoning,
1659
+ image,
1660
+ servableOnly
1661
+ }), [
1662
+ debounced,
1663
+ provider,
1664
+ api,
1665
+ reasoning,
1666
+ image,
1667
+ servableOnly
1668
+ ]), props.metaError === void 0);
1669
+ if (props.metaError !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1670
+ className: "lpc-invalid",
1671
+ children: `${t("browserFailed")}${props.metaError}`
1672
+ });
1673
+ const onAdd = (model) => {
1674
+ if (props.target === "") return;
1675
+ const added = props.onAdd(props.target, model);
1676
+ setNote(added ? `${t("browserAddedTo")} ${props.target}:${model.path}` : `${t("blockDuplicate")}:${model.path}`);
1677
+ };
1678
+ const disabled = props.disabled === true;
1679
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1680
+ className: "lpc-cat",
1681
+ children: [
1682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1408
1683
  className: "lpc-hint",
1409
- children: note
1684
+ children: t("browserHint")
1685
+ }),
1686
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BrowserToolbar, {
1687
+ t,
1688
+ query,
1689
+ onQuery: setQuery,
1690
+ provider,
1691
+ onProvider: setProvider,
1692
+ api,
1693
+ onApi: setApi,
1694
+ reasoning,
1695
+ onReasoning: setReasoning,
1696
+ image,
1697
+ onImage: setImage,
1698
+ servableOnly,
1699
+ onServableOnly: setServableOnly,
1700
+ providers: props.providers,
1701
+ targets: props.targets,
1702
+ target: props.target,
1703
+ onTarget: props.onTargetChange,
1704
+ targetApi: props.target === "" ? void 0 : props.routeApiOf(props.target),
1705
+ disabled
1706
+ }),
1707
+ props.targets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1708
+ className: "lpc-hint",
1709
+ children: t("browserNoTarget")
1710
+ }) : null,
1711
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1712
+ className: "lpc-catSummary",
1713
+ children: [
1714
+ `${t("browserMatched")} ${count(total)}`,
1715
+ servableHidden > 0 ? ` · ${t("browserHiddenServable")} ${count(servableHidden)} ${t("browserHiddenUnit")}` : "",
1716
+ loading ? ` · ${t("browserLoading")}` : ""
1717
+ ]
1718
+ }),
1719
+ error !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1720
+ className: "lpc-invalid",
1721
+ children: `${t("browserFailed")}${error}`
1410
1722
  }) : null,
1411
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
1412
- id: listId,
1413
- children: candidateModels.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: id }, id))
1723
+ !loading && error === "" && items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1724
+ className: "lpc-hint",
1725
+ children: t("browserEmpty")
1726
+ }) : null,
1727
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1728
+ className: "lpc-catList",
1729
+ children: items.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogRow, {
1730
+ t,
1731
+ model,
1732
+ disabled,
1733
+ eligibility: addEligibility({
1734
+ model,
1735
+ provider: props.providerOf(props.target),
1736
+ apis: props.apis
1737
+ }),
1738
+ onAdd: () => onAdd(model)
1739
+ }, model.path))
1740
+ }),
1741
+ items.length > 0 && items.length < total ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1742
+ className: "lpc-catMore",
1743
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1744
+ type: "button",
1745
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1746
+ disabled: loading,
1747
+ onClick: loadMore,
1748
+ children: t("browserLoadMore")
1749
+ })
1750
+ }) : null,
1751
+ note !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1752
+ className: "lpc-catNote",
1753
+ children: note
1754
+ }) : null
1755
+ ]
1756
+ });
1757
+ }
1758
+ //#endregion
1759
+ //#region src/client/views/compat.tsx
1760
+ /** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
1761
+ function pruneCompatForApi(compat, api) {
1762
+ const group = api !== "" && compatFieldsOf(api).length > 0 ? api : COMPAT_FALLBACK_API;
1763
+ const fields = new Set(compatFieldsOf(group));
1764
+ const next = {};
1765
+ for (const [key, value] of Object.entries(compat)) if (fields.has(key)) next[key] = value;
1766
+ return next;
1767
+ }
1768
+ /** 字段数超过该值时提供过滤框(19 项一屏放不下)。 */
1769
+ const FILTER_THRESHOLD = 10;
1770
+ function CompatEditor(props) {
1771
+ const [filter, setFilter] = (0, react.useState)("");
1772
+ const effective = props.api !== "" && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API;
1773
+ const allFields = compatFieldsOf(effective);
1774
+ const needle = filter.trim().toLowerCase();
1775
+ const fields = needle === "" ? allFields : allFields.filter((f) => f.toLowerCase().includes(needle));
1776
+ const setCount = allFields.filter((field) => props.compat[field] !== void 0).length;
1777
+ const setField = (field, value) => {
1778
+ const next = { ...props.compat };
1779
+ if (value === void 0) delete next[field];
1780
+ else next[field] = value;
1781
+ props.onEdit(next);
1782
+ };
1783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1784
+ className: "lpc-field lpc-wide",
1785
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
1786
+ id: `${props.idPrefix}-collapse`,
1787
+ title: props.t("compatGroup"),
1788
+ meta: allFields.length === 0 ? "" : `${props.t("compatSetCount")} ${setCount}/${allFields.length}`,
1789
+ defaultOpen: false,
1790
+ children: [
1791
+ props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1792
+ className: "lpc-hint",
1793
+ children: props.t("compatApiHint")
1794
+ }) : null,
1795
+ allFields.length > FILTER_THRESHOLD ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1796
+ className: "lpc-catBar",
1797
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1798
+ className: "lpc-input lpc-catSearch",
1799
+ type: "search",
1800
+ value: filter,
1801
+ placeholder: props.t("compatFilter"),
1802
+ "aria-label": props.t("compatFilter"),
1803
+ disabled: props.disabled === true,
1804
+ onChange: (event) => setFilter(event.target.value)
1805
+ })
1806
+ }) : null,
1807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1808
+ className: "lpc-grid",
1809
+ children: fields.map((field) => {
1810
+ const spec = compatFieldSpec(effective, field);
1811
+ if (spec === void 0) return null;
1812
+ if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1813
+ id: `${props.idPrefix}-${field}`,
1814
+ label: field,
1815
+ value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1816
+ options: ["true", "false"],
1817
+ unsetLabel: props.t("compatUnset"),
1818
+ disabled: props.disabled === true,
1819
+ onEdit: (value) => {
1820
+ if (value === "") setField(field, void 0);
1821
+ else setField(field, value === "true");
1822
+ }
1823
+ }, field);
1824
+ if (spec === "object") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1825
+ id: `${props.idPrefix}-${field}`,
1826
+ label: field,
1827
+ value: props.compat[field],
1828
+ epoch: props.epoch,
1829
+ disabled: props.disabled === true,
1830
+ invalidText: props.t("invalidJson"),
1831
+ onEdit: (value) => setField(field, value)
1832
+ }, field);
1833
+ if (spec === "integer" || spec === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IntegerField, {
1834
+ id: `${props.idPrefix}-${field}`,
1835
+ label: field,
1836
+ value: props.compat[field],
1837
+ epoch: props.epoch,
1838
+ invalidLabel: props.t("invalidInteger"),
1839
+ disabled: props.disabled === true,
1840
+ onEdit: (value) => setField(field, value)
1841
+ }, field);
1842
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1843
+ id: `${props.idPrefix}-${field}`,
1844
+ label: field,
1845
+ value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1846
+ options: spec,
1847
+ unsetLabel: props.t("compatUnset"),
1848
+ disabled: props.disabled === true,
1849
+ onEdit: (value) => {
1850
+ if (value === "") setField(field, void 0);
1851
+ else setField(field, value);
1852
+ }
1853
+ }, field);
1854
+ })
1855
+ })
1856
+ ]
1857
+ })
1858
+ });
1859
+ }
1860
+ //#endregion
1861
+ //#region src/client/views/models.tsx
1862
+ /**
1863
+ * 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat/
1864
+ * 其余字段),**行默认折叠**——route 内模型一多,展开态会把配置页拉得极长。
1865
+ * 工具条提供"全部展开/收起"与 id/继承源过滤;继承源候选不再内联 datalist,
1866
+ * 改由卡片顶部「内置模型目录」浏览器承担(搜索 + 一键添加)。
1867
+ * @module llm-pi/client/views/models
1868
+ */
1869
+ /** 行标题摘要:继承源与显示名(空则回退 id)。 */
1870
+ function rowSummary(model) {
1871
+ const parts = [];
1872
+ if (model.extends.trim() !== "") parts.push(`extends ${model.extends.trim()}`);
1873
+ if (model.name.trim() !== "" && model.name.trim() !== model.id.trim()) parts.push(model.name.trim());
1874
+ return parts.join(" · ");
1875
+ }
1876
+ function ModelsTable(props) {
1877
+ const { t } = props;
1878
+ const [openRows, setOpenRows] = (0, react.useState)({});
1879
+ const [filter, setFilter] = (0, react.useState)("");
1880
+ const needle = filter.trim().toLowerCase();
1881
+ const visible = props.models.map((model, index) => ({
1882
+ model,
1883
+ index
1884
+ })).filter(({ model }) => needle === "" ? true : model.id.toLowerCase().includes(needle) || model.extends.toLowerCase().includes(needle) || model.name.toLowerCase().includes(needle));
1885
+ const hidden = props.models.length - visible.length;
1886
+ const allOpen = props.models.length > 0 && props.models.every((_, index) => openRows[index] === true);
1887
+ const toggleAll = () => {
1888
+ if (allOpen) {
1889
+ setOpenRows({});
1890
+ return;
1891
+ }
1892
+ setOpenRows(Object.fromEntries(props.models.map((_, index) => [index, true])));
1893
+ };
1894
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1895
+ className: "lpc-models",
1896
+ children: [
1897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1898
+ className: "lpc-modelHead",
1899
+ children: [
1900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1901
+ className: "lpc-modelTitle",
1902
+ children: `${t("modelsGroup")}(${props.models.length})`
1903
+ }),
1904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1905
+ type: "button",
1906
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1907
+ disabled: props.disabled === true || props.models.length === 0,
1908
+ onClick: toggleAll,
1909
+ children: allOpen ? t("modelsCollapseAll") : t("modelsExpandAll")
1910
+ }),
1911
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1912
+ type: "button",
1913
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1914
+ disabled: props.disabled === true,
1915
+ onClick: props.onAddFromCatalog,
1916
+ children: t("addFromCatalog")
1917
+ }),
1918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1919
+ type: "button",
1920
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1921
+ disabled: props.disabled === true,
1922
+ onClick: () => props.onModels([...props.models, emptyModelDraft()]),
1923
+ children: t("addModel")
1924
+ })
1925
+ ]
1414
1926
  }),
1415
- props.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1927
+ props.models.length > 6 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1928
+ className: "lpc-catBar",
1929
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1930
+ className: "lpc-input lpc-catSearch",
1931
+ type: "search",
1932
+ value: filter,
1933
+ placeholder: t("modelsFilterPlaceholder"),
1934
+ "aria-label": t("modelsFilter"),
1935
+ disabled: props.disabled === true,
1936
+ onChange: (event) => setFilter(event.target.value)
1937
+ }), hidden > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1938
+ className: "lpc-catNote",
1939
+ children: `${t("modelsHidden")} ${hidden}`
1940
+ }) : null]
1941
+ }) : null,
1942
+ visible.map(({ model, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1416
1943
  index,
1417
1944
  model,
1418
1945
  api: props.api,
1419
- listId,
1946
+ uid: props.route.replace(/[^a-zA-Z0-9_-]/g, "_"),
1947
+ open: openRows[index] === true,
1420
1948
  epoch: props.epoch,
1421
1949
  disabled: props.disabled === true,
1422
1950
  t,
1951
+ onToggle: () => setOpenRows((prev) => ({
1952
+ ...prev,
1953
+ [index]: prev[index] !== true
1954
+ })),
1423
1955
  onPatch: (patch) => props.onModels(props.models.map((m, i) => i === index ? {
1424
1956
  ...m,
1425
1957
  ...patch
@@ -1429,125 +1961,141 @@ ${extra}
1429
1961
  ]
1430
1962
  });
1431
1963
  }
1964
+ /** 单行:折叠头(序号/id/摘要) + 展开体(全部字段)。 */
1432
1965
  function ModelRow(props) {
1433
1966
  const { model, t } = props;
1434
- const id = `${props.listId}-m${props.index}`;
1967
+ const id = `lpc-m-${props.uid}-${props.index}`;
1968
+ const summary = rowSummary(model);
1435
1969
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
1970
  className: "lpc-modelRow",
1437
- children: [
1438
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1439
- className: "lpc-modelHead",
1440
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1441
- className: "lpc-modelTitle",
1442
- children: [
1443
- t("modelRow"),
1444
- " ",
1445
- props.index + 1
1446
- ]
1447
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1448
- type: "button",
1449
- className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1450
- disabled: props.disabled === true,
1451
- onClick: props.onRemove,
1452
- children: t("deleteModel")
1453
- })]
1454
- }),
1455
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1456
- className: "lpc-grid",
1971
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1972
+ className: "lpc-modelHead",
1973
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1974
+ type: "button",
1975
+ className: "lpc-routeToggle",
1976
+ "aria-expanded": props.open,
1977
+ onClick: props.onToggle,
1457
1978
  children: [
1458
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1459
- id: `${id}-id`,
1460
- label: t("modelId"),
1461
- hint: t("modelIdHint"),
1462
- value: model.id,
1463
- disabled: props.disabled === true,
1464
- invalid: model.id.trim() === "",
1465
- invalidLabel: t("modelIdRequired"),
1466
- onEdit: (value) => props.onPatch({ id: value })
1467
- }),
1468
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1469
- id: `${id}-extends`,
1470
- label: t("modelExtends"),
1471
- hint: t("modelExtendsHint"),
1472
- value: model.extends,
1473
- list: props.listId,
1474
- disabled: props.disabled === true,
1475
- onEdit: (value) => props.onPatch({ extends: value })
1476
- }),
1477
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1478
- id: `${id}-name`,
1479
- label: t("modelName"),
1480
- value: model.name,
1481
- disabled: props.disabled === true,
1482
- onEdit: (value) => props.onPatch({ name: value })
1483
- }),
1484
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1485
- id: `${id}-ctx`,
1486
- label: t("contextWindow"),
1487
- numeric: true,
1488
- value: model.contextWindow,
1489
- disabled: props.disabled === true,
1490
- onEdit: (value) => props.onPatch({ contextWindow: value })
1979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${props.open ? " lpc-chevronOpen" : ""}` }),
1980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1981
+ className: "lpc-modelTitle",
1982
+ children: `#${props.index + 1}`
1491
1983
  }),
1492
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1493
- id: `${id}-max`,
1494
- label: t("maxTokens"),
1495
- numeric: true,
1496
- value: model.maxTokens,
1497
- disabled: props.disabled === true,
1498
- onEdit: (value) => props.onPatch({ maxTokens: value })
1984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1985
+ className: model.id.trim() === "" ? "lpc-invalid" : "lpc-routeKey",
1986
+ children: model.id.trim() === "" ? t("modelUntitled") : model.id
1499
1987
  }),
1500
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1501
- className: "lpc-field",
1502
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1503
- className: "lpc-label",
1504
- children: t("input")
1505
- }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1506
- prefix: "lpc",
1507
- id: `${id}-input-${modality}`,
1508
- label: modality,
1509
- checked: model.input[modality],
1510
- disabled: props.disabled === true,
1511
- onEdit: (checked) => props.onPatch({ input: {
1512
- ...model.input,
1513
- [modality]: checked
1514
- } })
1515
- }, modality))]
1516
- })
1988
+ summary !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1989
+ className: "lpc-routeApi",
1990
+ children: summary
1991
+ }) : null
1517
1992
  ]
1518
- }),
1519
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEditor, {
1520
- idPrefix: `${id}-re`,
1521
- value: model.reasoningEfforts,
1522
- disabled: props.disabled === true,
1523
- t,
1524
- onEdit: (reasoningEfforts) => props.onPatch({ reasoningEfforts })
1525
- }),
1526
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
1527
- idPrefix: `${id}-compat`,
1528
- api: props.api,
1529
- compat: model.compat,
1530
- epoch: props.epoch,
1531
- disabled: props.disabled === true,
1532
- wide: true,
1533
- t,
1534
- onEdit: (compat) => props.onPatch({ compat })
1535
- }),
1536
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1537
- id: `${id}-extra`,
1538
- label: t("extraFields"),
1539
- hint: t("extraFieldsHint"),
1540
- invalidText: t("invalidJson"),
1541
- value: Object.keys(model.extra).length === 0 ? void 0 : model.extra,
1542
- epoch: props.epoch,
1993
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1994
+ type: "button",
1995
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1543
1996
  disabled: props.disabled === true,
1544
- wide: true,
1545
- onEdit: (extra) => {
1546
- const next = extraOfJson(extra);
1547
- if (next !== void 0) props.onPatch({ extra: next });
1548
- }
1549
- })
1550
- ]
1997
+ onClick: props.onRemove,
1998
+ children: t("deleteModel")
1999
+ })]
2000
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2001
+ className: "lpc-modelBody",
2002
+ children: [
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2004
+ className: "lpc-grid",
2005
+ children: [
2006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
2007
+ id: `${id}-id`,
2008
+ label: t("modelId"),
2009
+ hint: t("modelIdHint"),
2010
+ value: model.id,
2011
+ disabled: props.disabled === true,
2012
+ invalid: model.id.trim() === "",
2013
+ invalidLabel: t("modelIdRequired"),
2014
+ onEdit: (value) => props.onPatch({ id: value })
2015
+ }),
2016
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
2017
+ id: `${id}-extends`,
2018
+ label: t("modelExtends"),
2019
+ hint: t("modelExtendsHint"),
2020
+ value: model.extends,
2021
+ disabled: props.disabled === true,
2022
+ onEdit: (value) => props.onPatch({ extends: value })
2023
+ }),
2024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
2025
+ id: `${id}-name`,
2026
+ label: t("modelName"),
2027
+ value: model.name,
2028
+ disabled: props.disabled === true,
2029
+ onEdit: (value) => props.onPatch({ name: value })
2030
+ }),
2031
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
2032
+ id: `${id}-ctx`,
2033
+ label: t("contextWindow"),
2034
+ numeric: true,
2035
+ value: model.contextWindow,
2036
+ disabled: props.disabled === true,
2037
+ onEdit: (value) => props.onPatch({ contextWindow: value })
2038
+ }),
2039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
2040
+ id: `${id}-max`,
2041
+ label: t("maxTokens"),
2042
+ numeric: true,
2043
+ value: model.maxTokens,
2044
+ disabled: props.disabled === true,
2045
+ onEdit: (value) => props.onPatch({ maxTokens: value })
2046
+ }),
2047
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2048
+ className: "lpc-field",
2049
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2050
+ className: "lpc-label",
2051
+ children: t("input")
2052
+ }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
2053
+ prefix: "lpc",
2054
+ id: `${id}-input-${modality}`,
2055
+ label: modality,
2056
+ checked: model.input[modality],
2057
+ disabled: props.disabled === true,
2058
+ onEdit: (checked) => props.onPatch({ input: {
2059
+ ...model.input,
2060
+ [modality]: checked
2061
+ } })
2062
+ }, modality))]
2063
+ })
2064
+ ]
2065
+ }),
2066
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEditor, {
2067
+ idPrefix: `${id}-re`,
2068
+ value: model.reasoningEfforts,
2069
+ disabled: props.disabled === true,
2070
+ t,
2071
+ onEdit: (reasoningEfforts) => props.onPatch({ reasoningEfforts })
2072
+ }),
2073
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
2074
+ idPrefix: `${id}-compat`,
2075
+ api: props.api,
2076
+ compat: model.compat,
2077
+ epoch: props.epoch,
2078
+ disabled: props.disabled === true,
2079
+ wide: true,
2080
+ t,
2081
+ onEdit: (compat) => props.onPatch({ compat })
2082
+ }),
2083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
2084
+ id: `${id}-extra`,
2085
+ label: t("extraFields"),
2086
+ hint: t("extraFieldsHint"),
2087
+ invalidText: t("invalidJson"),
2088
+ value: Object.keys(model.extra).length === 0 ? void 0 : model.extra,
2089
+ epoch: props.epoch,
2090
+ disabled: props.disabled === true,
2091
+ wide: true,
2092
+ onEdit: (extra) => {
2093
+ const next = extraOfJson(extra);
2094
+ if (next !== void 0) props.onPatch({ extra: next });
2095
+ }
2096
+ })
2097
+ ]
2098
+ }) : null]
1551
2099
  });
1552
2100
  }
1553
2101
  function ReasoningEditor(props) {
@@ -1739,7 +2287,7 @@ ${extra}
1739
2287
  id: `${props.id}-api`,
1740
2288
  label: t("api"),
1741
2289
  value: draft.api,
1742
- options: PROTOCOL_IDS,
2290
+ options: protocolOptions(),
1743
2291
  unsetLabel: t("compatUnset"),
1744
2292
  disabled: props.disabled === true,
1745
2293
  onEdit: (value) => props.onPatch({
@@ -1780,14 +2328,16 @@ ${extra}
1780
2328
  //#endregion
1781
2329
  //#region src/client/views/providers.tsx
1782
2330
  /**
1783
- * Provider 路由编辑:每个 route 一张可折叠小节(键名 + 标量字段 +
1784
- * headers 键值对 + retryPolicy JSON + compat + 模型目录)。
1785
- * 支持新增 route(输入键名)与删除 route。
2331
+ * Provider 路由编辑:每个 route 一张**受控**折叠小节(展开态由卡片持有,
2332
+ * 以便「从内置目录添加」后自动展开目标 route、以及卡片级"全部收起")。
2333
+ * 小节内容:基础字段 + 请求头 + 高级设置(其余字段 / retryPolicy / compat)
2334
+ * + 模型目录(行默认折叠,见 views/models.tsx)。
1786
2335
  * @module llm-pi/client/views/providers
1787
2336
  */
1788
2337
  function ProvidersSection(props) {
1789
2338
  const [newRoute, setNewRoute] = (0, react.useState)("");
1790
2339
  const [routeError, setRouteError] = (0, react.useState)("");
2340
+ const routes = Object.keys(props.providers);
1791
2341
  const submitAdd = () => {
1792
2342
  const key = newRoute.trim();
1793
2343
  if (key === "") {
@@ -1805,9 +2355,24 @@ ${extra}
1805
2355
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1806
2356
  className: "lpc-section",
1807
2357
  children: [
1808
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1809
- className: "lpc-groupLabel",
1810
- children: props.t("providersGroup")
2358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2359
+ className: "lpc-modelHead",
2360
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2361
+ className: "lpc-modelTitle",
2362
+ children: props.t("providersGroup")
2363
+ }), routes.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2364
+ type: "button",
2365
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
2366
+ disabled: props.disabled === true,
2367
+ onClick: props.onCollapseAll,
2368
+ children: props.t("providersCollapseAll")
2369
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2370
+ type: "button",
2371
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
2372
+ disabled: props.disabled === true,
2373
+ onClick: props.onExpandAll,
2374
+ children: props.t("providersExpandAll")
2375
+ })] }) : null]
1811
2376
  }),
1812
2377
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1813
2378
  className: "lpc-addRoute",
@@ -1835,20 +2400,25 @@ ${extra}
1835
2400
  Object.entries(props.providers).map(([route, draft]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderSection, {
1836
2401
  route,
1837
2402
  draft,
2403
+ open: props.openRoutes[route] === true,
1838
2404
  epoch: props.epoch,
1839
2405
  disabled: props.disabled === true,
1840
2406
  t: props.t,
2407
+ onToggle: () => props.onToggleRoute(route, props.openRoutes[route] !== true),
1841
2408
  onRemove: () => props.onRemoveRoute(route),
1842
- onPatch: (patch) => props.onPatchProvider(route, patch)
2409
+ onPatch: (patch) => props.onPatchProvider(route, patch),
2410
+ onAddFromCatalog: () => props.onAddFromCatalog(route)
1843
2411
  }, route))
1844
2412
  ]
1845
2413
  });
1846
2414
  }
1847
2415
  function ProviderSection(props) {
1848
- const [open, setOpen] = (0, react.useState)(false);
1849
2416
  const { route, draft, t } = props;
1850
2417
  const id = route.replace(/[^a-zA-Z0-9_-]/g, "_");
1851
- const summary = draft.api !== "" ? draft.api : draft.extends !== "" ? `extends ${draft.extends}` : "";
2418
+ const adapter = typeof draft.extra["adapter"] === "string" ? draft.extra["adapter"] : "pi";
2419
+ const summary = draft.api !== "" ? draft.api : draft.extends !== "" ? `extends ${draft.extends}` : adapter;
2420
+ const modelCount = draft.models.length;
2421
+ const meta = modelCount > 0 ? `${modelCount}` : "";
1852
2422
  const fieldProps = {
1853
2423
  id,
1854
2424
  draft,
@@ -1863,10 +2433,10 @@ ${extra}
1863
2433
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1864
2434
  type: "button",
1865
2435
  className: "lpc-routeToggle",
1866
- "aria-expanded": open,
1867
- onClick: () => setOpen(!open),
2436
+ "aria-expanded": props.open,
2437
+ onClick: props.onToggle,
1868
2438
  children: [
1869
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}` }),
2439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `lpc-chevron${props.open ? " lpc-chevronOpen" : ""}` }),
1870
2440
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1871
2441
  className: "lpc-routeKey",
1872
2442
  children: route
@@ -1874,6 +2444,10 @@ ${extra}
1874
2444
  summary !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1875
2445
  className: "lpc-routeApi",
1876
2446
  children: summary
2447
+ }) : null,
2448
+ meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2449
+ className: "lpc-collapseMeta",
2450
+ children: meta
1877
2451
  }) : null
1878
2452
  ]
1879
2453
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1883,7 +2457,7 @@ ${extra}
1883
2457
  onClick: props.onRemove,
1884
2458
  children: t("deleteRoute")
1885
2459
  })]
1886
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2460
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1887
2461
  className: "lpc-routeBody",
1888
2462
  children: [
1889
2463
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1949,12 +2523,12 @@ ${extra}
1949
2523
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTable, {
1950
2524
  route,
1951
2525
  api: draft.api,
1952
- defaultProvider: draft.extends,
1953
2526
  models: draft.models,
1954
2527
  epoch: props.epoch,
1955
2528
  disabled: props.disabled === true,
1956
2529
  t,
1957
- onModels: (models) => props.onPatch({ models })
2530
+ onModels: (models) => props.onPatch({ models }),
2531
+ onAddFromCatalog: props.onAddFromCatalog
1958
2532
  })
1959
2533
  ]
1960
2534
  }) : null]
@@ -1963,35 +2537,97 @@ ${extra}
1963
2537
  //#endregion
1964
2538
  //#region src/client/card.tsx
1965
2539
  /**
1966
- * 「LLM 路由」配置卡片:经 injectPluginConfigCard 注册(legacy settings.plugin.item
1967
- * 与 0.1.6-alpha.2 插件页 plugins.row.config / plugins.bundle.config,view 分发
1968
- * summary/page)。
1969
- * 顶部:enabled / catalogUrl / catalogRefreshHours / 只读状态行(kitSource、
1970
- * modelsDevStatus,来自模型目录端点)+ 保存(settings.update 全量深合并)与
1971
- * 错误/成功提示;下方为 providers 路由列表(新增/删除/字段编辑/compat/模型
1972
- * 目录,见 views/)。外壳与基础控件走 @dsh-plus/shared/client 套件。
2540
+ * 「LLM 路由」配置卡片:经 injectPluginConfigCard 注册(插件页
2541
+ * plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
2542
+ *
2543
+ * 页面结构(自上而下,除根字段外**默认全部收起**,避免长列表划不到头):
2544
+ * 1. 根字段:启用开关 + 运行期状态行(生效版本/安装树/目录规模/协议/compat 来源/诊断);
2545
+ * 2. 「内置模型目录」浏览器(折叠小节,默认收起):搜索/筛选/复制路径 id/
2546
+ * 一键把 `{id, extends}` 加到目标 route(添加后自动展开该 route 并回显);
2547
+ * 3. Provider 路由列表(每 route 收起;route 内模型行同样默认收起)。
2548
+ *
2549
+ * 数据来源:配置读写走官方 remote.settings(scope/api);运行期事实与目录搜索
2550
+ * 走本插件 `/catalog` 端点(浏览器半读不到 pi-ai 包)。草稿/保存与目录索引各由
2551
+ * 本文件的 hook 承担,全部状态(浏览器开合、目标 route、route 展开态)在卡片持有,
2552
+ * 子组件受控。
1973
2553
  * @module llm-pi/client/card
1974
2554
  */
1975
- /** 运行期诊断行数据来源:模型目录端点(compat 字段表同批安装)。 */
1976
- function useRuntimeDiagnostics() {
1977
- const [kitSource, setKitSource] = (0, react.useState)(null);
1978
- const [modelsDevStatus, setModelsDevStatus] = (0, react.useState)(null);
2555
+ /** 运行期事实 + 目录索引:卡片挂载拉一次(同时安装协议集合与 compat 字段表)。 */
2556
+ function useCatalogMeta() {
2557
+ const [meta, setMeta] = (0, react.useState)(null);
2558
+ const [error, setError] = (0, react.useState)("");
1979
2559
  (0, react.useEffect)(() => {
1980
2560
  let alive = true;
1981
- fetchCatalog("", "models-dev").then((result) => {
2561
+ fetchCatalogMeta().then((result) => {
2562
+ if (!alive) return;
2563
+ installProtocols(result.kit.protocols);
2564
+ installCompatFields(result.compat.fields);
2565
+ setMeta(result);
2566
+ }).catch((reason) => {
1982
2567
  if (!alive) return;
1983
- setKitSource(result.kitSource ?? null);
1984
- setModelsDevStatus(result.status ?? null);
1985
- if (result.compat !== void 0) installCompatFields(result.compat.fields);
1986
- }).catch(() => {});
2568
+ setError(reason instanceof Error ? reason.message : String(reason));
2569
+ });
1987
2570
  return () => {
1988
2571
  alive = false;
1989
2572
  };
1990
2573
  }, []);
1991
2574
  return {
1992
- kitSource,
1993
- modelsDevStatus,
1994
- setModelsDevStatus
2575
+ meta,
2576
+ error
2577
+ };
2578
+ }
2579
+ /**
2580
+ * 草稿与保存:首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑
2581
+ * (与官方 staged 表单一致)。dirty 由"提交形状"双向比较得出。
2582
+ */
2583
+ function useCardDraft(options) {
2584
+ const { value, scope, api, t } = options;
2585
+ const [draft, setDraft] = (0, react.useState)(null);
2586
+ const [epoch, setEpoch] = (0, react.useState)(0);
2587
+ const [saving, setSaving] = (0, react.useState)(false);
2588
+ const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
2589
+ (0, react.useEffect)(() => {
2590
+ if (value === void 0 || draft !== null) return;
2591
+ setDraft(draftFromValue(value));
2592
+ }, [value, draft]);
2593
+ const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
2594
+ const onSave = () => {
2595
+ if (draft === null) return;
2596
+ setSaving(true);
2597
+ const revision = scope.getSnapshot().revision;
2598
+ api.update(toPatch(draft), revision).then(async () => {
2599
+ await scope.load();
2600
+ const next = scope.getSnapshot().value;
2601
+ if (next !== void 0) setDraft(draftFromValue(next));
2602
+ setEpoch((current) => current + 1);
2603
+ setStatus({
2604
+ kind: "ok",
2605
+ text: t("saveOk")
2606
+ });
2607
+ }).catch((error) => {
2608
+ const message = error instanceof Error ? error.message : String(error);
2609
+ setStatus({
2610
+ kind: "error",
2611
+ text: `${t("saveFailed")}${message}`
2612
+ });
2613
+ }).finally(() => setSaving(false));
2614
+ };
2615
+ const onDiscard = () => {
2616
+ if (value === void 0) return;
2617
+ setDraft(draftFromValue(value));
2618
+ setEpoch((current) => current + 1);
2619
+ setStatus(IDLE_STATUS);
2620
+ };
2621
+ return {
2622
+ draft,
2623
+ setDraft,
2624
+ dirty,
2625
+ saving,
2626
+ status,
2627
+ idle: () => setStatus(IDLE_STATUS),
2628
+ epoch,
2629
+ onSave,
2630
+ onDiscard
1995
2631
  };
1996
2632
  }
1997
2633
  /** 路由级草稿改写:改字段 / 新增 route / 删除 route(返回新草稿,不改入参)。 */
@@ -2025,119 +2661,82 @@ ${extra}
2025
2661
  providers: next
2026
2662
  };
2027
2663
  }
2028
- /** 卡片动作:手动拉取目录、放弃、保存。 */
2664
+ /** 卡片动作:放弃、保存(模型目录已唯一来自 pi-ai,无可拉取项)。 */
2029
2665
  function cardActions(options) {
2030
2666
  const { t } = options;
2031
- return [
2032
- {
2033
- key: "refresh",
2034
- label: t(options.refreshing ? "refreshingCatalog" : "refreshCatalog"),
2035
- disabled: options.disabled || options.refreshing,
2036
- onClick: options.onRefresh
2037
- },
2038
- {
2039
- key: "discard",
2040
- label: t("discard"),
2041
- disabled: !options.dirty || options.saving,
2042
- onClick: options.onDiscard
2043
- },
2044
- {
2045
- key: "save",
2046
- label: t(options.saving ? "saving" : "save"),
2047
- variant: "primary",
2048
- disabled: !options.dirty || options.invalid || options.saving || options.disabled,
2049
- onClick: options.onSave
2050
- }
2051
- ];
2667
+ return [{
2668
+ key: "discard",
2669
+ label: t("discard"),
2670
+ disabled: !options.dirty || options.saving,
2671
+ onClick: options.onDiscard
2672
+ }, {
2673
+ key: "save",
2674
+ label: t(options.saving ? "saving" : "save"),
2675
+ variant: "primary",
2676
+ disabled: !options.dirty || options.saving || options.disabled,
2677
+ onClick: options.onSave
2678
+ }];
2679
+ }
2680
+ /** 打开浏览器时的目标 route 选择:优先当前目标,否则第一个候选。 */
2681
+ function pickTarget(targets, preferred) {
2682
+ if (preferred !== "" && targets.includes(preferred)) return preferred;
2683
+ return targets[0] ?? "";
2052
2684
  }
2053
2685
  function LlmPiCard(props) {
2054
2686
  const { t, scope, api } = props;
2055
2687
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
2056
2688
  const value = snapshot.value;
2057
- const [open, setOpen] = (0, react.useState)(props.view === "page");
2058
- const [draft, setDraft] = (0, react.useState)(null);
2059
- const [epoch, setEpoch] = (0, react.useState)(0);
2060
- const [saving, setSaving] = (0, react.useState)(false);
2061
- const [refreshing, setRefreshing] = (0, react.useState)(false);
2062
- const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
2063
- const { kitSource, modelsDevStatus, setModelsDevStatus } = useRuntimeDiagnostics();
2064
- (0, react.useEffect)(() => {
2065
- if (value === void 0 || draft !== null) return;
2066
- setDraft(draftFromValue(value));
2067
- }, [value, draft]);
2068
- const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
2069
- const invalid = (0, react.useMemo)(() => {
2070
- if (draft === null) return false;
2071
- return !numTextOk(draft.catalogRefreshHours) || Object.values(draft.providers).some((provider) => provider.models.some((model) => model.id.trim() === ""));
2072
- }, [draft]);
2689
+ const { draft, setDraft, dirty, saving, status, idle, epoch, onSave, onDiscard } = useCardDraft({
2690
+ value,
2691
+ scope,
2692
+ api,
2693
+ t
2694
+ });
2695
+ const [browserOpen, setBrowserOpen] = (0, react.useState)(false);
2696
+ const [browserTarget, setBrowserTarget] = (0, react.useState)("");
2697
+ const [openRoutes, setOpenRoutes] = (0, react.useState)({});
2698
+ const { meta, error: metaError } = useCatalogMeta();
2073
2699
  if (props.view === "summary") return t("summaryLine");
2074
- const variant = cardVariantFor(props.view);
2075
2700
  if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
2076
2701
  prefix: "lpc",
2077
- variant,
2078
2702
  text: t("loading")
2079
2703
  });
2080
- const setProvider = (route, patch) => {
2081
- setDraft(withProviderPatch(draft, route, patch));
2082
- setStatus(IDLE_STATUS);
2704
+ const apis = meta?.apis ?? {};
2705
+ const targets = targetRoutes(draft.providers);
2706
+ const target = pickTarget(targets, browserTarget);
2707
+ const openBrowserFor = (route) => {
2708
+ setBrowserTarget(route);
2709
+ setBrowserOpen(true);
2083
2710
  };
2084
2711
  const onAddRoute = (key) => {
2085
2712
  setDraft(withNewRoute(draft, key));
2086
- setStatus(IDLE_STATUS);
2713
+ setOpenRoutes((prev) => ({
2714
+ ...prev,
2715
+ [key]: true
2716
+ }));
2717
+ setBrowserTarget((prev) => prev === "" ? key : prev);
2718
+ idle();
2087
2719
  };
2088
- const onRemoveRoute = (route) => {
2089
- setDraft(withoutRoute(draft, route));
2090
- setStatus(IDLE_STATUS);
2091
- };
2092
- const onSave = () => {
2093
- setSaving(true);
2094
- const revision = scope.getSnapshot().revision;
2095
- api.update(toPatch(draft), revision).then(async () => {
2096
- await scope.load();
2097
- const next = scope.getSnapshot().value;
2098
- if (next !== void 0) setDraft(draftFromValue(next));
2099
- setEpoch((value) => value + 1);
2100
- setStatus({
2101
- kind: "ok",
2102
- text: t("saveOk")
2103
- });
2104
- }).catch((error) => {
2105
- const message = error instanceof Error ? error.message : String(error);
2106
- setStatus({
2107
- kind: "error",
2108
- text: `${t("saveFailed")}${message}`
2109
- });
2110
- }).finally(() => setSaving(false));
2111
- };
2112
- const onDiscard = () => {
2113
- setDraft(draftFromValue(value));
2114
- setEpoch((value) => value + 1);
2115
- setStatus(IDLE_STATUS);
2116
- };
2117
- const onRefreshCatalog = () => {
2118
- setRefreshing(true);
2119
- refreshCatalog().then((result) => {
2120
- setModelsDevStatus(result.status);
2121
- setStatus({
2122
- kind: "ok",
2123
- text: t("refreshOk")
2124
- });
2125
- }).catch((error) => {
2126
- const message = error instanceof Error ? error.message : String(error);
2127
- setStatus({
2128
- kind: "error",
2129
- text: `${t("refreshFailed")}${message}`
2130
- });
2131
- }).finally(() => setRefreshing(false));
2720
+ /**
2721
+ * 目录一键添加:把 `{id, extends: 'provider/model'}` 写进目标 route 草稿
2722
+ * (不预填 name/容量——继承才能跟随 pi-ai 目录升级),并展开该 route 给出反馈。
2723
+ */
2724
+ const onAddModel = (route, model) => {
2725
+ const result = addModelEntry(draft, route, model);
2726
+ if (!result.added) return false;
2727
+ setDraft(result.draft);
2728
+ setOpenRoutes((prev) => ({
2729
+ ...prev,
2730
+ [route]: true
2731
+ }));
2732
+ idle();
2733
+ return true;
2132
2734
  };
2133
2735
  const disabled = !snapshot.writable;
2134
2736
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
2135
2737
  prefix: "lpc",
2136
2738
  title: t("title"),
2137
2739
  description: t("description"),
2138
- open,
2139
- onToggle: setOpen,
2140
- variant,
2141
2740
  statusBadge: {
2142
2741
  text: t(draft.enabled ? "enabledOn" : "enabledOff"),
2143
2742
  on: draft.enabled
@@ -2149,36 +2748,74 @@ ${extra}
2149
2748
  actions: cardActions({
2150
2749
  t,
2151
2750
  dirty,
2152
- invalid,
2153
2751
  saving,
2154
- refreshing,
2155
2752
  disabled,
2156
- onRefresh: onRefreshCatalog,
2157
2753
  onDiscard,
2158
2754
  onSave
2159
2755
  }),
2160
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootFields, {
2161
- t,
2162
- draft,
2163
- disabled,
2164
- kitSource,
2165
- modelsDevStatus,
2166
- onEdit: (patch) => {
2167
- setDraft({
2168
- ...draft,
2169
- ...patch
2170
- });
2171
- setStatus(IDLE_STATUS);
2172
- }
2173
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
2174
- providers: draft.providers,
2175
- epoch,
2176
- disabled,
2177
- t,
2178
- onAddRoute,
2179
- onRemoveRoute,
2180
- onPatchProvider: setProvider
2181
- })]
2756
+ children: [
2757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootFields, {
2758
+ t,
2759
+ draft,
2760
+ disabled,
2761
+ kit: meta?.kit ?? null,
2762
+ kitError: metaError,
2763
+ onEdit: (patch) => {
2764
+ setDraft({
2765
+ ...draft,
2766
+ ...patch
2767
+ });
2768
+ idle();
2769
+ }
2770
+ }),
2771
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapseSection, {
2772
+ id: "lpc-catalog-browser",
2773
+ title: t("browserGroup"),
2774
+ defaultOpen: false,
2775
+ open: browserOpen,
2776
+ onToggle: () => setBrowserOpen((open) => !open),
2777
+ meta: meta === null ? "" : `${meta.kit.catalog.models}`,
2778
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogBrowser, {
2779
+ t,
2780
+ targets,
2781
+ apis,
2782
+ providers: meta?.providers ?? [],
2783
+ target,
2784
+ onTargetChange: setBrowserTarget,
2785
+ providerOf: (route) => draft.providers[route],
2786
+ routeApiOf: (route) => {
2787
+ const provider = draft.providers[route];
2788
+ return provider === void 0 ? void 0 : routeApiFacts(provider, apis).api;
2789
+ },
2790
+ onAdd: onAddModel,
2791
+ disabled,
2792
+ ...metaError === "" ? {} : { metaError }
2793
+ })
2794
+ }),
2795
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
2796
+ providers: draft.providers,
2797
+ openRoutes,
2798
+ epoch,
2799
+ disabled,
2800
+ t,
2801
+ onAddRoute,
2802
+ onRemoveRoute: (route) => {
2803
+ setDraft(withoutRoute(draft, route));
2804
+ idle();
2805
+ },
2806
+ onPatchProvider: (route, patch) => {
2807
+ setDraft(withProviderPatch(draft, route, patch));
2808
+ idle();
2809
+ },
2810
+ onToggleRoute: (route, open) => setOpenRoutes((prev) => ({
2811
+ ...prev,
2812
+ [route]: open
2813
+ })),
2814
+ onCollapseAll: () => setOpenRoutes({}),
2815
+ onExpandAll: () => setOpenRoutes(Object.fromEntries(Object.keys(draft.providers).map((route) => [route, true]))),
2816
+ onAddFromCatalog: openBrowserFor
2817
+ })
2818
+ ]
2182
2819
  });
2183
2820
  }
2184
2821
  //#endregion
@@ -2186,33 +2823,76 @@ ${extra}
2186
2823
  /**
2187
2824
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
2188
2825
  * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
2826
+ * 服务端推导出来的诊断/提示(compat problem、版本区间提示、降级诊断)按原文展示,
2827
+ * 不做二次翻译——它们是事实描述,且与日志同一份文本。
2189
2828
  * @module llm-pi/client/i18n
2190
2829
  */
2191
2830
  const NS = "dsh-plus-llm-pi";
2192
2831
  const ownZh = {
2193
2832
  title: "LLM 路由(llm-pi)",
2194
- description: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
2195
- summaryLine: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
2833
+ description: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
2834
+ summaryLine: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
2196
2835
  enabled: "启用插件",
2197
- catalogUrl: "models.dev 目录数据端点",
2198
- catalogUrlHint: "快照数据源;一般无需修改。",
2199
- catalogRefreshHours: "目录刷新间隔(小时)",
2200
- catalogRefreshHoursHint: "0 = 不自动拉取(可手动拉取或读已有缓存);>0 = 每 N 小时自动拉取。",
2201
- catalogProxy: "拉取代理地址",
2202
- catalogProxyHint: "HTTP 代理(如 http://127.0.0.1:7890);留空直连。",
2203
- kitSource: "模块来源",
2204
- modelsDevStatus: "models.dev 快照",
2205
- modelsDevEmpty: "未拉取(无缓存数据;可手动拉取)",
2206
- modelsDevStatusLine: "已加载",
2207
- modelsDevError: "加载失败:",
2208
- refreshCatalog: "手动拉取",
2209
- refreshingCatalog: "拉取中…",
2210
- refreshOk: "目录已拉取。",
2211
- refreshFailed: "拉取失败:",
2836
+ runtimeGroup: "运行期套件",
2837
+ runtimeSource: "来源",
2838
+ runtimeRoot: "安装树",
2839
+ runtimeVersions: "生效版本",
2840
+ runtimeCatalog: "内置模型目录",
2841
+ runtimeProtocols: "可服务协议",
2842
+ runtimeCompat: "compat 门控表",
2843
+ runtimeDiagnostics: "诊断",
2844
+ runtimeVersionUnknown: "未知",
2845
+ runtimeCatalogUnit: "个 provider /",
2846
+ runtimeCatalogModels: "个模型",
2847
+ runtimeCatalogGenerated: "数据生成于",
2848
+ runtimeNotice: "提示",
2849
+ runtimeDerived: "现场推导",
2850
+ runtimeFallback: "回退快照",
2851
+ browserGroup: "内置模型目录",
2852
+ browserHint: "浏览 pi-ai 内置目录(随已装 dsh 的 pi-ai 版本自动更新):模糊搜索、按供应商/协议/能力筛选;复制路径 id,或一键把「模型 id + 继承源」加到某个 route。",
2853
+ browserSearch: "模糊搜索",
2854
+ browserSearchPlaceholder: "模型 id / 显示名 / provider,多词用空格",
2855
+ browserProvider: "供应商",
2856
+ browserApi: "协议",
2857
+ browserAll: "全部",
2858
+ browserOnlyServable: "仅本插件可服务",
2859
+ browserReasoning: "推理",
2860
+ browserImage: "图片输入",
2861
+ browserTarget: "目标 route",
2862
+ browserNoTarget: "还没有可添加的 route:先在上方新增一个 route(adapter: deepseek 的路由不适用 pi-ai 继承)。",
2863
+ browserMatched: "命中",
2864
+ browserHiddenServable: "另有协议不受支持的模型",
2865
+ browserHiddenUnit: "个已隐藏",
2866
+ browserLoadMore: "加载更多",
2867
+ browserLoading: "目录加载中…",
2868
+ browserEmpty: "没有命中的模型。",
2869
+ browserFailed: "目录加载失败:",
2870
+ browserAdd: "添加",
2871
+ browserAdded: "已添加",
2872
+ browserCopy: "复制路径 id",
2873
+ browserCopied: "已复制",
2874
+ browserDetails: "参数明细",
2875
+ browserCompat: "compat",
2876
+ browserExtra: "其余参数",
2877
+ browserThinkMap: "思考档位",
2878
+ browserInputText: "文本",
2879
+ browserInputImage: "图片",
2880
+ browserContextShort: "上下文",
2881
+ browserMaxShort: "输出",
2882
+ browserAddedTo: "已添加到 route",
2883
+ browserTargetProtocol: "当前协议",
2884
+ browserNoProtocol: "协议待定",
2885
+ blockUnsupported: "该协议本插件无法服务(不可添加)",
2886
+ blockDuplicate: "该 route 已有同 id 模型",
2887
+ blockConflict: "与目标 route 的协议不一致",
2888
+ blockMixed: "目标 route 已混用多个协议,请先修正",
2889
+ blockNoRoute: "目标 route 不存在",
2212
2890
  advancedGroup: "高级设置(retryPolicy / compat)",
2213
2891
  extraFields: "其余字段(JSON)",
2214
2892
  extraFieldsHint: "表单未覆盖的 wire 字段原样往返:如 adapter=deepseek 的 thinking / reasoningEffort、Files API 与图片配额等。对象整体合并进该 route(模型级同 key 覆盖 route 级),非法 JSON 不会提交。",
2215
2893
  providersGroup: "Provider 路由",
2894
+ providersCollapseAll: "全部收起",
2895
+ providersExpandAll: "全部展开",
2216
2896
  addRoute: "新增 route",
2217
2897
  addRoutePlaceholder: "新 route 键名(如 my-llm)",
2218
2898
  routeEmpty: "route 键名不能为空。",
@@ -2220,7 +2900,7 @@ ${extra}
2220
2900
  deleteRoute: "删除 route",
2221
2901
  providerFields: "基础字段",
2222
2902
  extends: "继承内置 provider",
2223
- extendsHint: "如 openai、anthropic;提供 api/baseURL 默认值与模型查找源。",
2903
+ extendsHint: "如 openai、anthropic(候选见「内置模型目录」);提供 api/baseURL 默认值与模型查找源。",
2224
2904
  displayName: "显示名",
2225
2905
  api: "线协议",
2226
2906
  baseURL: "端点 URL",
@@ -2251,52 +2931,98 @@ ${extra}
2251
2931
  compatGroup: "Compat 覆盖",
2252
2932
  compatApiHint: "api 未设置时暂按 openai-completions 字段组渲染;保存时后端按实际协议校验。",
2253
2933
  compatUnset: "未设置",
2934
+ compatSetCount: "已设",
2935
+ compatFilter: "过滤字段",
2254
2936
  modelsGroup: "模型目录",
2255
2937
  addModel: "添加模型",
2256
- deleteModel: "删除",
2938
+ addFromCatalog: "从内置目录添加",
2939
+ modelsCollapseAll: "全部收起",
2940
+ modelsExpandAll: "全部展开",
2941
+ modelsFilter: "过滤模型",
2942
+ modelsFilterPlaceholder: "按 id / 继承源筛选",
2943
+ modelsHidden: "已按筛选隐藏",
2257
2944
  modelRow: "模型",
2945
+ modelUntitled: "(未填 id)",
2946
+ deleteModel: "删除",
2258
2947
  modelId: "模型 id",
2259
2948
  modelIdHint: "发送给 provider 的标识,必填。",
2260
2949
  modelIdRequired: "模型 id 必填。",
2261
2950
  modelExtends: "继承源",
2262
- modelExtendsHint: "\"provider/model\" 或裸 model id(候选来自下方目录)。",
2951
+ modelExtendsHint: "\"provider/model\" 或裸 model id;候选见「内置模型目录」。",
2263
2952
  modelName: "显示名",
2264
2953
  contextWindow: "上下文容量",
2265
2954
  maxTokens: "输出上限",
2266
2955
  input: "输入模态",
2267
2956
  reasoningEfforts: "reasoningEfforts",
2268
2957
  nonReasoning: "非推理模型",
2269
- catalogSource: "候选来源",
2270
- catalogProvider: "候选 provider",
2271
- catalogLoading: "目录加载中…",
2272
- catalogFailed: "目录加载失败。",
2273
2958
  saveOk: "已保存。",
2274
2959
  saveFailed: "保存失败:"
2275
2960
  };
2276
2961
  const ownEn = {
2277
2962
  title: "LLM routes (llm-pi)",
2278
- description: "Custom LLM routes: protocol, compat, model catalog and models.dev fallback.",
2279
- summaryLine: "Custom LLM routes: protocol, compat, model catalog and models.dev fallback.",
2963
+ description: "Custom LLM routes: protocol, compat, model inheritance; the model catalog comes solely from the pi-ai built-in catalog (following the installed dsh).",
2964
+ summaryLine: "Custom LLM routes: protocol, compat, model inheritance; the model catalog comes solely from the pi-ai built-in catalog.",
2280
2965
  enabled: "Enable plugin",
2281
- catalogUrl: "models.dev catalog endpoint",
2282
- catalogUrlHint: "Snapshot data source; usually no change needed.",
2283
- catalogRefreshHours: "Catalog refresh (hours)",
2284
- catalogRefreshHoursHint: "0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.",
2285
- catalogProxy: "Fetch proxy",
2286
- catalogProxyHint: "HTTP proxy (e.g. http://127.0.0.1:7890); leave empty for direct.",
2287
- kitSource: "Module source",
2288
- modelsDevStatus: "models.dev snapshot",
2289
- modelsDevEmpty: "Not fetched (no cached data; use manual refresh)",
2290
- modelsDevStatusLine: "Loaded",
2291
- modelsDevError: "Load failed: ",
2292
- refreshCatalog: "Refresh now",
2293
- refreshingCatalog: "Refreshing…",
2294
- refreshOk: "Catalog refreshed.",
2295
- refreshFailed: "Refresh failed: ",
2966
+ runtimeGroup: "Runtime kit",
2967
+ runtimeSource: "Source",
2968
+ runtimeRoot: "Install tree",
2969
+ runtimeVersions: "Effective versions",
2970
+ runtimeCatalog: "Built-in catalog",
2971
+ runtimeProtocols: "Servable protocols",
2972
+ runtimeCompat: "compat gates",
2973
+ runtimeDiagnostics: "Diagnostics",
2974
+ runtimeVersionUnknown: "unknown",
2975
+ runtimeCatalogUnit: "providers /",
2976
+ runtimeCatalogModels: "models",
2977
+ runtimeCatalogGenerated: "data generated at",
2978
+ runtimeNotice: "Notice",
2979
+ runtimeDerived: "derived live",
2980
+ runtimeFallback: "fallback snapshot",
2981
+ browserGroup: "Built-in model catalog",
2982
+ browserHint: "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.",
2983
+ browserSearch: "Fuzzy search",
2984
+ browserSearchPlaceholder: "model id / display name / provider; space-separated terms",
2985
+ browserProvider: "Provider",
2986
+ browserApi: "Protocol",
2987
+ browserAll: "All",
2988
+ browserOnlyServable: "Servable only",
2989
+ browserReasoning: "Reasoning",
2990
+ browserImage: "Image input",
2991
+ browserTarget: "Target route",
2992
+ browserNoTarget: "No route to add to yet: create one above (adapter: deepseek routes do not use pi-ai inheritance).",
2993
+ browserMatched: "Matched",
2994
+ browserHiddenServable: "Unsupported-protocol models",
2995
+ browserHiddenUnit: "hidden",
2996
+ browserLoadMore: "Load more",
2997
+ browserLoading: "Loading catalog…",
2998
+ browserEmpty: "No matching model.",
2999
+ browserFailed: "Catalog request failed: ",
3000
+ browserAdd: "Add",
3001
+ browserAdded: "Added",
3002
+ browserCopy: "Copy path id",
3003
+ browserCopied: "Copied",
3004
+ browserDetails: "Parameters",
3005
+ browserCompat: "compat",
3006
+ browserExtra: "Other parameters",
3007
+ browserThinkMap: "Thinking levels",
3008
+ browserInputText: "text",
3009
+ browserInputImage: "image",
3010
+ browserContextShort: "context",
3011
+ browserMaxShort: "output",
3012
+ browserAddedTo: "Added to route",
3013
+ browserTargetProtocol: "protocol",
3014
+ browserNoProtocol: "protocol undecided",
3015
+ blockUnsupported: "This protocol is not servable by this plugin",
3016
+ blockDuplicate: "This route already has a model with the same id",
3017
+ blockConflict: "Protocol differs from the target route",
3018
+ blockMixed: "The target route already mixes protocols — fix it first",
3019
+ blockNoRoute: "Target route does not exist",
2296
3020
  advancedGroup: "Advanced (retryPolicy / compat)",
2297
3021
  extraFields: "Other fields (JSON)",
2298
3022
  extraFieldsHint: "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.",
2299
3023
  providersGroup: "Provider routes",
3024
+ providersCollapseAll: "Collapse all",
3025
+ providersExpandAll: "Expand all",
2300
3026
  addRoute: "Add route",
2301
3027
  addRoutePlaceholder: "New route key (e.g. my-llm)",
2302
3028
  routeEmpty: "Route key must not be empty.",
@@ -2304,7 +3030,7 @@ ${extra}
2304
3030
  deleteRoute: "Delete route",
2305
3031
  providerFields: "Basic fields",
2306
3032
  extends: "Inherit built-in provider",
2307
- extendsHint: "e.g. openai, anthropic; provides api/baseURL defaults and the model lookup source.",
3033
+ extendsHint: "e.g. openai, anthropic (see the built-in catalog below); provides api/baseURL defaults and the model lookup source.",
2308
3034
  displayName: "Display name",
2309
3035
  api: "Wire protocol",
2310
3036
  baseURL: "Base URL",
@@ -2335,25 +3061,30 @@ ${extra}
2335
3061
  compatGroup: "Compat overrides",
2336
3062
  compatApiHint: "When api is unset, fields render per openai-completions; the backend validates per the effective protocol.",
2337
3063
  compatUnset: "Unset",
3064
+ compatSetCount: "set",
3065
+ compatFilter: "Filter fields",
2338
3066
  modelsGroup: "Model catalog",
2339
3067
  addModel: "Add model",
2340
- deleteModel: "Delete",
3068
+ addFromCatalog: "Add from catalog",
3069
+ modelsCollapseAll: "Collapse all",
3070
+ modelsExpandAll: "Expand all",
3071
+ modelsFilter: "Filter models",
3072
+ modelsFilterPlaceholder: "filter by id / inheritance source",
3073
+ modelsHidden: "hidden by filter",
2341
3074
  modelRow: "Model",
3075
+ modelUntitled: "(no id yet)",
3076
+ deleteModel: "Delete",
2342
3077
  modelId: "Model id",
2343
3078
  modelIdHint: "The identifier sent to the provider; required.",
2344
3079
  modelIdRequired: "Model id is required.",
2345
3080
  modelExtends: "Inherits from",
2346
- modelExtendsHint: "\"provider/model\" or a bare model id (candidates from the catalog below).",
3081
+ modelExtendsHint: "\"provider/model\" or a bare model id; candidates in the built-in catalog above.",
2347
3082
  modelName: "Display name",
2348
3083
  contextWindow: "Context window",
2349
3084
  maxTokens: "Max tokens",
2350
3085
  input: "Input modalities",
2351
3086
  reasoningEfforts: "reasoningEfforts",
2352
3087
  nonReasoning: "Non-reasoning model",
2353
- catalogSource: "Candidate source",
2354
- catalogProvider: "Candidate provider",
2355
- catalogLoading: "Loading catalog…",
2356
- catalogFailed: "Failed to load catalog.",
2357
3088
  saveOk: "Saved.",
2358
3089
  saveFailed: "Save failed: "
2359
3090
  };
@@ -2363,9 +3094,9 @@ ${extra}
2363
3094
  //#region src/client/styles.ts
2364
3095
  /**
2365
3096
  * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('lpc'),
2366
- * 本文件补插件特有规则(route/model 折叠块、键值对行、网格、JSON 文本框)
2367
- * 及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
2368
- * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放。
3097
+ * 本文件补插件特有规则(route/model 折叠块、目录浏览器行、键值对行、网格、
3098
+ * JSON 文本框)及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
3099
+ * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放、目录结果区限高滚动。
2369
3100
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
2370
3101
  * @module llm-pi/client/styles
2371
3102
  */
@@ -2383,6 +3114,11 @@ ${extra}
2383
3114
  .lpc-wide{grid-column:1 / -1}
2384
3115
  .lpc-addRoute{display:flex;gap:8px;align-items:center;padding:10px 0}
2385
3116
  .lpc-addRoute .lpc-input{flex:1;min-width:0}
3117
+ .lpc-chevron{flex:none;transition:transform .18s}
3118
+ .lpc-chevronOpen{transform:rotate(180deg)}
3119
+ .lpc-iconBtn{appearance:none;border:none;background:0 0;cursor:pointer;color:var(--dsw-alias-label-tertiary);border-radius:6px;padding:4px;display:inline-flex;flex:none}
3120
+ .lpc-iconBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
3121
+ .lpc-iconBtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
2386
3122
  .lpc-route{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin:10px 0;background:var(--dsw-alias-bg-layer-3)}
2387
3123
  .lpc-routeHead{display:flex;align-items:center;gap:8px;padding:6px 10px}
2388
3124
  .lpc-routeToggle{appearance:none;background:0 0;border:0;font:inherit;color:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;flex:1;min-width:0;text-align:left;padding:4px 0;border-radius:6px}
@@ -2394,17 +3130,37 @@ ${extra}
2394
3130
  .lpc-kvRow .lpc-input{flex:1;min-width:0}
2395
3131
  .lpc-kvAdd{padding-top:8px}
2396
3132
  .lpc-modelRow{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;margin:10px 0;padding:0 14px;background:var(--dsw-alias-bg-layer-3)}
2397
- .lpc-modelHead{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 0}
2398
- .lpc-modelTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
2399
- .lpc-catalogBar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:4px 0}
2400
- .lpc-catalogLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}
2401
- .lpc-catalogSelect{width:auto;height:30px}
3133
+ .lpc-modelHead{display:flex;align-items:center;gap:8px;padding:8px 0}
3134
+ .lpc-modelTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;flex:none}
3135
+ .lpc-modelRow .lpc-modelHead .lpc-routeToggle{flex:1}
3136
+ .lpc-modelBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:8px}
2402
3137
  .lpc-collapse{border-top:1px solid var(--dsw-alias-border-l2);margin:2px 0}
2403
3138
  .lpc-collapseHead{appearance:none;background:0 0;border:0;font:inherit;color:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:10px 0;border-radius:6px}
2404
3139
  .lpc-collapseHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
2405
3140
  .lpc-collapseTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
3141
+ .lpc-collapseMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;white-space:nowrap}
2406
3142
  .lpc-collapseBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:6px}
2407
- .lpc-refreshBtn{margin-left:8px;vertical-align:middle}
3143
+ .lpc-cat{display:flex;flex-direction:column;gap:6px}
3144
+ .lpc-catBar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:4px 0}
3145
+ .lpc-catLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:none}
3146
+ .lpc-catSearch{flex:1;min-width:180px}
3147
+ .lpc-catSelect{width:auto;max-width:260px;height:30px}
3148
+ .lpc-catNote{color:var(--dsw-alias-label-tertiary);font-size:12px;min-width:0;word-break:break-all}
3149
+ .lpc-catSummary{color:var(--dsw-alias-label-secondary);margin:2px 0;font-size:12px}
3150
+ .lpc-catList{display:flex;flex-direction:column;max-height:420px;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-3)}
3151
+ .lpc-catRow{border-bottom:1px solid var(--dsw-alias-border-l2);padding:8px 10px}
3152
+ .lpc-catRow:last-child{border-bottom:none}
3153
+ .lpc-catHead{display:flex;align-items:center;gap:8px;min-width:0}
3154
+ .lpc-catPath{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3155
+ .lpc-catName{color:var(--dsw-alias-label-secondary);font-size:12px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3156
+ .lpc-catParams{color:var(--dsw-alias-label-tertiary);margin:2px 0;font-size:12px;line-height:1.6;word-break:break-all}
3157
+ .lpc-catDetailsToggle{appearance:none;background:0 0;border:0;font:inherit;color:var(--dsw-alias-label-tertiary);cursor:pointer;display:inline-flex;align-items:center;gap:6px;padding:2px 0;font-size:12px;border-radius:6px}
3158
+ .lpc-catDetailsToggle:hover{color:var(--dsw-alias-label-primary)}
3159
+ .lpc-catDetailsToggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
3160
+ .lpc-catDetails{padding:2px 0 4px}
3161
+ .lpc-catDetailLabel{color:var(--dsw-alias-label-secondary);margin:6px 0 2px;font-size:12px;font-weight:600}
3162
+ .lpc-catJson{background:var(--dsw-alias-bg-module-platform);border-radius:8px;color:var(--dsw-alias-label-secondary);margin:0;padding:8px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;max-height:240px;overflow:auto;white-space:pre-wrap;word-break:break-all}
3163
+ .lpc-catMore{padding:6px 0}
2408
3164
  @media (max-width:767px){
2409
3165
  .lpc-grid{grid-template-columns:1fr}
2410
3166
  .lpc-routeHead{flex-wrap:wrap}
@@ -2416,7 +3172,12 @@ ${extra}
2416
3172
  .lpc-kvRow .lpc-btn{align-self:flex-end}
2417
3173
  .lpc-textarea{font-size:16px}
2418
3174
  .lpc-statusRow{display:flex;flex-direction:column;gap:4px}
2419
- .lpc-catalogBar .lpc-input{flex:1;min-width:140px}
3175
+ .lpc-catBar .lpc-input{flex:1;min-width:140px}
3176
+ .lpc-catSelect{max-width:none}
3177
+ .lpc-catHead{flex-wrap:wrap}
3178
+ .lpc-catPath{white-space:normal;word-break:break-all}
3179
+ .lpc-catName{flex-basis:100%;white-space:normal}
3180
+ .lpc-catList{max-height:60vh}
2420
3181
  }
2421
3182
  `);
2422
3183
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */