@dsh-plus/llm-pi 0.1.42 → 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
@@ -80,6 +80,30 @@ window.__ModuleLoader__.load({
80
80
  });
81
81
  }
82
82
  //#endregion
83
+ //#region ../shared/src/client/clipboard.ts
84
+ /**
85
+ * 剪贴板复制助手(浏览器半共享件):`navigator.clipboard` 优先,
86
+ * 非安全上下文(如非 localhost 直连的 HTTP 页面)无该 API 时退 `execCommand`。
87
+ * @module @dsh-plus/shared/client/clipboard
88
+ */
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
+ }
105
+ }
106
+ //#endregion
83
107
  //#region ../shared/src/client/config-slots.ts
84
108
  /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
85
109
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
@@ -116,41 +140,6 @@ window.__ModuleLoader__.load({
116
140
  */
117
141
  //#endregion
118
142
  //#region ../shared/src/client/fields.tsx
119
- function TextField$1(props) {
120
- const p = props.prefix ?? "dshp";
121
- const invalid = props.invalid === true;
122
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
123
- className: `${p}-field`,
124
- children: [
125
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
126
- className: `${p}-head`,
127
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
128
- className: `${p}-label`,
129
- htmlFor: props.id,
130
- children: props.label
131
- }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
132
- className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
133
- children: props.badge.text
134
- }) : null]
135
- }),
136
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
137
- id: props.id,
138
- className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
139
- type: props.password === true ? "password" : "text",
140
- inputMode: props.numeric === true ? "numeric" : void 0,
141
- autoComplete: props.password === true ? "off" : void 0,
142
- "aria-invalid": invalid || void 0,
143
- value: props.value,
144
- disabled: props.disabled === true,
145
- onChange: (event) => props.onEdit(event.target.value)
146
- }),
147
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
148
- className: invalid ? `${p}-invalid` : `${p}-hint`,
149
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
150
- })
151
- ]
152
- });
153
- }
154
143
  /**
155
144
  * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
156
145
  * 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
@@ -424,28 +413,41 @@ ${extra}
424
413
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
425
414
  return body;
426
415
  }
427
- /** 目录查询:provider 为空时只返回该源的 provider 列表。 */
428
- async function fetchCatalog(provider, source) {
429
- const url = `${ROUTE_CATALOG}?provider=${encodeURIComponent(provider)}&source=${source}`;
430
- return parse(await fetch(url, { credentials: "same-origin" }));
431
- }
432
- /** 手动拉取 models.dev 目录:POST /catalog/refresh → 最新快照状态。 */
433
- async function refreshCatalog() {
434
- return parse(await fetch(`${ROUTE_CATALOG}/refresh`, {
435
- method: "POST",
436
- 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 }
437
435
  }));
438
436
  }
439
437
  //#endregion
440
438
  //#region src/client/constants.ts
441
439
  /**
442
- * 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts、compat.ts 逐字对齐。
440
+ * 浏览器半内联常量:与服务端 packages/llm-pi/src/config.ts 逐字对齐。
443
441
  * 浏览器半不能 import 服务端模块(tsdown 只打包 client 侧入口),
444
442
  * 改动服务端这些常量时必须同步本文件。
443
+ *
444
+ * 两个集合改为**服务端下发**(与服务端校验同源,浏览器半不再手抄):
445
+ * - 线协议:取自已装官方适配器的 supportedProtocols()(`installProtocols`);
446
+ * - compat 字段表:取自官方 bundle + Config schema 推导(`installCompatFields`)。
445
447
  * @module llm-pi/client/constants
446
448
  */
447
- /** 协议枚举(来源:config.ts PROTOCOL_IDS)。 */
448
- const PROTOCOL_IDS = [
449
+ /** 协议兜底集合(来源:config.ts FALLBACK_PROTOCOLS);服务端下发前/失败时使用。 */
450
+ const FALLBACK_PROTOCOLS = [
449
451
  "openai-completions",
450
452
  "openai-responses",
451
453
  "anthropic-messages"
@@ -483,13 +485,26 @@ ${extra}
483
485
  "high"
484
486
  ];
485
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
+ /**
486
501
  * 逐协议 compat 字段表:**服务端推导结果经 /catalog 下发**,浏览器半不再手抄
487
- * (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 compat-gates.ts)。
502
+ * (手抄遗漏官方新增字段 = UI 不渲染 + 保存被后端拒绝;见 official-surface.ts)。
488
503
  * 未拿到服务端表时用 EMPTY(UI 只渲染提示行),不放内置副本兜底——宁可少画
489
504
  * 控件,也不给出可能与官方不符的字段集。
490
505
  */
491
506
  let compatTable = {};
492
- /** 安装服务端下发的字段表(card 挂载/刷新目录时调用)。 */
507
+ /** 安装服务端下发的字段表(card 挂载时调用)。 */
493
508
  function installCompatFields(table) {
494
509
  compatTable = table;
495
510
  }
@@ -515,9 +530,6 @@ ${extra}
515
530
  const value = Number(trimmed);
516
531
  return Number.isFinite(value) ? value : void 0;
517
532
  }
518
- function numTextOk(text) {
519
- return text.trim() !== "" && Number.isFinite(Number(text.trim()));
520
- }
521
533
  function headersToPairs(headers) {
522
534
  return Object.entries(headers ?? {}).map(([key, value]) => ({
523
535
  key,
@@ -777,95 +789,49 @@ ${extra}
777
789
  function draftFromValue(value) {
778
790
  return {
779
791
  enabled: value.enabled,
780
- catalogUrl: value.catalogUrl,
781
- catalogRefreshHours: String(value.catalogRefreshHours),
782
- catalogProxy: value.catalogProxy,
783
792
  providers: Object.fromEntries(Object.entries(value.providers).map(([route, provider]) => [route, providerDraftFromWire(provider)]))
784
793
  };
785
794
  }
786
795
  /** 提交补丁:完整配置对象,providers 全量替换;空值一律剔除。 */
787
796
  function toPatch(draft) {
788
- const refreshHours = toNum(draft.catalogRefreshHours);
789
797
  return {
790
798
  enabled: draft.enabled,
791
- catalogUrl: draft.catalogUrl.trim(),
792
- catalogRefreshHours: refreshHours,
793
- catalogProxy: draft.catalogProxy.trim(),
794
799
  providers: Object.fromEntries(Object.entries(draft.providers).map(([route, provider]) => [route, providerToWire(provider)]))
795
800
  };
796
801
  }
797
- //#endregion
798
- //#region src/client/root-fields.tsx
799
802
  /**
800
- * 「LLM 路由」卡片的根字段组:总开关、目录端点/刷新间隔/代理,以及运行期诊断行
801
- * (kitSource 与 models-dev 快照状态,来自模型目录端点而非配置数据)。
802
- * @module llm-pi/client/root-fields
803
+ * 把一个内置目录模型加进目标 route:只写 `id` + `extends`(**不预填**
804
+ * name/contextWindow/maxTokens——继承才能跟随 pi-ai 目录升级,预填会把值冻住)。
805
+ * 同 id 已存在时原样返回(added=false),不产生重复条目。
803
806
  */
804
- /** models-dev 状态行文案(缺席/错误/未拉取/正常四态)。 */
805
- function modelsDevText(status, t) {
806
- if (status === null) return t("modelsDevEmpty");
807
- if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
808
- if (status.fetchedAt === null) return t("modelsDevEmpty");
809
- return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt}`;
810
- }
811
- function RootFields(props) {
812
- const { t, draft, disabled, onEdit } = props;
813
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
814
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
815
- prefix: "lpc",
816
- id: "lpc-enabled",
817
- label: t("enabled"),
818
- checked: draft.enabled,
819
- disabled,
820
- onEdit: (value) => onEdit({ enabled: value })
821
- }),
822
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
823
- prefix: "lpc",
824
- id: "lpc-catalogUrl",
825
- label: t("catalogUrl"),
826
- hint: t("catalogUrlHint"),
827
- value: draft.catalogUrl,
828
- disabled,
829
- onEdit: (value) => onEdit({ catalogUrl: value })
830
- }),
831
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
832
- prefix: "lpc",
833
- id: "lpc-catalogRefresh",
834
- label: t("catalogRefreshHours"),
835
- hint: t("catalogRefreshHoursHint"),
836
- value: draft.catalogRefreshHours,
837
- numeric: true,
838
- disabled,
839
- invalid: !numTextOk(draft.catalogRefreshHours),
840
- invalidLabel: t("invalidNumber"),
841
- onEdit: (value) => onEdit({ catalogRefreshHours: value })
842
- }),
843
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
844
- prefix: "lpc",
845
- id: "lpc-catalogProxy",
846
- label: t("catalogProxy"),
847
- hint: t("catalogProxyHint"),
848
- value: draft.catalogProxy,
849
- disabled,
850
- onEdit: (value) => onEdit({ catalogProxy: value })
851
- }),
852
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
853
- className: "lpc-statusRow",
854
- children: [
855
- t("kitSource"),
856
- ":",
857
- props.kitSource ?? ""
858
- ]
859
- }),
860
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
861
- className: "lpc-statusRow",
862
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
863
- t("modelsDevStatus"),
864
- ":",
865
- modelsDevText(props.modelsDevStatus, t)
866
- ] })
867
- })
868
- ] });
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
+ };
869
835
  }
870
836
  //#endregion
871
837
  //#region src/client/fields.tsx
@@ -876,7 +842,12 @@ ${extra}
876
842
  * @module llm-pi/client/fields
877
843
  */
878
844
  function CollapseSection(props) {
879
- 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
+ };
880
851
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
881
852
  className: "lpc-collapse",
882
853
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -884,11 +855,18 @@ ${extra}
884
855
  className: "lpc-collapseHead",
885
856
  id: props.id,
886
857
  "aria-expanded": open,
887
- onClick: () => setOpen(!open),
888
- 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", {
889
- className: "lpc-collapseTitle",
890
- children: props.title
891
- })]
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
+ ]
892
870
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
893
871
  className: "lpc-collapseBody",
894
872
  children: props.children
@@ -1130,341 +1108,996 @@ ${extra}
1130
1108
  });
1131
1109
  }
1132
1110
  //#endregion
1133
- //#region src/client/views/compat.tsx
1134
- /** api 变更后裁剪 compat:只保留新渲染组的字段,避免保存时被后端拒绝。 */
1135
- function pruneCompatForApi(compat, api) {
1136
- const group = api !== "" && compatFieldsOf(api).length > 0 ? api : COMPAT_FALLBACK_API;
1137
- const fields = new Set(compatFieldsOf(group));
1138
- const next = {};
1139
- for (const [key, value] of Object.entries(compat)) if (fields.has(key)) next[key] = value;
1140
- 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(" · ");
1141
1134
  }
1142
- function CompatEditor(props) {
1143
- const effective = props.api !== "" && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API;
1144
- const fields = compatFieldsOf(effective);
1145
- const setField = (field, value) => {
1146
- const next = { ...props.compat };
1147
- if (value === void 0) delete next[field];
1148
- else next[field] = value;
1149
- props.onEdit(next);
1150
- };
1151
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1152
- className: `lpc-field lpc-wide`,
1153
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapseSection, {
1154
- id: `${props.idPrefix}-collapse`,
1155
- title: props.t("compatGroup"),
1156
- defaultOpen: false,
1157
- children: [props.api === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1158
- className: "lpc-hint",
1159
- children: props.t("compatApiHint")
1160
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1161
- className: "lpc-grid",
1162
- children: fields.map((field) => {
1163
- const spec = compatFieldSpec(effective, field);
1164
- if (spec === void 0) return null;
1165
- if (spec === "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1166
- id: `${props.idPrefix}-${field}`,
1167
- label: field,
1168
- value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1169
- options: ["true", "false"],
1170
- unsetLabel: props.t("compatUnset"),
1171
- disabled: props.disabled === true,
1172
- onEdit: (value) => {
1173
- if (value === "") setField(field, void 0);
1174
- else setField(field, value === "true");
1175
- }
1176
- }, field);
1177
- if (spec === "object") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1178
- id: `${props.idPrefix}-${field}`,
1179
- label: field,
1180
- value: props.compat[field],
1181
- epoch: props.epoch,
1182
- disabled: props.disabled === true,
1183
- invalidText: props.t("invalidJson"),
1184
- onEdit: (value) => setField(field, value)
1185
- }, field);
1186
- if (spec === "integer" || spec === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IntegerField, {
1187
- id: `${props.idPrefix}-${field}`,
1188
- label: field,
1189
- value: props.compat[field],
1190
- epoch: props.epoch,
1191
- invalidLabel: props.t("invalidInteger"),
1192
- disabled: props.disabled === true,
1193
- onEdit: (value) => setField(field, value)
1194
- }, field);
1195
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
1196
- id: `${props.idPrefix}-${field}`,
1197
- label: field,
1198
- value: props.compat[field] === void 0 ? "" : String(props.compat[field]),
1199
- options: spec,
1200
- unsetLabel: props.t("compatUnset"),
1201
- disabled: props.disabled === true,
1202
- onEdit: (value) => {
1203
- if (value === "") setField(field, void 0);
1204
- else setField(field, value);
1205
- }
1206
- }, field);
1207
- })
1208
- })]
1209
- })
1210
- });
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
+ ] });
1211
1195
  }
1212
1196
  //#endregion
1213
- //#region src/client/views/models.tsx
1197
+ //#region src/client/route-fit.ts
1214
1198
  /**
1215
- * 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat),
1216
- * extends 输入框带 datalist 候选——候选来自 /catalog 端点:选定 source
1217
- * (builtin / models-dev)与 provider 后拉取其 models 列表。
1218
- * @module llm-pi/client/views/models
1199
+ * 解析条目指向的继承引用(与服务端 inherit.ts 同规则):
1200
+ * `"provider/model"` 显式引用;裸 `"model"` 随 route 级 extends 源;
1201
+ * 条目无 extends 时按同名模型挂在 route 级 extends 源下。
1202
+ * 两段任一为空(无引用可解析)时返回空串。
1219
1203
  */
1220
- function catalogNote(status, t) {
1221
- if (status === void 0) return "";
1222
- if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
1223
- 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
+ };
1224
1219
  }
1225
- function ModelsTable(props) {
1226
- const { t } = props;
1227
- const [source, setSource] = (0, react.useState)("builtin");
1228
- const [providerIds, setProviderIds] = (0, react.useState)([]);
1229
- const [provider, setProvider] = (0, react.useState)("");
1230
- const [candidateModels, setCandidateModels] = (0, react.useState)([]);
1231
- const [note, setNote] = (0, react.useState)("");
1232
- const listId = `lpc-models-${props.route.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
1233
- (0, react.useEffect)(() => {
1234
- let alive = true;
1235
- setNote(t("catalogLoading"));
1236
- (async () => {
1237
- try {
1238
- const list = await fetchCatalog("", source);
1239
- if (!alive) return;
1240
- setProviderIds(list.providers);
1241
- const preferred = list.providers.includes(props.defaultProvider) ? props.defaultProvider : list.providers[0] ?? "";
1242
- setProvider(preferred);
1243
- setNote(catalogNote(list.status, t));
1244
- if (preferred === "") return;
1245
- const result = await fetchCatalog(preferred, source);
1246
- if (alive) setCandidateModels(result.models);
1247
- } catch {
1248
- if (alive) setNote(t("catalogFailed"));
1249
- }
1250
- })();
1251
- return () => {
1252
- alive = false;
1253
- };
1254
- }, [source]);
1255
- const onProviderChange = (value) => {
1256
- setProvider(value);
1257
- if (value === "") {
1258
- setCandidateModels([]);
1259
- 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;
1260
1241
  }
1261
- 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"
1262
1285
  };
1286
+ const facts = routeApiFacts(provider, apis);
1287
+ if (facts.mixed) return {
1288
+ ok: false,
1289
+ reason: "route-mixed"
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);
1263
1336
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1264
- className: "lpc-models",
1337
+ className: "lpc-catRow",
1265
1338
  children: [
1266
1339
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1267
- className: "lpc-modelHead",
1268
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1269
- className: "lpc-modelTitle",
1270
- children: t("modelsGroup")
1271
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1272
- type: "button",
1273
- className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1274
- disabled: props.disabled === true,
1275
- onClick: () => props.onModels([...props.models, emptyModelDraft()]),
1276
- children: t("addModel")
1277
- })]
1278
- }),
1279
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1280
- className: "lpc-catalogBar",
1340
+ className: "lpc-catHead",
1281
1341
  children: [
1282
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1283
- className: "lpc-catalogLabel",
1284
- htmlFor: `${listId}-source`,
1285
- children: t("catalogSource")
1342
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1343
+ className: "lpc-catPath",
1344
+ children: model.path
1286
1345
  }),
1287
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1288
- id: `${listId}-source`,
1289
- className: "lpc-input lpc-select lpc-catalogSelect",
1290
- value: source,
1291
- disabled: props.disabled === true,
1292
- onChange: (event) => setSource(event.target.value),
1293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1294
- value: "builtin",
1295
- children: "builtin"
1296
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1297
- value: "models-dev",
1298
- children: "models-dev"
1299
- })]
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
1300
1364
  }),
1301
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1302
- className: "lpc-catalogLabel",
1303
- htmlFor: `${listId}-provider`,
1304
- children: t("catalogProvider")
1365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1366
+ className: model.servable ? "lpc-badge lpc-badgeSet" : "lpc-badge lpc-badgeUnset",
1367
+ children: model.api
1305
1368
  }),
1306
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1307
- id: `${listId}-provider`,
1308
- className: "lpc-input lpc-select lpc-catalogSelect",
1309
- value: provider,
1310
- disabled: props.disabled === true || providerIds.length === 0,
1311
- onChange: (event) => onProviderChange(event.target.value),
1312
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1313
- value: "",
1314
- children: "-"
1315
- }), providerIds.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1316
- value: id,
1317
- children: id
1318
- }, id))]
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)
1319
1376
  })
1320
1377
  ]
1321
1378
  }),
1322
- note !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1323
- className: "lpc-hint",
1324
- children: note
1325
- }) : null,
1326
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
1327
- id: listId,
1328
- children: candidateModels.map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: id }, id))
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
+ ]
1329
1387
  }),
1330
- props.models.map((model, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
1331
- index,
1332
- model,
1333
- api: props.api,
1334
- listId,
1335
- epoch: props.epoch,
1336
- disabled: props.disabled === true,
1337
- t,
1338
- onPatch: (patch) => props.onModels(props.models.map((m, i) => i === index ? {
1339
- ...m,
1340
- ...patch
1341
- } : m)),
1342
- onRemove: () => props.onModels(props.models.filter((_, i) => i !== index))
1343
- }, `${index}:${model.id}`))
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
1344
1422
  ]
1345
1423
  });
1346
1424
  }
1347
- function ModelRow(props) {
1348
- const { model, t } = props;
1349
- const id = `${props.listId}-m${props.index}`;
1350
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
- className: "lpc-modelRow",
1352
- children: [
1353
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1354
- className: "lpc-modelHead",
1355
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1356
- className: "lpc-modelTitle",
1357
- children: [
1358
- t("modelRow"),
1359
- " ",
1360
- props.index + 1
1361
- ]
1362
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1363
- type: "button",
1364
- className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1365
- disabled: props.disabled === true,
1366
- onClick: props.onRemove,
1367
- children: t("deleteModel")
1368
- })]
1369
- }),
1370
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
- className: "lpc-grid",
1372
- children: [
1373
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1374
- id: `${id}-id`,
1375
- label: t("modelId"),
1376
- hint: t("modelIdHint"),
1377
- value: model.id,
1378
- disabled: props.disabled === true,
1379
- invalid: model.id.trim() === "",
1380
- invalidLabel: t("modelIdRequired"),
1381
- onEdit: (value) => props.onPatch({ id: value })
1382
- }),
1383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1384
- id: `${id}-extends`,
1385
- label: t("modelExtends"),
1386
- hint: t("modelExtendsHint"),
1387
- value: model.extends,
1388
- list: props.listId,
1389
- disabled: props.disabled === true,
1390
- onEdit: (value) => props.onPatch({ extends: value })
1391
- }),
1392
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1393
- id: `${id}-name`,
1394
- label: t("modelName"),
1395
- value: model.name,
1396
- disabled: props.disabled === true,
1397
- onEdit: (value) => props.onPatch({ name: value })
1398
- }),
1399
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1400
- id: `${id}-ctx`,
1401
- label: t("contextWindow"),
1402
- numeric: true,
1403
- value: model.contextWindow,
1404
- disabled: props.disabled === true,
1405
- onEdit: (value) => props.onPatch({ contextWindow: value })
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", {
1683
+ className: "lpc-hint",
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}`
1722
+ }) : null,
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
1406
1737
  }),
1407
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1408
- id: `${id}-max`,
1409
- label: t("maxTokens"),
1410
- numeric: true,
1411
- value: model.maxTokens,
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"),
1412
1803
  disabled: props.disabled === true,
1413
- onEdit: (value) => props.onPatch({ maxTokens: value })
1414
- }),
1415
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1416
- className: "lpc-field",
1417
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1418
- className: "lpc-label",
1419
- children: t("input")
1420
- }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1421
- prefix: "lpc",
1422
- id: `${id}-input-${modality}`,
1423
- label: modality,
1424
- checked: model.input[modality],
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,
1425
1829
  disabled: props.disabled === true,
1426
- onEdit: (checked) => props.onPatch({ input: {
1427
- ...model.input,
1428
- [modality]: checked
1429
- } })
1430
- }, modality))]
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")
1431
1924
  })
1432
1925
  ]
1433
1926
  }),
1434
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEditor, {
1435
- idPrefix: `${id}-re`,
1436
- value: model.reasoningEfforts,
1437
- disabled: props.disabled === true,
1438
- t,
1439
- onEdit: (reasoningEfforts) => props.onPatch({ reasoningEfforts })
1440
- }),
1441
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
1442
- idPrefix: `${id}-compat`,
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, {
1943
+ index,
1944
+ model,
1443
1945
  api: props.api,
1444
- compat: model.compat,
1946
+ uid: props.route.replace(/[^a-zA-Z0-9_-]/g, "_"),
1947
+ open: openRows[index] === true,
1445
1948
  epoch: props.epoch,
1446
1949
  disabled: props.disabled === true,
1447
- wide: true,
1448
1950
  t,
1449
- onEdit: (compat) => props.onPatch({ compat })
1450
- }),
1451
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
1452
- id: `${id}-extra`,
1453
- label: t("extraFields"),
1454
- hint: t("extraFieldsHint"),
1455
- invalidText: t("invalidJson"),
1456
- value: Object.keys(model.extra).length === 0 ? void 0 : model.extra,
1457
- epoch: props.epoch,
1458
- disabled: props.disabled === true,
1459
- wide: true,
1460
- onEdit: (extra) => {
1461
- const next = extraOfJson(extra);
1462
- if (next !== void 0) props.onPatch({ extra: next });
1463
- }
1464
- })
1951
+ onToggle: () => setOpenRows((prev) => ({
1952
+ ...prev,
1953
+ [index]: prev[index] !== true
1954
+ })),
1955
+ onPatch: (patch) => props.onModels(props.models.map((m, i) => i === index ? {
1956
+ ...m,
1957
+ ...patch
1958
+ } : m)),
1959
+ onRemove: () => props.onModels(props.models.filter((_, i) => i !== index))
1960
+ }, `${index}:${model.id}`))
1465
1961
  ]
1466
1962
  });
1467
1963
  }
1964
+ /** 单行:折叠头(序号/id/摘要) + 展开体(全部字段)。 */
1965
+ function ModelRow(props) {
1966
+ const { model, t } = props;
1967
+ const id = `lpc-m-${props.uid}-${props.index}`;
1968
+ const summary = rowSummary(model);
1969
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1970
+ className: "lpc-modelRow",
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,
1978
+ children: [
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}`
1983
+ }),
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
1987
+ }),
1988
+ summary !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1989
+ className: "lpc-routeApi",
1990
+ children: summary
1991
+ }) : null
1992
+ ]
1993
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1994
+ type: "button",
1995
+ className: "lpc-btn lpc-btnGhost lpc-btnSmall",
1996
+ disabled: props.disabled === true,
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]
2099
+ });
2100
+ }
1468
2101
  function ReasoningEditor(props) {
1469
2102
  const { value } = props;
1470
2103
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1654,7 +2287,7 @@ ${extra}
1654
2287
  id: `${props.id}-api`,
1655
2288
  label: t("api"),
1656
2289
  value: draft.api,
1657
- options: PROTOCOL_IDS,
2290
+ options: protocolOptions(),
1658
2291
  unsetLabel: t("compatUnset"),
1659
2292
  disabled: props.disabled === true,
1660
2293
  onEdit: (value) => props.onPatch({
@@ -1695,14 +2328,16 @@ ${extra}
1695
2328
  //#endregion
1696
2329
  //#region src/client/views/providers.tsx
1697
2330
  /**
1698
- * Provider 路由编辑:每个 route 一张可折叠小节(键名 + 标量字段 +
1699
- * headers 键值对 + retryPolicy JSON + compat + 模型目录)。
1700
- * 支持新增 route(输入键名)与删除 route。
2331
+ * Provider 路由编辑:每个 route 一张**受控**折叠小节(展开态由卡片持有,
2332
+ * 以便「从内置目录添加」后自动展开目标 route、以及卡片级"全部收起")。
2333
+ * 小节内容:基础字段 + 请求头 + 高级设置(其余字段 / retryPolicy / compat)
2334
+ * + 模型目录(行默认折叠,见 views/models.tsx)。
1701
2335
  * @module llm-pi/client/views/providers
1702
2336
  */
1703
2337
  function ProvidersSection(props) {
1704
2338
  const [newRoute, setNewRoute] = (0, react.useState)("");
1705
2339
  const [routeError, setRouteError] = (0, react.useState)("");
2340
+ const routes = Object.keys(props.providers);
1706
2341
  const submitAdd = () => {
1707
2342
  const key = newRoute.trim();
1708
2343
  if (key === "") {
@@ -1720,9 +2355,24 @@ ${extra}
1720
2355
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1721
2356
  className: "lpc-section",
1722
2357
  children: [
1723
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1724
- className: "lpc-groupLabel",
1725
- 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]
1726
2376
  }),
1727
2377
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1728
2378
  className: "lpc-addRoute",
@@ -1750,20 +2400,25 @@ ${extra}
1750
2400
  Object.entries(props.providers).map(([route, draft]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderSection, {
1751
2401
  route,
1752
2402
  draft,
2403
+ open: props.openRoutes[route] === true,
1753
2404
  epoch: props.epoch,
1754
2405
  disabled: props.disabled === true,
1755
2406
  t: props.t,
2407
+ onToggle: () => props.onToggleRoute(route, props.openRoutes[route] !== true),
1756
2408
  onRemove: () => props.onRemoveRoute(route),
1757
- onPatch: (patch) => props.onPatchProvider(route, patch)
2409
+ onPatch: (patch) => props.onPatchProvider(route, patch),
2410
+ onAddFromCatalog: () => props.onAddFromCatalog(route)
1758
2411
  }, route))
1759
2412
  ]
1760
2413
  });
1761
2414
  }
1762
2415
  function ProviderSection(props) {
1763
- const [open, setOpen] = (0, react.useState)(false);
1764
2416
  const { route, draft, t } = props;
1765
2417
  const id = route.replace(/[^a-zA-Z0-9_-]/g, "_");
1766
- 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}` : "";
1767
2422
  const fieldProps = {
1768
2423
  id,
1769
2424
  draft,
@@ -1778,10 +2433,10 @@ ${extra}
1778
2433
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1779
2434
  type: "button",
1780
2435
  className: "lpc-routeToggle",
1781
- "aria-expanded": open,
1782
- onClick: () => setOpen(!open),
2436
+ "aria-expanded": props.open,
2437
+ onClick: props.onToggle,
1783
2438
  children: [
1784
- /* @__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" : ""}` }),
1785
2440
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1786
2441
  className: "lpc-routeKey",
1787
2442
  children: route
@@ -1789,6 +2444,10 @@ ${extra}
1789
2444
  summary !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1790
2445
  className: "lpc-routeApi",
1791
2446
  children: summary
2447
+ }) : null,
2448
+ meta !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2449
+ className: "lpc-collapseMeta",
2450
+ children: meta
1792
2451
  }) : null
1793
2452
  ]
1794
2453
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -1798,7 +2457,7 @@ ${extra}
1798
2457
  onClick: props.onRemove,
1799
2458
  children: t("deleteRoute")
1800
2459
  })]
1801
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2460
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1802
2461
  className: "lpc-routeBody",
1803
2462
  children: [
1804
2463
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1864,12 +2523,12 @@ ${extra}
1864
2523
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTable, {
1865
2524
  route,
1866
2525
  api: draft.api,
1867
- defaultProvider: draft.extends,
1868
2526
  models: draft.models,
1869
2527
  epoch: props.epoch,
1870
2528
  disabled: props.disabled === true,
1871
2529
  t,
1872
- onModels: (models) => props.onPatch({ models })
2530
+ onModels: (models) => props.onPatch({ models }),
2531
+ onAddFromCatalog: props.onAddFromCatalog
1873
2532
  })
1874
2533
  ]
1875
2534
  }) : null]
@@ -1880,32 +2539,95 @@ ${extra}
1880
2539
  /**
1881
2540
  * 「LLM 路由」配置卡片:经 injectPluginConfigCard 注册(插件页
1882
2541
  * plugins.row.config / plugins.bundle.config,view 分发 summary/page)。
1883
- * 顶部:enabled / catalogUrl / catalogRefreshHours / 只读状态行(kitSource、
1884
- * modelsDevStatus,来自模型目录端点)+ 保存(settings.update 全量深合并)与
1885
- * 错误/成功提示;下方为 providers 路由列表(新增/删除/字段编辑/compat/模型
1886
- * 目录,见 views/)。外壳与基础控件走 @dsh-plus/shared/client 套件。
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
+ * 子组件受控。
1887
2553
  * @module llm-pi/client/card
1888
2554
  */
1889
- /** 运行期诊断行数据来源:模型目录端点(compat 字段表同批安装)。 */
1890
- function useRuntimeDiagnostics() {
1891
- const [kitSource, setKitSource] = (0, react.useState)(null);
1892
- 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)("");
1893
2559
  (0, react.useEffect)(() => {
1894
2560
  let alive = true;
1895
- fetchCatalog("", "models-dev").then((result) => {
2561
+ fetchCatalogMeta().then((result) => {
1896
2562
  if (!alive) return;
1897
- setKitSource(result.kitSource ?? null);
1898
- setModelsDevStatus(result.status ?? null);
1899
- if (result.compat !== void 0) installCompatFields(result.compat.fields);
1900
- }).catch(() => {});
2563
+ installProtocols(result.kit.protocols);
2564
+ installCompatFields(result.compat.fields);
2565
+ setMeta(result);
2566
+ }).catch((reason) => {
2567
+ if (!alive) return;
2568
+ setError(reason instanceof Error ? reason.message : String(reason));
2569
+ });
1901
2570
  return () => {
1902
2571
  alive = false;
1903
2572
  };
1904
2573
  }, []);
1905
2574
  return {
1906
- kitSource,
1907
- modelsDevStatus,
1908
- 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
1909
2631
  };
1910
2632
  }
1911
2633
  /** 路由级草稿改写:改字段 / 新增 route / 删除 route(返回新草稿,不改入参)。 */
@@ -1939,107 +2661,76 @@ ${extra}
1939
2661
  providers: next
1940
2662
  };
1941
2663
  }
1942
- /** 卡片动作:手动拉取目录、放弃、保存。 */
2664
+ /** 卡片动作:放弃、保存(模型目录已唯一来自 pi-ai,无可拉取项)。 */
1943
2665
  function cardActions(options) {
1944
2666
  const { t } = options;
1945
- return [
1946
- {
1947
- key: "refresh",
1948
- label: t(options.refreshing ? "refreshingCatalog" : "refreshCatalog"),
1949
- disabled: options.disabled || options.refreshing,
1950
- onClick: options.onRefresh
1951
- },
1952
- {
1953
- key: "discard",
1954
- label: t("discard"),
1955
- disabled: !options.dirty || options.saving,
1956
- onClick: options.onDiscard
1957
- },
1958
- {
1959
- key: "save",
1960
- label: t(options.saving ? "saving" : "save"),
1961
- variant: "primary",
1962
- disabled: !options.dirty || options.invalid || options.saving || options.disabled,
1963
- onClick: options.onSave
1964
- }
1965
- ];
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] ?? "";
1966
2684
  }
1967
2685
  function LlmPiCard(props) {
1968
2686
  const { t, scope, api } = props;
1969
2687
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
1970
2688
  const value = snapshot.value;
1971
- const [draft, setDraft] = (0, react.useState)(null);
1972
- const [epoch, setEpoch] = (0, react.useState)(0);
1973
- const [saving, setSaving] = (0, react.useState)(false);
1974
- const [refreshing, setRefreshing] = (0, react.useState)(false);
1975
- const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
1976
- const { kitSource, modelsDevStatus, setModelsDevStatus } = useRuntimeDiagnostics();
1977
- (0, react.useEffect)(() => {
1978
- if (value === void 0 || draft !== null) return;
1979
- setDraft(draftFromValue(value));
1980
- }, [value, draft]);
1981
- const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
1982
- const invalid = (0, react.useMemo)(() => {
1983
- if (draft === null) return false;
1984
- return !numTextOk(draft.catalogRefreshHours) || Object.values(draft.providers).some((provider) => provider.models.some((model) => model.id.trim() === ""));
1985
- }, [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();
1986
2699
  if (props.view === "summary") return t("summaryLine");
1987
2700
  if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
1988
2701
  prefix: "lpc",
1989
2702
  text: t("loading")
1990
2703
  });
1991
- const setProvider = (route, patch) => {
1992
- setDraft(withProviderPatch(draft, route, patch));
1993
- 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);
1994
2710
  };
1995
2711
  const onAddRoute = (key) => {
1996
2712
  setDraft(withNewRoute(draft, key));
1997
- setStatus(IDLE_STATUS);
2713
+ setOpenRoutes((prev) => ({
2714
+ ...prev,
2715
+ [key]: true
2716
+ }));
2717
+ setBrowserTarget((prev) => prev === "" ? key : prev);
2718
+ idle();
1998
2719
  };
1999
- const onRemoveRoute = (route) => {
2000
- setDraft(withoutRoute(draft, route));
2001
- setStatus(IDLE_STATUS);
2002
- };
2003
- const onSave = () => {
2004
- setSaving(true);
2005
- const revision = scope.getSnapshot().revision;
2006
- api.update(toPatch(draft), revision).then(async () => {
2007
- await scope.load();
2008
- const next = scope.getSnapshot().value;
2009
- if (next !== void 0) setDraft(draftFromValue(next));
2010
- setEpoch((value) => value + 1);
2011
- setStatus({
2012
- kind: "ok",
2013
- text: t("saveOk")
2014
- });
2015
- }).catch((error) => {
2016
- const message = error instanceof Error ? error.message : String(error);
2017
- setStatus({
2018
- kind: "error",
2019
- text: `${t("saveFailed")}${message}`
2020
- });
2021
- }).finally(() => setSaving(false));
2022
- };
2023
- const onDiscard = () => {
2024
- setDraft(draftFromValue(value));
2025
- setEpoch((value) => value + 1);
2026
- setStatus(IDLE_STATUS);
2027
- };
2028
- const onRefreshCatalog = () => {
2029
- setRefreshing(true);
2030
- refreshCatalog().then((result) => {
2031
- setModelsDevStatus(result.status);
2032
- setStatus({
2033
- kind: "ok",
2034
- text: t("refreshOk")
2035
- });
2036
- }).catch((error) => {
2037
- const message = error instanceof Error ? error.message : String(error);
2038
- setStatus({
2039
- kind: "error",
2040
- text: `${t("refreshFailed")}${message}`
2041
- });
2042
- }).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;
2043
2734
  };
2044
2735
  const disabled = !snapshot.writable;
2045
2736
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
@@ -2057,36 +2748,74 @@ ${extra}
2057
2748
  actions: cardActions({
2058
2749
  t,
2059
2750
  dirty,
2060
- invalid,
2061
2751
  saving,
2062
- refreshing,
2063
2752
  disabled,
2064
- onRefresh: onRefreshCatalog,
2065
2753
  onDiscard,
2066
2754
  onSave
2067
2755
  }),
2068
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootFields, {
2069
- t,
2070
- draft,
2071
- disabled,
2072
- kitSource,
2073
- modelsDevStatus,
2074
- onEdit: (patch) => {
2075
- setDraft({
2076
- ...draft,
2077
- ...patch
2078
- });
2079
- setStatus(IDLE_STATUS);
2080
- }
2081
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
2082
- providers: draft.providers,
2083
- epoch,
2084
- disabled,
2085
- t,
2086
- onAddRoute,
2087
- onRemoveRoute,
2088
- onPatchProvider: setProvider
2089
- })]
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
+ ]
2090
2819
  });
2091
2820
  }
2092
2821
  //#endregion
@@ -2094,33 +2823,76 @@ ${extra}
2094
2823
  /**
2095
2824
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
2096
2825
  * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
2826
+ * 服务端推导出来的诊断/提示(compat problem、版本区间提示、降级诊断)按原文展示,
2827
+ * 不做二次翻译——它们是事实描述,且与日志同一份文本。
2097
2828
  * @module llm-pi/client/i18n
2098
2829
  */
2099
2830
  const NS = "dsh-plus-llm-pi";
2100
2831
  const ownZh = {
2101
2832
  title: "LLM 路由(llm-pi)",
2102
- description: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
2103
- summaryLine: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
2833
+ description: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
2834
+ summaryLine: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
2104
2835
  enabled: "启用插件",
2105
- catalogUrl: "models.dev 目录数据端点",
2106
- catalogUrlHint: "快照数据源;一般无需修改。",
2107
- catalogRefreshHours: "目录刷新间隔(小时)",
2108
- catalogRefreshHoursHint: "0 = 不自动拉取(可手动拉取或读已有缓存);>0 = 每 N 小时自动拉取。",
2109
- catalogProxy: "拉取代理地址",
2110
- catalogProxyHint: "HTTP 代理(如 http://127.0.0.1:7890);留空直连。",
2111
- kitSource: "模块来源",
2112
- modelsDevStatus: "models.dev 快照",
2113
- modelsDevEmpty: "未拉取(无缓存数据;可手动拉取)",
2114
- modelsDevStatusLine: "已加载",
2115
- modelsDevError: "加载失败:",
2116
- refreshCatalog: "手动拉取",
2117
- refreshingCatalog: "拉取中…",
2118
- refreshOk: "目录已拉取。",
2119
- 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 不存在",
2120
2890
  advancedGroup: "高级设置(retryPolicy / compat)",
2121
2891
  extraFields: "其余字段(JSON)",
2122
2892
  extraFieldsHint: "表单未覆盖的 wire 字段原样往返:如 adapter=deepseek 的 thinking / reasoningEffort、Files API 与图片配额等。对象整体合并进该 route(模型级同 key 覆盖 route 级),非法 JSON 不会提交。",
2123
2893
  providersGroup: "Provider 路由",
2894
+ providersCollapseAll: "全部收起",
2895
+ providersExpandAll: "全部展开",
2124
2896
  addRoute: "新增 route",
2125
2897
  addRoutePlaceholder: "新 route 键名(如 my-llm)",
2126
2898
  routeEmpty: "route 键名不能为空。",
@@ -2128,7 +2900,7 @@ ${extra}
2128
2900
  deleteRoute: "删除 route",
2129
2901
  providerFields: "基础字段",
2130
2902
  extends: "继承内置 provider",
2131
- extendsHint: "如 openai、anthropic;提供 api/baseURL 默认值与模型查找源。",
2903
+ extendsHint: "如 openai、anthropic(候选见「内置模型目录」);提供 api/baseURL 默认值与模型查找源。",
2132
2904
  displayName: "显示名",
2133
2905
  api: "线协议",
2134
2906
  baseURL: "端点 URL",
@@ -2159,52 +2931,98 @@ ${extra}
2159
2931
  compatGroup: "Compat 覆盖",
2160
2932
  compatApiHint: "api 未设置时暂按 openai-completions 字段组渲染;保存时后端按实际协议校验。",
2161
2933
  compatUnset: "未设置",
2934
+ compatSetCount: "已设",
2935
+ compatFilter: "过滤字段",
2162
2936
  modelsGroup: "模型目录",
2163
2937
  addModel: "添加模型",
2164
- deleteModel: "删除",
2938
+ addFromCatalog: "从内置目录添加",
2939
+ modelsCollapseAll: "全部收起",
2940
+ modelsExpandAll: "全部展开",
2941
+ modelsFilter: "过滤模型",
2942
+ modelsFilterPlaceholder: "按 id / 继承源筛选",
2943
+ modelsHidden: "已按筛选隐藏",
2165
2944
  modelRow: "模型",
2945
+ modelUntitled: "(未填 id)",
2946
+ deleteModel: "删除",
2166
2947
  modelId: "模型 id",
2167
2948
  modelIdHint: "发送给 provider 的标识,必填。",
2168
2949
  modelIdRequired: "模型 id 必填。",
2169
2950
  modelExtends: "继承源",
2170
- modelExtendsHint: "\"provider/model\" 或裸 model id(候选来自下方目录)。",
2951
+ modelExtendsHint: "\"provider/model\" 或裸 model id;候选见「内置模型目录」。",
2171
2952
  modelName: "显示名",
2172
2953
  contextWindow: "上下文容量",
2173
2954
  maxTokens: "输出上限",
2174
2955
  input: "输入模态",
2175
2956
  reasoningEfforts: "reasoningEfforts",
2176
2957
  nonReasoning: "非推理模型",
2177
- catalogSource: "候选来源",
2178
- catalogProvider: "候选 provider",
2179
- catalogLoading: "目录加载中…",
2180
- catalogFailed: "目录加载失败。",
2181
2958
  saveOk: "已保存。",
2182
2959
  saveFailed: "保存失败:"
2183
2960
  };
2184
2961
  const ownEn = {
2185
2962
  title: "LLM routes (llm-pi)",
2186
- description: "Custom LLM routes: protocol, compat, model catalog and models.dev fallback.",
2187
- 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.",
2188
2965
  enabled: "Enable plugin",
2189
- catalogUrl: "models.dev catalog endpoint",
2190
- catalogUrlHint: "Snapshot data source; usually no change needed.",
2191
- catalogRefreshHours: "Catalog refresh (hours)",
2192
- catalogRefreshHoursHint: "0 = no auto refresh (manual refresh or existing cache); >0 = refresh every N hours.",
2193
- catalogProxy: "Fetch proxy",
2194
- catalogProxyHint: "HTTP proxy (e.g. http://127.0.0.1:7890); leave empty for direct.",
2195
- kitSource: "Module source",
2196
- modelsDevStatus: "models.dev snapshot",
2197
- modelsDevEmpty: "Not fetched (no cached data; use manual refresh)",
2198
- modelsDevStatusLine: "Loaded",
2199
- modelsDevError: "Load failed: ",
2200
- refreshCatalog: "Refresh now",
2201
- refreshingCatalog: "Refreshing…",
2202
- refreshOk: "Catalog refreshed.",
2203
- 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",
2204
3020
  advancedGroup: "Advanced (retryPolicy / compat)",
2205
3021
  extraFields: "Other fields (JSON)",
2206
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.",
2207
3023
  providersGroup: "Provider routes",
3024
+ providersCollapseAll: "Collapse all",
3025
+ providersExpandAll: "Expand all",
2208
3026
  addRoute: "Add route",
2209
3027
  addRoutePlaceholder: "New route key (e.g. my-llm)",
2210
3028
  routeEmpty: "Route key must not be empty.",
@@ -2212,7 +3030,7 @@ ${extra}
2212
3030
  deleteRoute: "Delete route",
2213
3031
  providerFields: "Basic fields",
2214
3032
  extends: "Inherit built-in provider",
2215
- 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.",
2216
3034
  displayName: "Display name",
2217
3035
  api: "Wire protocol",
2218
3036
  baseURL: "Base URL",
@@ -2243,25 +3061,30 @@ ${extra}
2243
3061
  compatGroup: "Compat overrides",
2244
3062
  compatApiHint: "When api is unset, fields render per openai-completions; the backend validates per the effective protocol.",
2245
3063
  compatUnset: "Unset",
3064
+ compatSetCount: "set",
3065
+ compatFilter: "Filter fields",
2246
3066
  modelsGroup: "Model catalog",
2247
3067
  addModel: "Add model",
2248
- 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",
2249
3074
  modelRow: "Model",
3075
+ modelUntitled: "(no id yet)",
3076
+ deleteModel: "Delete",
2250
3077
  modelId: "Model id",
2251
3078
  modelIdHint: "The identifier sent to the provider; required.",
2252
3079
  modelIdRequired: "Model id is required.",
2253
3080
  modelExtends: "Inherits from",
2254
- 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.",
2255
3082
  modelName: "Display name",
2256
3083
  contextWindow: "Context window",
2257
3084
  maxTokens: "Max tokens",
2258
3085
  input: "Input modalities",
2259
3086
  reasoningEfforts: "reasoningEfforts",
2260
3087
  nonReasoning: "Non-reasoning model",
2261
- catalogSource: "Candidate source",
2262
- catalogProvider: "Candidate provider",
2263
- catalogLoading: "Loading catalog…",
2264
- catalogFailed: "Failed to load catalog.",
2265
3088
  saveOk: "Saved.",
2266
3089
  saveFailed: "Save failed: "
2267
3090
  };
@@ -2271,9 +3094,9 @@ ${extra}
2271
3094
  //#region src/client/styles.ts
2272
3095
  /**
2273
3096
  * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('lpc'),
2274
- * 本文件补插件特有规则(route/model 折叠块、键值对行、网格、JSON 文本框)
2275
- * 及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
2276
- * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放。
3097
+ * 本文件补插件特有规则(route/model 折叠块、目录浏览器行、键值对行、网格、
3098
+ * JSON 文本框)及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
3099
+ * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放、目录结果区限高滚动。
2277
3100
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
2278
3101
  * @module llm-pi/client/styles
2279
3102
  */
@@ -2291,6 +3114,11 @@ ${extra}
2291
3114
  .lpc-wide{grid-column:1 / -1}
2292
3115
  .lpc-addRoute{display:flex;gap:8px;align-items:center;padding:10px 0}
2293
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}
2294
3122
  .lpc-route{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin:10px 0;background:var(--dsw-alias-bg-layer-3)}
2295
3123
  .lpc-routeHead{display:flex;align-items:center;gap:8px;padding:6px 10px}
2296
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}
@@ -2302,17 +3130,37 @@ ${extra}
2302
3130
  .lpc-kvRow .lpc-input{flex:1;min-width:0}
2303
3131
  .lpc-kvAdd{padding-top:8px}
2304
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)}
2305
- .lpc-modelHead{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 0}
2306
- .lpc-modelTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
2307
- .lpc-catalogBar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:4px 0}
2308
- .lpc-catalogLabel{color:var(--dsw-alias-label-tertiary);font-size:12px}
2309
- .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}
2310
3137
  .lpc-collapse{border-top:1px solid var(--dsw-alias-border-l2);margin:2px 0}
2311
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}
2312
3139
  .lpc-collapseHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
2313
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}
2314
3142
  .lpc-collapseBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:6px}
2315
- .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}
2316
3164
  @media (max-width:767px){
2317
3165
  .lpc-grid{grid-template-columns:1fr}
2318
3166
  .lpc-routeHead{flex-wrap:wrap}
@@ -2324,7 +3172,12 @@ ${extra}
2324
3172
  .lpc-kvRow .lpc-btn{align-self:flex-end}
2325
3173
  .lpc-textarea{font-size:16px}
2326
3174
  .lpc-statusRow{display:flex;flex-direction:column;gap:4px}
2327
- .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}
2328
3181
  }
2329
3182
  `);
2330
3183
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */