@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 +1489 -636
- package/lib/index.d.ts +4 -8
- package/lib/index.js +943 -391
- package/package.json +35 -36
- package/src/catalog/browse.ts +297 -0
- package/src/catalog/builtin.ts +183 -35
- package/src/client/api.ts +96 -33
- package/src/client/card.tsx +180 -120
- package/src/client/client.ts +2 -2
- package/src/client/constants.ts +25 -5
- package/src/client/draft.ts +34 -13
- package/src/client/fields.tsx +18 -3
- package/src/client/i18n.ts +153 -49
- package/src/client/root-fields.tsx +64 -52
- package/src/client/route-fit.ts +117 -0
- package/src/client/styles.ts +40 -10
- package/src/client/views/catalog-browser.tsx +376 -0
- package/src/client/views/catalog-row.tsx +147 -0
- package/src/client/views/compat.tsx +31 -3
- package/src/client/views/models.tsx +189 -193
- package/src/client/views/provider-fields.tsx +2 -2
- package/src/client/views/providers.tsx +51 -11
- package/src/compat.ts +43 -19
- package/src/config-api.ts +65 -37
- package/src/config.ts +20 -28
- package/src/inherit.ts +15 -31
- package/src/kit-meta.ts +156 -0
- package/src/{compat-gates.ts → official-surface.ts} +60 -16
- package/src/profiles.ts +52 -13
- package/src/resolve-dsh.ts +291 -72
- package/src/service.ts +209 -90
- package/src/catalog/models-dev.ts +0 -261
package/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
|
-
/**
|
|
428
|
-
async function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
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
|
-
/**
|
|
448
|
-
const
|
|
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 不渲染 + 保存被后端拒绝;见
|
|
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
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
+
* 把一个内置目录模型加进目标 route:只写 `id` + `extends`(**不预填**
|
|
804
|
+
* name/contextWindow/maxTokens——继承才能跟随 pi-ai 目录升级,预填会把值冻住)。
|
|
805
|
+
* 同 id 已存在时原样返回(added=false),不产生重复条目。
|
|
803
806
|
*/
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 [
|
|
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:
|
|
888
|
-
children: [
|
|
889
|
-
className: "lpc-
|
|
890
|
-
|
|
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/
|
|
1134
|
-
/**
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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/
|
|
1197
|
+
//#region src/client/route-fit.ts
|
|
1214
1198
|
/**
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1199
|
+
* 解析条目指向的继承引用(与服务端 inherit.ts 同规则):
|
|
1200
|
+
* `"provider/model"` 显式引用;裸 `"model"` 随 route 级 extends 源;
|
|
1201
|
+
* 条目无 extends 时按同名模型挂在 route 级 extends 源下。
|
|
1202
|
+
* 两段任一为空(无引用可解析)时返回空串。
|
|
1219
1203
|
*/
|
|
1220
|
-
function
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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-
|
|
1337
|
+
className: "lpc-catRow",
|
|
1265
1338
|
children: [
|
|
1266
1339
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1267
|
-
className: "lpc-
|
|
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)("
|
|
1283
|
-
className: "lpc-
|
|
1284
|
-
|
|
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.
|
|
1288
|
-
|
|
1289
|
-
className: "lpc-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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)("
|
|
1302
|
-
className: "lpc-
|
|
1303
|
-
|
|
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.
|
|
1307
|
-
|
|
1308
|
-
className: "lpc-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
children:
|
|
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
|
-
|
|
1323
|
-
className: "lpc-
|
|
1324
|
-
children:
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
-
|
|
1414
|
-
})
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
id: `${
|
|
1423
|
-
label:
|
|
1424
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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.
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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:
|
|
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
|
-
*
|
|
1700
|
-
*
|
|
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.
|
|
1724
|
-
className: "lpc-
|
|
1725
|
-
children:
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
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
|
-
/**
|
|
1890
|
-
function
|
|
1891
|
-
const [
|
|
1892
|
-
const [
|
|
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
|
-
|
|
2561
|
+
fetchCatalogMeta().then((result) => {
|
|
1896
2562
|
if (!alive) return;
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
(0, react.
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
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
|
|
1992
|
-
|
|
1993
|
-
|
|
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
|
-
|
|
2713
|
+
setOpenRoutes((prev) => ({
|
|
2714
|
+
...prev,
|
|
2715
|
+
[key]: true
|
|
2716
|
+
}));
|
|
2717
|
+
setBrowserTarget((prev) => prev === "" ? key : prev);
|
|
2718
|
+
idle();
|
|
1998
2719
|
};
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
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: [
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
|
2103
|
-
summaryLine: "自定义 LLM 路由:协议、compat
|
|
2833
|
+
description: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
|
|
2834
|
+
summaryLine: "自定义 LLM 路由:协议、compat、模型继承;模型目录唯一来自 pi-ai 内置目录(随已装 dsh 自动跟随)。",
|
|
2104
2835
|
enabled: "启用插件",
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2187
|
-
summaryLine: "Custom LLM routes: protocol, compat, model catalog
|
|
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
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2275
|
-
*
|
|
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;
|
|
2306
|
-
.lpc-modelTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
|
|
2307
|
-
.lpc-
|
|
2308
|
-
.lpc-
|
|
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-
|
|
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-
|
|
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)。 */
|