@dsh-plus/llm-pi 0.1.39 → 0.1.41
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 +375 -201
- package/package.json +2 -2
- package/src/client/card.tsx +106 -118
- package/src/client/draft.ts +12 -0
- package/src/client/i18n.ts +6 -0
- package/src/client/root-fields.tsx +84 -0
- package/src/client/views/models.tsx +17 -2
- package/src/client/views/providers.tsx +16 -0
package/lib/client.js
CHANGED
|
@@ -12,9 +12,72 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
kind: "idle",
|
|
13
13
|
text: ""
|
|
14
14
|
};
|
|
15
|
+
/** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
|
|
16
|
+
function CardFoot(props) {
|
|
17
|
+
const p = props.prefix;
|
|
18
|
+
const section = props.variant === "section";
|
|
19
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
20
|
+
className: section ? `${p}-sectionFooter` : `${p}-footer`,
|
|
21
|
+
children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
22
|
+
className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
|
|
23
|
+
role: "status",
|
|
24
|
+
children: props.status.text
|
|
25
|
+
}) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
26
|
+
type: "button",
|
|
27
|
+
className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
|
|
28
|
+
disabled: action.disabled === true,
|
|
29
|
+
onClick: action.onClick,
|
|
30
|
+
children: action.label
|
|
31
|
+
}, action.key))]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
|
|
35
|
+
function CardBody(props) {
|
|
36
|
+
const p = props.prefix;
|
|
37
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
38
|
+
props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
39
|
+
className: `${p}-readOnly`,
|
|
40
|
+
role: "status",
|
|
41
|
+
children: props.readOnlyNotice
|
|
42
|
+
}) : null,
|
|
43
|
+
props.children,
|
|
44
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
|
|
45
|
+
] });
|
|
46
|
+
}
|
|
15
47
|
function CardChrome(props) {
|
|
16
48
|
const p = props.prefix;
|
|
17
49
|
const badge = props.statusBadge;
|
|
50
|
+
if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
51
|
+
className: `${p}-section`,
|
|
52
|
+
"aria-label": props.title,
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55
|
+
className: `${p}-sectionHead`,
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
58
|
+
className: `${p}-sectionTitle`,
|
|
59
|
+
children: props.title
|
|
60
|
+
}),
|
|
61
|
+
badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
62
|
+
className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
|
|
63
|
+
children: badge.text
|
|
64
|
+
}) : null,
|
|
65
|
+
props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
66
|
+
className: `${p}-pending`,
|
|
67
|
+
children: props.dirtyLabel
|
|
68
|
+
}) : null
|
|
69
|
+
]
|
|
70
|
+
}),
|
|
71
|
+
props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
72
|
+
className: `${p}-sectionDesc`,
|
|
73
|
+
children: props.description
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
76
|
+
className: `${p}-sectionBody`,
|
|
77
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
});
|
|
18
81
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
19
82
|
className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
|
|
20
83
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -44,38 +107,44 @@ window.__ModuleLoader__.load({
|
|
|
44
107
|
}) : null,
|
|
45
108
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
|
|
46
109
|
]
|
|
47
|
-
}), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
110
|
+
}), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
48
111
|
className: `${p}-body`,
|
|
49
|
-
children:
|
|
50
|
-
props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
51
|
-
className: `${p}-readOnly`,
|
|
52
|
-
role: "status",
|
|
53
|
-
children: props.readOnlyNotice
|
|
54
|
-
}) : null,
|
|
55
|
-
props.children,
|
|
56
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
57
|
-
className: `${p}-footer`,
|
|
58
|
-
children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
59
|
-
className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
|
|
60
|
-
role: "status",
|
|
61
|
-
children: props.status.text
|
|
62
|
-
}) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
63
|
-
type: "button",
|
|
64
|
-
className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
|
|
65
|
-
disabled: action.disabled === true,
|
|
66
|
-
onClick: action.onClick,
|
|
67
|
-
children: action.label
|
|
68
|
-
}, action.key))]
|
|
69
|
-
})
|
|
70
|
-
]
|
|
112
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
|
|
71
113
|
}) : null]
|
|
72
114
|
});
|
|
73
115
|
}
|
|
116
|
+
/** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
|
|
117
|
+
function CardLoading(props) {
|
|
118
|
+
if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
119
|
+
className: `${props.prefix}-section`,
|
|
120
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
121
|
+
className: `${props.prefix}-readOnly`,
|
|
122
|
+
role: "status",
|
|
123
|
+
children: props.text
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
127
|
+
className: `${props.prefix}-card`,
|
|
128
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
129
|
+
className: `${props.prefix}-readOnly`,
|
|
130
|
+
role: "status",
|
|
131
|
+
children: props.text
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
}
|
|
74
135
|
//#endregion
|
|
75
136
|
//#region ../shared/src/client/config-slots.ts
|
|
76
137
|
/** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
|
|
77
138
|
const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
|
|
78
139
|
/**
|
|
140
|
+
* 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
|
|
141
|
+
* 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
|
|
142
|
+
* 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
|
|
143
|
+
*/
|
|
144
|
+
function cardVariantFor(view) {
|
|
145
|
+
return view === "page" ? "section" : "card";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
79
148
|
* 把一张配置卡片同时注册进 legacy 与新版三个槽位。
|
|
80
149
|
* 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
|
|
81
150
|
*/
|
|
@@ -101,6 +170,17 @@ window.__ModuleLoader__.load({
|
|
|
101
170
|
}, reg.component));
|
|
102
171
|
}
|
|
103
172
|
//#endregion
|
|
173
|
+
//#region ../shared/src/client/draft.ts
|
|
174
|
+
/**
|
|
175
|
+
* 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
|
|
176
|
+
* 脏判定、保存(revision fencing + 重新装载)与状态行。
|
|
177
|
+
*
|
|
178
|
+
* 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
|
|
179
|
+
* 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
|
|
180
|
+
* 依赖,必须是模块级稳定函数(不要内联箭头函数)。
|
|
181
|
+
* @module @dsh-plus/shared/client/draft
|
|
182
|
+
*/
|
|
183
|
+
//#endregion
|
|
104
184
|
//#region ../shared/src/client/fields.tsx
|
|
105
185
|
function TextField$1(props) {
|
|
106
186
|
const p = props.prefix ?? "dshp";
|
|
@@ -304,6 +384,9 @@ window.__ModuleLoader__.load({
|
|
|
304
384
|
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
305
385
|
* - footer 按钮等宽换行、状态文字独占一行;
|
|
306
386
|
* - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
|
|
387
|
+
* - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
|
|
388
|
+
* 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
|
|
389
|
+
* 缩窄移动端可用宽度(卡片套卡片)。
|
|
307
390
|
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
308
391
|
* @module @dsh-plus/shared/client/styles
|
|
309
392
|
*/
|
|
@@ -350,6 +433,15 @@ window.__ModuleLoader__.load({
|
|
|
350
433
|
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
351
434
|
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
352
435
|
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
436
|
+
/* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
|
|
437
|
+
横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
|
|
438
|
+
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
439
|
+
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
440
|
+
.${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
441
|
+
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
442
|
+
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
443
|
+
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
444
|
+
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
353
445
|
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
354
446
|
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
355
447
|
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
@@ -371,6 +463,8 @@ window.__ModuleLoader__.load({
|
|
|
371
463
|
.${prefix}-status{flex-basis:100%;flex:none}
|
|
372
464
|
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
373
465
|
.${prefix}-statusBadge{display:none}
|
|
466
|
+
.${prefix}-sectionBadge{display:inline-block}
|
|
467
|
+
.${prefix}-sectionFooter{position:static}
|
|
374
468
|
}
|
|
375
469
|
@media (pointer:coarse){
|
|
376
470
|
.${prefix}-btn{min-height:44px}
|
|
@@ -621,6 +715,17 @@ ${extra}
|
|
|
621
715
|
for (const [key, value] of Object.entries(wire)) if (!known.has(key) && value !== void 0) out[key] = value;
|
|
622
716
|
return out;
|
|
623
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* JSON 文本框值 → extra 映射。
|
|
720
|
+
* `undefined`(空文本)表示清空为 `{}`;合法但非对象的 JSON(数组/标量/null)
|
|
721
|
+
* 返回 undefined,调用方据此忽略这次编辑——extra 是「其余 wire 字段」的映射,
|
|
722
|
+
* 静默丢弃已配置字段比拒绝一次编辑更糟。
|
|
723
|
+
*/
|
|
724
|
+
function extraOfJson(value) {
|
|
725
|
+
if (value === void 0) return {};
|
|
726
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
727
|
+
return { ...value };
|
|
728
|
+
}
|
|
624
729
|
function modelDraftFromWire(model) {
|
|
625
730
|
return {
|
|
626
731
|
id: model.id,
|
|
@@ -775,6 +880,79 @@ ${extra}
|
|
|
775
880
|
};
|
|
776
881
|
}
|
|
777
882
|
//#endregion
|
|
883
|
+
//#region src/client/root-fields.tsx
|
|
884
|
+
/**
|
|
885
|
+
* 「LLM 路由」卡片的根字段组:总开关、目录端点/刷新间隔/代理,以及运行期诊断行
|
|
886
|
+
* (kitSource 与 models-dev 快照状态,来自模型目录端点而非配置数据)。
|
|
887
|
+
* @module llm-pi/client/root-fields
|
|
888
|
+
*/
|
|
889
|
+
/** models-dev 状态行文案(缺席/错误/未拉取/正常四态)。 */
|
|
890
|
+
function modelsDevText(status, t) {
|
|
891
|
+
if (status === null) return t("modelsDevEmpty");
|
|
892
|
+
if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
|
|
893
|
+
if (status.fetchedAt === null) return t("modelsDevEmpty");
|
|
894
|
+
return `${t("modelsDevStatusLine")}:${status.providers} 个 provider,快照 ${status.fetchedAt}`;
|
|
895
|
+
}
|
|
896
|
+
function RootFields(props) {
|
|
897
|
+
const { t, draft, disabled, onEdit } = props;
|
|
898
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
900
|
+
prefix: "lpc",
|
|
901
|
+
id: "lpc-enabled",
|
|
902
|
+
label: t("enabled"),
|
|
903
|
+
checked: draft.enabled,
|
|
904
|
+
disabled,
|
|
905
|
+
onEdit: (value) => onEdit({ enabled: value })
|
|
906
|
+
}),
|
|
907
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
908
|
+
prefix: "lpc",
|
|
909
|
+
id: "lpc-catalogUrl",
|
|
910
|
+
label: t("catalogUrl"),
|
|
911
|
+
hint: t("catalogUrlHint"),
|
|
912
|
+
value: draft.catalogUrl,
|
|
913
|
+
disabled,
|
|
914
|
+
onEdit: (value) => onEdit({ catalogUrl: value })
|
|
915
|
+
}),
|
|
916
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
917
|
+
prefix: "lpc",
|
|
918
|
+
id: "lpc-catalogRefresh",
|
|
919
|
+
label: t("catalogRefreshHours"),
|
|
920
|
+
hint: t("catalogRefreshHoursHint"),
|
|
921
|
+
value: draft.catalogRefreshHours,
|
|
922
|
+
numeric: true,
|
|
923
|
+
disabled,
|
|
924
|
+
invalid: !numTextOk(draft.catalogRefreshHours),
|
|
925
|
+
invalidLabel: t("invalidNumber"),
|
|
926
|
+
onEdit: (value) => onEdit({ catalogRefreshHours: value })
|
|
927
|
+
}),
|
|
928
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
929
|
+
prefix: "lpc",
|
|
930
|
+
id: "lpc-catalogProxy",
|
|
931
|
+
label: t("catalogProxy"),
|
|
932
|
+
hint: t("catalogProxyHint"),
|
|
933
|
+
value: draft.catalogProxy,
|
|
934
|
+
disabled,
|
|
935
|
+
onEdit: (value) => onEdit({ catalogProxy: value })
|
|
936
|
+
}),
|
|
937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
938
|
+
className: "lpc-statusRow",
|
|
939
|
+
children: [
|
|
940
|
+
t("kitSource"),
|
|
941
|
+
":",
|
|
942
|
+
props.kitSource ?? ""
|
|
943
|
+
]
|
|
944
|
+
}),
|
|
945
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
946
|
+
className: "lpc-statusRow",
|
|
947
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
948
|
+
t("modelsDevStatus"),
|
|
949
|
+
":",
|
|
950
|
+
modelsDevText(props.modelsDevStatus, t)
|
|
951
|
+
] })
|
|
952
|
+
})
|
|
953
|
+
] });
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
778
956
|
//#region src/client/fields.tsx
|
|
779
957
|
/**
|
|
780
958
|
* llm-pi 特有卡片控件:可折叠小节、键值对编辑、JSON 文本框、带 datalist 的
|
|
@@ -1354,6 +1532,20 @@ ${extra}
|
|
|
1354
1532
|
wide: true,
|
|
1355
1533
|
t,
|
|
1356
1534
|
onEdit: (compat) => props.onPatch({ compat })
|
|
1535
|
+
}),
|
|
1536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
|
|
1537
|
+
id: `${id}-extra`,
|
|
1538
|
+
label: t("extraFields"),
|
|
1539
|
+
hint: t("extraFieldsHint"),
|
|
1540
|
+
invalidText: t("invalidJson"),
|
|
1541
|
+
value: Object.keys(model.extra).length === 0 ? void 0 : model.extra,
|
|
1542
|
+
epoch: props.epoch,
|
|
1543
|
+
disabled: props.disabled === true,
|
|
1544
|
+
wide: true,
|
|
1545
|
+
onEdit: (extra) => {
|
|
1546
|
+
const next = extraOfJson(extra);
|
|
1547
|
+
if (next !== void 0) props.onPatch({ extra: next });
|
|
1548
|
+
}
|
|
1357
1549
|
})
|
|
1358
1550
|
]
|
|
1359
1551
|
});
|
|
@@ -1716,26 +1908,43 @@ ${extra}
|
|
|
1716
1908
|
id: `${id}-advanced`,
|
|
1717
1909
|
title: t("advancedGroup"),
|
|
1718
1910
|
defaultOpen: false,
|
|
1719
|
-
children: [
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1911
|
+
children: [
|
|
1912
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
|
|
1913
|
+
id: `${id}-extra`,
|
|
1914
|
+
label: t("extraFields"),
|
|
1915
|
+
hint: t("extraFieldsHint"),
|
|
1916
|
+
invalidText: t("invalidJson"),
|
|
1917
|
+
value: Object.keys(draft.extra).length === 0 ? void 0 : draft.extra,
|
|
1918
|
+
epoch: props.epoch,
|
|
1919
|
+
disabled: props.disabled === true,
|
|
1920
|
+
wide: true,
|
|
1921
|
+
onEdit: (extra) => {
|
|
1922
|
+
const next = extraOfJson(extra);
|
|
1923
|
+
if (next !== void 0) props.onPatch({ extra: next });
|
|
1924
|
+
}
|
|
1925
|
+
}),
|
|
1926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(JsonField, {
|
|
1927
|
+
id: `${id}-retry`,
|
|
1928
|
+
label: t("retryPolicy"),
|
|
1929
|
+
hint: t("retryPolicyHint"),
|
|
1930
|
+
invalidText: t("invalidJson"),
|
|
1931
|
+
value: draft.retryPolicy,
|
|
1932
|
+
epoch: props.epoch,
|
|
1933
|
+
disabled: props.disabled === true,
|
|
1934
|
+
wide: true,
|
|
1935
|
+
onEdit: (retryPolicy) => props.onPatch({ retryPolicy })
|
|
1936
|
+
}),
|
|
1937
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CompatEditor, {
|
|
1938
|
+
idPrefix: `${id}-compat`,
|
|
1939
|
+
api: draft.api,
|
|
1940
|
+
compat: draft.compat,
|
|
1941
|
+
epoch: props.epoch,
|
|
1942
|
+
disabled: props.disabled === true,
|
|
1943
|
+
wide: true,
|
|
1944
|
+
t,
|
|
1945
|
+
onEdit: (compat) => props.onPatch({ compat })
|
|
1946
|
+
})
|
|
1947
|
+
]
|
|
1739
1948
|
}),
|
|
1740
1949
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsTable, {
|
|
1741
1950
|
route,
|
|
@@ -1763,11 +1972,83 @@ ${extra}
|
|
|
1763
1972
|
* 目录,见 views/)。外壳与基础控件走 @dsh-plus/shared/client 套件。
|
|
1764
1973
|
* @module llm-pi/client/card
|
|
1765
1974
|
*/
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1975
|
+
/** 运行期诊断行数据来源:模型目录端点(compat 字段表同批安装)。 */
|
|
1976
|
+
function useRuntimeDiagnostics() {
|
|
1977
|
+
const [kitSource, setKitSource] = (0, react.useState)(null);
|
|
1978
|
+
const [modelsDevStatus, setModelsDevStatus] = (0, react.useState)(null);
|
|
1979
|
+
(0, react.useEffect)(() => {
|
|
1980
|
+
let alive = true;
|
|
1981
|
+
fetchCatalog("", "models-dev").then((result) => {
|
|
1982
|
+
if (!alive) return;
|
|
1983
|
+
setKitSource(result.kitSource ?? null);
|
|
1984
|
+
setModelsDevStatus(result.status ?? null);
|
|
1985
|
+
if (result.compat !== void 0) installCompatFields(result.compat.fields);
|
|
1986
|
+
}).catch(() => {});
|
|
1987
|
+
return () => {
|
|
1988
|
+
alive = false;
|
|
1989
|
+
};
|
|
1990
|
+
}, []);
|
|
1991
|
+
return {
|
|
1992
|
+
kitSource,
|
|
1993
|
+
modelsDevStatus,
|
|
1994
|
+
setModelsDevStatus
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/** 路由级草稿改写:改字段 / 新增 route / 删除 route(返回新草稿,不改入参)。 */
|
|
1998
|
+
function withProviderPatch(draft, route, patch) {
|
|
1999
|
+
const current = draft.providers[route] ?? emptyProviderDraft();
|
|
2000
|
+
return {
|
|
2001
|
+
...draft,
|
|
2002
|
+
providers: {
|
|
2003
|
+
...draft.providers,
|
|
2004
|
+
[route]: {
|
|
2005
|
+
...current,
|
|
2006
|
+
...patch
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
function withNewRoute(draft, key) {
|
|
2012
|
+
return {
|
|
2013
|
+
...draft,
|
|
2014
|
+
providers: {
|
|
2015
|
+
...draft.providers,
|
|
2016
|
+
[key]: emptyProviderDraft()
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function withoutRoute(draft, route) {
|
|
2021
|
+
const next = { ...draft.providers };
|
|
2022
|
+
delete next[route];
|
|
2023
|
+
return {
|
|
2024
|
+
...draft,
|
|
2025
|
+
providers: next
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
/** 卡片动作:手动拉取目录、放弃、保存。 */
|
|
2029
|
+
function cardActions(options) {
|
|
2030
|
+
const { t } = options;
|
|
2031
|
+
return [
|
|
2032
|
+
{
|
|
2033
|
+
key: "refresh",
|
|
2034
|
+
label: t(options.refreshing ? "refreshingCatalog" : "refreshCatalog"),
|
|
2035
|
+
disabled: options.disabled || options.refreshing,
|
|
2036
|
+
onClick: options.onRefresh
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
key: "discard",
|
|
2040
|
+
label: t("discard"),
|
|
2041
|
+
disabled: !options.dirty || options.saving,
|
|
2042
|
+
onClick: options.onDiscard
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
key: "save",
|
|
2046
|
+
label: t(options.saving ? "saving" : "save"),
|
|
2047
|
+
variant: "primary",
|
|
2048
|
+
disabled: !options.dirty || options.invalid || options.saving || options.disabled,
|
|
2049
|
+
onClick: options.onSave
|
|
2050
|
+
}
|
|
2051
|
+
];
|
|
1771
2052
|
}
|
|
1772
2053
|
function LlmPiCard(props) {
|
|
1773
2054
|
const { t, scope, api } = props;
|
|
@@ -1776,71 +2057,36 @@ ${extra}
|
|
|
1776
2057
|
const [open, setOpen] = (0, react.useState)(props.view === "page");
|
|
1777
2058
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
1778
2059
|
const [epoch, setEpoch] = (0, react.useState)(0);
|
|
1779
|
-
const [kitSource, setKitSource] = (0, react.useState)(null);
|
|
1780
|
-
const [modelsDevStatus, setModelsDevStatus] = (0, react.useState)(null);
|
|
1781
2060
|
const [saving, setSaving] = (0, react.useState)(false);
|
|
1782
2061
|
const [refreshing, setRefreshing] = (0, react.useState)(false);
|
|
1783
2062
|
const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
|
|
2063
|
+
const { kitSource, modelsDevStatus, setModelsDevStatus } = useRuntimeDiagnostics();
|
|
1784
2064
|
(0, react.useEffect)(() => {
|
|
1785
2065
|
if (value === void 0 || draft !== null) return;
|
|
1786
2066
|
setDraft(draftFromValue(value));
|
|
1787
2067
|
}, [value, draft]);
|
|
1788
|
-
(0, react.useEffect)(() => {
|
|
1789
|
-
let alive = true;
|
|
1790
|
-
fetchCatalog("", "models-dev").then((result) => {
|
|
1791
|
-
if (!alive) return;
|
|
1792
|
-
setKitSource(result.kitSource ?? null);
|
|
1793
|
-
setModelsDevStatus(result.status ?? null);
|
|
1794
|
-
if (result.compat !== void 0) installCompatFields(result.compat.fields);
|
|
1795
|
-
}).catch(() => {});
|
|
1796
|
-
return () => {
|
|
1797
|
-
alive = false;
|
|
1798
|
-
};
|
|
1799
|
-
}, []);
|
|
1800
2068
|
const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
|
|
1801
2069
|
const invalid = (0, react.useMemo)(() => {
|
|
1802
2070
|
if (draft === null) return false;
|
|
1803
2071
|
return !numTextOk(draft.catalogRefreshHours) || Object.values(draft.providers).some((provider) => provider.models.some((model) => model.id.trim() === ""));
|
|
1804
2072
|
}, [draft]);
|
|
1805
2073
|
if (props.view === "summary") return t("summaryLine");
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
})
|
|
2074
|
+
const variant = cardVariantFor(props.view);
|
|
2075
|
+
if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
|
|
2076
|
+
prefix: "lpc",
|
|
2077
|
+
variant,
|
|
2078
|
+
text: t("loading")
|
|
1812
2079
|
});
|
|
1813
2080
|
const setProvider = (route, patch) => {
|
|
1814
|
-
|
|
1815
|
-
setDraft({
|
|
1816
|
-
...draft,
|
|
1817
|
-
providers: {
|
|
1818
|
-
...draft.providers,
|
|
1819
|
-
[route]: {
|
|
1820
|
-
...current,
|
|
1821
|
-
...patch
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
2081
|
+
setDraft(withProviderPatch(draft, route, patch));
|
|
1825
2082
|
setStatus(IDLE_STATUS);
|
|
1826
2083
|
};
|
|
1827
2084
|
const onAddRoute = (key) => {
|
|
1828
|
-
setDraft(
|
|
1829
|
-
...draft,
|
|
1830
|
-
providers: {
|
|
1831
|
-
...draft.providers,
|
|
1832
|
-
[key]: emptyProviderDraft()
|
|
1833
|
-
}
|
|
1834
|
-
});
|
|
2085
|
+
setDraft(withNewRoute(draft, key));
|
|
1835
2086
|
setStatus(IDLE_STATUS);
|
|
1836
2087
|
};
|
|
1837
2088
|
const onRemoveRoute = (route) => {
|
|
1838
|
-
|
|
1839
|
-
delete next[route];
|
|
1840
|
-
setDraft({
|
|
1841
|
-
...draft,
|
|
1842
|
-
providers: next
|
|
1843
|
-
});
|
|
2089
|
+
setDraft(withoutRoute(draft, route));
|
|
1844
2090
|
setStatus(IDLE_STATUS);
|
|
1845
2091
|
};
|
|
1846
2092
|
const onSave = () => {
|
|
@@ -1891,6 +2137,7 @@ ${extra}
|
|
|
1891
2137
|
description: t("description"),
|
|
1892
2138
|
open,
|
|
1893
2139
|
onToggle: setOpen,
|
|
2140
|
+
variant,
|
|
1894
2141
|
statusBadge: {
|
|
1895
2142
|
text: t(draft.enabled ? "enabledOn" : "enabledOff"),
|
|
1896
2143
|
on: draft.enabled
|
|
@@ -1899,116 +2146,39 @@ ${extra}
|
|
|
1899
2146
|
dirtyLabel: t("unsaved"),
|
|
1900
2147
|
readOnlyNotice: disabled ? t("readOnly") : void 0,
|
|
1901
2148
|
status,
|
|
1902
|
-
actions:
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2149
|
+
actions: cardActions({
|
|
2150
|
+
t,
|
|
2151
|
+
dirty,
|
|
2152
|
+
invalid,
|
|
2153
|
+
saving,
|
|
2154
|
+
refreshing,
|
|
2155
|
+
disabled,
|
|
2156
|
+
onRefresh: onRefreshCatalog,
|
|
2157
|
+
onDiscard,
|
|
2158
|
+
onSave
|
|
2159
|
+
}),
|
|
2160
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootFields, {
|
|
2161
|
+
t,
|
|
2162
|
+
draft,
|
|
2163
|
+
disabled,
|
|
2164
|
+
kitSource,
|
|
2165
|
+
modelsDevStatus,
|
|
2166
|
+
onEdit: (patch) => {
|
|
2167
|
+
setDraft({
|
|
2168
|
+
...draft,
|
|
2169
|
+
...patch
|
|
2170
|
+
});
|
|
2171
|
+
setStatus(IDLE_STATUS);
|
|
1921
2172
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
setDraft({
|
|
1932
|
-
...draft,
|
|
1933
|
-
enabled: value
|
|
1934
|
-
});
|
|
1935
|
-
setStatus(IDLE_STATUS);
|
|
1936
|
-
}
|
|
1937
|
-
}),
|
|
1938
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
1939
|
-
prefix: "lpc",
|
|
1940
|
-
id: "lpc-catalogUrl",
|
|
1941
|
-
label: t("catalogUrl"),
|
|
1942
|
-
hint: t("catalogUrlHint"),
|
|
1943
|
-
value: draft.catalogUrl,
|
|
1944
|
-
disabled,
|
|
1945
|
-
onEdit: (value) => {
|
|
1946
|
-
setDraft({
|
|
1947
|
-
...draft,
|
|
1948
|
-
catalogUrl: value
|
|
1949
|
-
});
|
|
1950
|
-
setStatus(IDLE_STATUS);
|
|
1951
|
-
}
|
|
1952
|
-
}),
|
|
1953
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
1954
|
-
prefix: "lpc",
|
|
1955
|
-
id: "lpc-catalogRefresh",
|
|
1956
|
-
label: t("catalogRefreshHours"),
|
|
1957
|
-
hint: t("catalogRefreshHoursHint"),
|
|
1958
|
-
value: draft.catalogRefreshHours,
|
|
1959
|
-
numeric: true,
|
|
1960
|
-
disabled,
|
|
1961
|
-
invalid: !numTextOk(draft.catalogRefreshHours),
|
|
1962
|
-
invalidLabel: t("invalidNumber"),
|
|
1963
|
-
onEdit: (value) => {
|
|
1964
|
-
setDraft({
|
|
1965
|
-
...draft,
|
|
1966
|
-
catalogRefreshHours: value
|
|
1967
|
-
});
|
|
1968
|
-
setStatus(IDLE_STATUS);
|
|
1969
|
-
}
|
|
1970
|
-
}),
|
|
1971
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
|
|
1972
|
-
prefix: "lpc",
|
|
1973
|
-
id: "lpc-catalogProxy",
|
|
1974
|
-
label: t("catalogProxy"),
|
|
1975
|
-
hint: t("catalogProxyHint"),
|
|
1976
|
-
value: draft.catalogProxy,
|
|
1977
|
-
disabled,
|
|
1978
|
-
onEdit: (value) => {
|
|
1979
|
-
setDraft({
|
|
1980
|
-
...draft,
|
|
1981
|
-
catalogProxy: value
|
|
1982
|
-
});
|
|
1983
|
-
setStatus(IDLE_STATUS);
|
|
1984
|
-
}
|
|
1985
|
-
}),
|
|
1986
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1987
|
-
className: "lpc-statusRow",
|
|
1988
|
-
children: [
|
|
1989
|
-
t("kitSource"),
|
|
1990
|
-
":",
|
|
1991
|
-
kitSource ?? ""
|
|
1992
|
-
]
|
|
1993
|
-
}),
|
|
1994
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1995
|
-
className: "lpc-statusRow",
|
|
1996
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1997
|
-
t("modelsDevStatus"),
|
|
1998
|
-
":",
|
|
1999
|
-
modelsDevText(modelsDevStatus, t)
|
|
2000
|
-
] })
|
|
2001
|
-
}),
|
|
2002
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
|
|
2003
|
-
providers: draft.providers,
|
|
2004
|
-
epoch,
|
|
2005
|
-
disabled,
|
|
2006
|
-
t,
|
|
2007
|
-
onAddRoute,
|
|
2008
|
-
onRemoveRoute,
|
|
2009
|
-
onPatchProvider: setProvider
|
|
2010
|
-
})
|
|
2011
|
-
]
|
|
2173
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
|
|
2174
|
+
providers: draft.providers,
|
|
2175
|
+
epoch,
|
|
2176
|
+
disabled,
|
|
2177
|
+
t,
|
|
2178
|
+
onAddRoute,
|
|
2179
|
+
onRemoveRoute,
|
|
2180
|
+
onPatchProvider: setProvider
|
|
2181
|
+
})]
|
|
2012
2182
|
});
|
|
2013
2183
|
}
|
|
2014
2184
|
//#endregion
|
|
@@ -2040,6 +2210,8 @@ ${extra}
|
|
|
2040
2210
|
refreshOk: "目录已拉取。",
|
|
2041
2211
|
refreshFailed: "拉取失败:",
|
|
2042
2212
|
advancedGroup: "高级设置(retryPolicy / compat)",
|
|
2213
|
+
extraFields: "其余字段(JSON)",
|
|
2214
|
+
extraFieldsHint: "表单未覆盖的 wire 字段原样往返:如 adapter=deepseek 的 thinking / reasoningEffort、Files API 与图片配额等。对象整体合并进该 route(模型级同 key 覆盖 route 级),非法 JSON 不会提交。",
|
|
2043
2215
|
providersGroup: "Provider 路由",
|
|
2044
2216
|
addRoute: "新增 route",
|
|
2045
2217
|
addRoutePlaceholder: "新 route 键名(如 my-llm)",
|
|
@@ -2122,6 +2294,8 @@ ${extra}
|
|
|
2122
2294
|
refreshOk: "Catalog refreshed.",
|
|
2123
2295
|
refreshFailed: "Refresh failed: ",
|
|
2124
2296
|
advancedGroup: "Advanced (retryPolicy / compat)",
|
|
2297
|
+
extraFields: "Other fields (JSON)",
|
|
2298
|
+
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.",
|
|
2125
2299
|
providersGroup: "Provider routes",
|
|
2126
2300
|
addRoute: "Add route",
|
|
2127
2301
|
addRoutePlaceholder: "New route key (e.g. my-llm)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/llm-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 基于 PiAiAdapter 的自定义 LLM 路由(全量 compat、模型继承、models.dev 目录兜底)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"https-proxy-agent": "^9.1.0",
|
|
35
|
-
"@dsh-plus/shared": "0.1.
|
|
35
|
+
"@dsh-plus/shared": "0.1.31"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.4",
|
package/src/client/card.tsx
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
+
type CardAction,
|
|
13
14
|
CardChrome,
|
|
15
|
+
CardLoading,
|
|
14
16
|
type CardStatusState,
|
|
15
|
-
|
|
17
|
+
cardVariantFor,
|
|
16
18
|
IDLE_STATUS,
|
|
17
19
|
type NamespaceSettingsApi,
|
|
18
20
|
type PluginConfigViewProps,
|
|
19
21
|
type Scope,
|
|
20
|
-
TextField,
|
|
21
22
|
} from '@dsh-plus/shared/client'
|
|
22
23
|
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
23
24
|
import { type ConfigValue, fetchCatalog, refreshCatalog, type WireModelsDevStatus } from './api.ts'
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
toPatch,
|
|
32
33
|
} from './draft.ts'
|
|
33
34
|
import type { Translate } from './i18n.ts'
|
|
35
|
+
import { RootFields } from './root-fields.tsx'
|
|
34
36
|
import { ProvidersSection } from './views/providers.tsx'
|
|
35
37
|
|
|
36
38
|
export interface CardProps extends PluginConfigViewProps {
|
|
@@ -39,11 +41,81 @@ export interface CardProps extends PluginConfigViewProps {
|
|
|
39
41
|
api: NamespaceSettingsApi
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
/** 运行期诊断行数据来源:模型目录端点(compat 字段表同批安装)。 */
|
|
45
|
+
function useRuntimeDiagnostics(): {
|
|
46
|
+
kitSource: string | null
|
|
47
|
+
modelsDevStatus: WireModelsDevStatus | null
|
|
48
|
+
setModelsDevStatus(next: WireModelsDevStatus): void
|
|
49
|
+
} {
|
|
50
|
+
const [kitSource, setKitSource] = useState<string | null>(null)
|
|
51
|
+
const [modelsDevStatus, setModelsDevStatus] = useState<WireModelsDevStatus | null>(null)
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
let alive = true
|
|
54
|
+
fetchCatalog('', 'models-dev')
|
|
55
|
+
.then((result) => {
|
|
56
|
+
if (!alive) return
|
|
57
|
+
setKitSource(result.kitSource ?? null)
|
|
58
|
+
setModelsDevStatus(result.status ?? null)
|
|
59
|
+
if (result.compat !== undefined) installCompatFields(result.compat.fields)
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {})
|
|
62
|
+
return () => {
|
|
63
|
+
alive = false
|
|
64
|
+
}
|
|
65
|
+
}, [])
|
|
66
|
+
return { kitSource, modelsDevStatus, setModelsDevStatus }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 路由级草稿改写:改字段 / 新增 route / 删除 route(返回新草稿,不改入参)。 */
|
|
70
|
+
function withProviderPatch(draft: Draft, route: string, patch: Partial<ProviderDraft>): Draft {
|
|
71
|
+
const current = draft.providers[route] ?? emptyProviderDraft()
|
|
72
|
+
return { ...draft, providers: { ...draft.providers, [route]: { ...current, ...patch } } }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function withNewRoute(draft: Draft, key: string): Draft {
|
|
76
|
+
return { ...draft, providers: { ...draft.providers, [key]: emptyProviderDraft() } }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function withoutRoute(draft: Draft, route: string): Draft {
|
|
80
|
+
const next = { ...draft.providers }
|
|
81
|
+
delete next[route]
|
|
82
|
+
return { ...draft, providers: next }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 卡片动作:手动拉取目录、放弃、保存。 */
|
|
86
|
+
function cardActions(options: {
|
|
87
|
+
t: Translate
|
|
88
|
+
dirty: boolean
|
|
89
|
+
invalid: boolean
|
|
90
|
+
saving: boolean
|
|
91
|
+
refreshing: boolean
|
|
92
|
+
disabled: boolean
|
|
93
|
+
onRefresh(): void
|
|
94
|
+
onDiscard(): void
|
|
95
|
+
onSave(): void
|
|
96
|
+
}): CardAction[] {
|
|
97
|
+
const { t } = options
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
key: 'refresh',
|
|
101
|
+
label: t(options.refreshing ? 'refreshingCatalog' : 'refreshCatalog'),
|
|
102
|
+
disabled: options.disabled || options.refreshing,
|
|
103
|
+
onClick: options.onRefresh,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: 'discard',
|
|
107
|
+
label: t('discard'),
|
|
108
|
+
disabled: !options.dirty || options.saving,
|
|
109
|
+
onClick: options.onDiscard,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: 'save',
|
|
113
|
+
label: t(options.saving ? 'saving' : 'save'),
|
|
114
|
+
variant: 'primary',
|
|
115
|
+
disabled: !options.dirty || options.invalid || options.saving || options.disabled,
|
|
116
|
+
onClick: options.onSave,
|
|
117
|
+
},
|
|
118
|
+
]
|
|
47
119
|
}
|
|
48
120
|
|
|
49
121
|
export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
@@ -57,11 +129,10 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
57
129
|
const [open, setOpen] = useState(props.view === 'page')
|
|
58
130
|
const [draft, setDraft] = useState<Draft | null>(null)
|
|
59
131
|
const [epoch, setEpoch] = useState(0)
|
|
60
|
-
const [kitSource, setKitSource] = useState<string | null>(null)
|
|
61
|
-
const [modelsDevStatus, setModelsDevStatus] = useState<WireModelsDevStatus | null>(null)
|
|
62
132
|
const [saving, setSaving] = useState(false)
|
|
63
133
|
const [refreshing, setRefreshing] = useState(false)
|
|
64
134
|
const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
|
|
135
|
+
const { kitSource, modelsDevStatus, setModelsDevStatus } = useRuntimeDiagnostics()
|
|
65
136
|
|
|
66
137
|
// 首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑(与官方 staged 表单一致)。
|
|
67
138
|
useEffect(() => {
|
|
@@ -69,23 +140,6 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
69
140
|
setDraft(draftFromValue(value))
|
|
70
141
|
}, [value, draft])
|
|
71
142
|
|
|
72
|
-
// 运行期诊断行(kitSource / models-dev 状态)来自模型目录端点,非配置数据。
|
|
73
|
-
// compat 字段表同批下发:安装后 UI 渲染与服务端校验同源(不再手抄镜像表)。
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
let alive = true
|
|
76
|
-
fetchCatalog('', 'models-dev')
|
|
77
|
-
.then((result) => {
|
|
78
|
-
if (!alive) return
|
|
79
|
-
setKitSource(result.kitSource ?? null)
|
|
80
|
-
setModelsDevStatus(result.status ?? null)
|
|
81
|
-
if (result.compat !== undefined) installCompatFields(result.compat.fields)
|
|
82
|
-
})
|
|
83
|
-
.catch(() => {})
|
|
84
|
-
return () => {
|
|
85
|
-
alive = false
|
|
86
|
-
}
|
|
87
|
-
}, [])
|
|
88
|
-
|
|
89
143
|
const dirty = useMemo(
|
|
90
144
|
() =>
|
|
91
145
|
value !== undefined &&
|
|
@@ -106,33 +160,23 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
106
160
|
// 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
|
|
107
161
|
if (props.view === 'summary') return t('summaryLine')
|
|
108
162
|
|
|
163
|
+
// 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
|
|
164
|
+
const variant = cardVariantFor(props.view)
|
|
165
|
+
|
|
109
166
|
if (value === undefined || draft === null) {
|
|
110
|
-
return (
|
|
111
|
-
<li className="lpc-card">
|
|
112
|
-
<p className="lpc-readOnly">{t('loading')}</p>
|
|
113
|
-
</li>
|
|
114
|
-
)
|
|
167
|
+
return <CardLoading prefix="lpc" variant={variant} text={t('loading')} />
|
|
115
168
|
}
|
|
116
169
|
|
|
117
170
|
const setProvider = (route: string, patch: Partial<ProviderDraft>): void => {
|
|
118
|
-
|
|
119
|
-
setDraft({
|
|
120
|
-
...draft,
|
|
121
|
-
providers: { ...draft.providers, [route]: { ...current, ...patch } },
|
|
122
|
-
})
|
|
171
|
+
setDraft(withProviderPatch(draft, route, patch))
|
|
123
172
|
setStatus(IDLE_STATUS)
|
|
124
173
|
}
|
|
125
174
|
const onAddRoute = (key: string): void => {
|
|
126
|
-
setDraft(
|
|
127
|
-
...draft,
|
|
128
|
-
providers: { ...draft.providers, [key]: emptyProviderDraft() },
|
|
129
|
-
})
|
|
175
|
+
setDraft(withNewRoute(draft, key))
|
|
130
176
|
setStatus(IDLE_STATUS)
|
|
131
177
|
}
|
|
132
178
|
const onRemoveRoute = (route: string): void => {
|
|
133
|
-
|
|
134
|
-
delete next[route]
|
|
135
|
-
setDraft({ ...draft, providers: next })
|
|
179
|
+
setDraft(withoutRoute(draft, route))
|
|
136
180
|
setStatus(IDLE_STATUS)
|
|
137
181
|
}
|
|
138
182
|
const onSave = (): void => {
|
|
@@ -180,91 +224,35 @@ export function LlmPiCard(props: CardProps): ReactElement | string | null {
|
|
|
180
224
|
description={t('description')}
|
|
181
225
|
open={open}
|
|
182
226
|
onToggle={setOpen}
|
|
227
|
+
variant={variant}
|
|
183
228
|
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
184
229
|
dirty={dirty}
|
|
185
230
|
dirtyLabel={t('unsaved')}
|
|
186
231
|
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
187
232
|
status={status}
|
|
188
|
-
actions={
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
onClick: onDiscard,
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
key: 'save',
|
|
203
|
-
label: t(saving ? 'saving' : 'save'),
|
|
204
|
-
variant: 'primary',
|
|
205
|
-
disabled: !dirty || invalid || saving || disabled,
|
|
206
|
-
onClick: onSave,
|
|
207
|
-
},
|
|
208
|
-
]}
|
|
233
|
+
actions={cardActions({
|
|
234
|
+
t,
|
|
235
|
+
dirty,
|
|
236
|
+
invalid,
|
|
237
|
+
saving,
|
|
238
|
+
refreshing,
|
|
239
|
+
disabled,
|
|
240
|
+
onRefresh: onRefreshCatalog,
|
|
241
|
+
onDiscard,
|
|
242
|
+
onSave,
|
|
243
|
+
})}
|
|
209
244
|
>
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
label={t('enabled')}
|
|
214
|
-
checked={draft.enabled}
|
|
215
|
-
disabled={disabled}
|
|
216
|
-
onEdit={(value) => {
|
|
217
|
-
setDraft({ ...draft, enabled: value })
|
|
218
|
-
setStatus(IDLE_STATUS)
|
|
219
|
-
}}
|
|
220
|
-
/>
|
|
221
|
-
<TextField
|
|
222
|
-
prefix="lpc"
|
|
223
|
-
id="lpc-catalogUrl"
|
|
224
|
-
label={t('catalogUrl')}
|
|
225
|
-
hint={t('catalogUrlHint')}
|
|
226
|
-
value={draft.catalogUrl}
|
|
227
|
-
disabled={disabled}
|
|
228
|
-
onEdit={(value) => {
|
|
229
|
-
setDraft({ ...draft, catalogUrl: value })
|
|
230
|
-
setStatus(IDLE_STATUS)
|
|
231
|
-
}}
|
|
232
|
-
/>
|
|
233
|
-
<TextField
|
|
234
|
-
prefix="lpc"
|
|
235
|
-
id="lpc-catalogRefresh"
|
|
236
|
-
label={t('catalogRefreshHours')}
|
|
237
|
-
hint={t('catalogRefreshHoursHint')}
|
|
238
|
-
value={draft.catalogRefreshHours}
|
|
239
|
-
numeric
|
|
240
|
-
disabled={disabled}
|
|
241
|
-
invalid={!numTextOk(draft.catalogRefreshHours)}
|
|
242
|
-
invalidLabel={t('invalidNumber')}
|
|
243
|
-
onEdit={(value) => {
|
|
244
|
-
setDraft({ ...draft, catalogRefreshHours: value })
|
|
245
|
-
setStatus(IDLE_STATUS)
|
|
246
|
-
}}
|
|
247
|
-
/>
|
|
248
|
-
<TextField
|
|
249
|
-
prefix="lpc"
|
|
250
|
-
id="lpc-catalogProxy"
|
|
251
|
-
label={t('catalogProxy')}
|
|
252
|
-
hint={t('catalogProxyHint')}
|
|
253
|
-
value={draft.catalogProxy}
|
|
245
|
+
<RootFields
|
|
246
|
+
t={t}
|
|
247
|
+
draft={draft}
|
|
254
248
|
disabled={disabled}
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
kitSource={kitSource}
|
|
250
|
+
modelsDevStatus={modelsDevStatus}
|
|
251
|
+
onEdit={(patch) => {
|
|
252
|
+
setDraft({ ...draft, ...patch })
|
|
257
253
|
setStatus(IDLE_STATUS)
|
|
258
254
|
}}
|
|
259
255
|
/>
|
|
260
|
-
<p className="lpc-statusRow">
|
|
261
|
-
{t('kitSource')}:{kitSource ?? ''}
|
|
262
|
-
</p>
|
|
263
|
-
<div className="lpc-statusRow">
|
|
264
|
-
<span>
|
|
265
|
-
{t('modelsDevStatus')}:{modelsDevText(modelsDevStatus, t)}
|
|
266
|
-
</span>
|
|
267
|
-
</div>
|
|
268
256
|
<ProvidersSection
|
|
269
257
|
providers={draft.providers}
|
|
270
258
|
epoch={epoch}
|
package/src/client/draft.ts
CHANGED
|
@@ -215,6 +215,18 @@ function extraOf(wire: object, known: Set<string>): Record<string, unknown> {
|
|
|
215
215
|
return out
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* JSON 文本框值 → extra 映射。
|
|
220
|
+
* `undefined`(空文本)表示清空为 `{}`;合法但非对象的 JSON(数组/标量/null)
|
|
221
|
+
* 返回 undefined,调用方据此忽略这次编辑——extra 是「其余 wire 字段」的映射,
|
|
222
|
+
* 静默丢弃已配置字段比拒绝一次编辑更糟。
|
|
223
|
+
*/
|
|
224
|
+
export function extraOfJson(value: unknown): Record<string, unknown> | undefined {
|
|
225
|
+
if (value === undefined) return {}
|
|
226
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
|
|
227
|
+
return { ...(value as Record<string, unknown>) }
|
|
228
|
+
}
|
|
229
|
+
|
|
218
230
|
function modelDraftFromWire(model: WireModel): ModelDraft {
|
|
219
231
|
return {
|
|
220
232
|
id: model.id,
|
package/src/client/i18n.ts
CHANGED
|
@@ -28,6 +28,9 @@ const ownZh = {
|
|
|
28
28
|
refreshOk: '目录已拉取。',
|
|
29
29
|
refreshFailed: '拉取失败:',
|
|
30
30
|
advancedGroup: '高级设置(retryPolicy / compat)',
|
|
31
|
+
extraFields: '其余字段(JSON)',
|
|
32
|
+
extraFieldsHint:
|
|
33
|
+
'表单未覆盖的 wire 字段原样往返:如 adapter=deepseek 的 thinking / reasoningEffort、Files API 与图片配额等。对象整体合并进该 route(模型级同 key 覆盖 route 级),非法 JSON 不会提交。',
|
|
31
34
|
providersGroup: 'Provider 路由',
|
|
32
35
|
addRoute: '新增 route',
|
|
33
36
|
addRoutePlaceholder: '新 route 键名(如 my-llm)',
|
|
@@ -112,6 +115,9 @@ const ownEn = {
|
|
|
112
115
|
refreshOk: 'Catalog refreshed.',
|
|
113
116
|
refreshFailed: 'Refresh failed: ',
|
|
114
117
|
advancedGroup: 'Advanced (retryPolicy / compat)',
|
|
118
|
+
extraFields: 'Other fields (JSON)',
|
|
119
|
+
extraFieldsHint:
|
|
120
|
+
'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.',
|
|
115
121
|
providersGroup: 'Provider routes',
|
|
116
122
|
addRoute: 'Add route',
|
|
117
123
|
addRoutePlaceholder: 'New route key (e.g. my-llm)',
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「LLM 路由」卡片的根字段组:总开关、目录端点/刷新间隔/代理,以及运行期诊断行
|
|
3
|
+
* (kitSource 与 models-dev 快照状态,来自模型目录端点而非配置数据)。
|
|
4
|
+
* @module llm-pi/client/root-fields
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CheckRow, TextField } from '@dsh-plus/shared/client'
|
|
8
|
+
import type { ReactElement } from 'react'
|
|
9
|
+
|
|
10
|
+
import type { WireModelsDevStatus } from './api.ts'
|
|
11
|
+
import { type Draft, numTextOk } from './draft.ts'
|
|
12
|
+
import type { Translate } from './i18n.ts'
|
|
13
|
+
|
|
14
|
+
/** models-dev 状态行文案(缺席/错误/未拉取/正常四态)。 */
|
|
15
|
+
function modelsDevText(status: WireModelsDevStatus | null, t: Translate): string {
|
|
16
|
+
if (status === null) return t('modelsDevEmpty')
|
|
17
|
+
if (status.error !== null) return `${t('modelsDevError')}${status.error}`
|
|
18
|
+
if (status.fetchedAt === null) return t('modelsDevEmpty')
|
|
19
|
+
return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt}`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RootFieldsProps {
|
|
23
|
+
t: Translate
|
|
24
|
+
draft: Draft
|
|
25
|
+
disabled: boolean
|
|
26
|
+
kitSource: string | null
|
|
27
|
+
modelsDevStatus: WireModelsDevStatus | null
|
|
28
|
+
/** 根字段编辑(合并式写入并清空状态行)。 */
|
|
29
|
+
onEdit(patch: Partial<Draft>): void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function RootFields(props: RootFieldsProps): ReactElement {
|
|
33
|
+
const { t, draft, disabled, onEdit } = props
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<CheckRow
|
|
37
|
+
prefix="lpc"
|
|
38
|
+
id="lpc-enabled"
|
|
39
|
+
label={t('enabled')}
|
|
40
|
+
checked={draft.enabled}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
onEdit={(value) => onEdit({ enabled: value })}
|
|
43
|
+
/>
|
|
44
|
+
<TextField
|
|
45
|
+
prefix="lpc"
|
|
46
|
+
id="lpc-catalogUrl"
|
|
47
|
+
label={t('catalogUrl')}
|
|
48
|
+
hint={t('catalogUrlHint')}
|
|
49
|
+
value={draft.catalogUrl}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
onEdit={(value) => onEdit({ catalogUrl: value })}
|
|
52
|
+
/>
|
|
53
|
+
<TextField
|
|
54
|
+
prefix="lpc"
|
|
55
|
+
id="lpc-catalogRefresh"
|
|
56
|
+
label={t('catalogRefreshHours')}
|
|
57
|
+
hint={t('catalogRefreshHoursHint')}
|
|
58
|
+
value={draft.catalogRefreshHours}
|
|
59
|
+
numeric
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
invalid={!numTextOk(draft.catalogRefreshHours)}
|
|
62
|
+
invalidLabel={t('invalidNumber')}
|
|
63
|
+
onEdit={(value) => onEdit({ catalogRefreshHours: value })}
|
|
64
|
+
/>
|
|
65
|
+
<TextField
|
|
66
|
+
prefix="lpc"
|
|
67
|
+
id="lpc-catalogProxy"
|
|
68
|
+
label={t('catalogProxy')}
|
|
69
|
+
hint={t('catalogProxyHint')}
|
|
70
|
+
value={draft.catalogProxy}
|
|
71
|
+
disabled={disabled}
|
|
72
|
+
onEdit={(value) => onEdit({ catalogProxy: value })}
|
|
73
|
+
/>
|
|
74
|
+
<p className="lpc-statusRow">
|
|
75
|
+
{t('kitSource')}:{props.kitSource ?? ''}
|
|
76
|
+
</p>
|
|
77
|
+
<div className="lpc-statusRow">
|
|
78
|
+
<span>
|
|
79
|
+
{t('modelsDevStatus')}:{modelsDevText(props.modelsDevStatus, t)}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -9,8 +9,8 @@ import { CheckRow } from '@dsh-plus/shared/client'
|
|
|
9
9
|
import { type ReactElement, useEffect, useState } from 'react'
|
|
10
10
|
import { fetchCatalog, type WireModelsDevStatus } from '../api.ts'
|
|
11
11
|
import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
|
|
12
|
-
import { emptyModelDraft, type ModelDraft, type ReasoningDraft } from '../draft.ts'
|
|
13
|
-
import { TextField } from '../fields.tsx'
|
|
12
|
+
import { emptyModelDraft, extraOfJson, type ModelDraft, type ReasoningDraft } from '../draft.ts'
|
|
13
|
+
import { JsonField, TextField } from '../fields.tsx'
|
|
14
14
|
import type { Translate } from '../i18n.ts'
|
|
15
15
|
import { CompatEditor } from './compat.tsx'
|
|
16
16
|
|
|
@@ -261,6 +261,21 @@ export function ModelRow(props: ModelRowProps): ReactElement {
|
|
|
261
261
|
t={t}
|
|
262
262
|
onEdit={(compat) => props.onPatch({ compat })}
|
|
263
263
|
/>
|
|
264
|
+
<JsonField
|
|
265
|
+
id={`${id}-extra`}
|
|
266
|
+
label={t('extraFields')}
|
|
267
|
+
hint={t('extraFieldsHint')}
|
|
268
|
+
invalidText={t('invalidJson')}
|
|
269
|
+
value={Object.keys(model.extra).length === 0 ? undefined : model.extra}
|
|
270
|
+
epoch={props.epoch}
|
|
271
|
+
disabled={props.disabled === true}
|
|
272
|
+
wide
|
|
273
|
+
onEdit={(extra) => {
|
|
274
|
+
const next = extraOfJson(extra)
|
|
275
|
+
// 合法但非对象的 JSON(数组/标量)忽略这次编辑,不静默清空已配置字段。
|
|
276
|
+
if (next !== undefined) props.onPatch({ extra: next })
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
264
279
|
</div>
|
|
265
280
|
)
|
|
266
281
|
}
|
|
@@ -9,6 +9,7 @@ import { ChevronDownIcon } from '@dsh-plus/shared/client'
|
|
|
9
9
|
import { type ReactElement, useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import type { ProviderDraft } from '../draft.ts'
|
|
12
|
+
import { extraOfJson } from '../draft.ts'
|
|
12
13
|
import { CollapseSection, JsonField, KeyValueEditor } from '../fields.tsx'
|
|
13
14
|
import type { Translate } from '../i18n.ts'
|
|
14
15
|
import { CompatEditor } from './compat.tsx'
|
|
@@ -145,6 +146,21 @@ export function ProviderSection(props: ProviderSectionProps): ReactElement {
|
|
|
145
146
|
onEdit={(headers) => props.onPatch({ headers })}
|
|
146
147
|
/>
|
|
147
148
|
<CollapseSection id={`${id}-advanced`} title={t('advancedGroup')} defaultOpen={false}>
|
|
149
|
+
<JsonField
|
|
150
|
+
id={`${id}-extra`}
|
|
151
|
+
label={t('extraFields')}
|
|
152
|
+
hint={t('extraFieldsHint')}
|
|
153
|
+
invalidText={t('invalidJson')}
|
|
154
|
+
value={Object.keys(draft.extra).length === 0 ? undefined : draft.extra}
|
|
155
|
+
epoch={props.epoch}
|
|
156
|
+
disabled={props.disabled === true}
|
|
157
|
+
wide
|
|
158
|
+
onEdit={(extra) => {
|
|
159
|
+
const next = extraOfJson(extra)
|
|
160
|
+
// 合法但非对象的 JSON(数组/标量)忽略这次编辑,不静默清空已配置字段。
|
|
161
|
+
if (next !== undefined) props.onPatch({ extra: next })
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
148
164
|
<JsonField
|
|
149
165
|
id={`${id}-retry`}
|
|
150
166
|
label={t('retryPolicy')}
|