@dsh-plus/llm-pi 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,8 +4,338 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
+ //#region ../shared/src/client/card.tsx
10
+ const IDLE_STATUS = {
11
+ kind: "idle",
12
+ text: ""
13
+ };
14
+ function CardChrome(props) {
15
+ const p = props.prefix;
16
+ const badge = props.statusBadge;
17
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
18
+ className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
19
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
20
+ type: "button",
21
+ className: `${p}-header`,
22
+ "aria-expanded": props.open,
23
+ "aria-label": props.title,
24
+ onClick: () => props.onToggle(!props.open),
25
+ children: [
26
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
27
+ className: `${p}-headText`,
28
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
29
+ className: `${p}-name`,
30
+ children: props.title
31
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
32
+ className: `${p}-description`,
33
+ children: props.description
34
+ })]
35
+ }),
36
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
37
+ className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
38
+ children: badge.text
39
+ }) : null,
40
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
41
+ className: `${p}-pending`,
42
+ children: props.dirtyLabel
43
+ }) : null,
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
45
+ className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}`,
46
+ children: "▾"
47
+ })
48
+ ]
49
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
50
+ className: `${p}-body`,
51
+ children: [
52
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
53
+ className: `${p}-readOnly`,
54
+ role: "status",
55
+ children: props.readOnlyNotice
56
+ }) : null,
57
+ props.children,
58
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
59
+ className: `${p}-footer`,
60
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
61
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
62
+ role: "status",
63
+ children: props.status.text
64
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
65
+ type: "button",
66
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
67
+ disabled: action.disabled === true,
68
+ onClick: action.onClick,
69
+ children: action.label
70
+ }, action.key))]
71
+ })
72
+ ]
73
+ }) : null]
74
+ });
75
+ }
76
+ //#endregion
77
+ //#region ../shared/src/client/fields.tsx
78
+ function TextField$1(props) {
79
+ const p = props.prefix ?? "dshp";
80
+ const invalid = props.invalid === true;
81
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
82
+ className: `${p}-field`,
83
+ children: [
84
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
85
+ className: `${p}-head`,
86
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
87
+ className: `${p}-label`,
88
+ htmlFor: props.id,
89
+ children: props.label
90
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
91
+ className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
92
+ children: props.badge.text
93
+ }) : null]
94
+ }),
95
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
96
+ id: props.id,
97
+ className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
98
+ type: props.password === true ? "password" : "text",
99
+ inputMode: props.numeric === true ? "numeric" : void 0,
100
+ autoComplete: props.password === true ? "off" : void 0,
101
+ "aria-invalid": invalid || void 0,
102
+ value: props.value,
103
+ disabled: props.disabled === true,
104
+ onChange: (event) => props.onEdit(event.target.value)
105
+ }),
106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
107
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
108
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
109
+ })
110
+ ]
111
+ });
112
+ }
113
+ function CheckRow(props) {
114
+ const p = props.prefix ?? "dshp";
115
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
116
+ className: `${p}-checkRow`,
117
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
118
+ id: props.id,
119
+ type: "checkbox",
120
+ checked: props.checked,
121
+ disabled: props.disabled === true,
122
+ onChange: (event) => props.onEdit(event.target.checked)
123
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
124
+ htmlFor: props.id,
125
+ children: props.label
126
+ })]
127
+ });
128
+ }
129
+ //#endregion
130
+ //#region ../shared/src/client/i18n.ts
131
+ /** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
132
+ const commonZh = {
133
+ save: "保存",
134
+ saving: "保存中…",
135
+ discard: "放弃",
136
+ unsaved: "未保存",
137
+ loading: "加载中…",
138
+ unavailable: "配置服务不可用。",
139
+ readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
140
+ invalidNumber: "请输入正整数。",
141
+ collapse: "收起",
142
+ expand: "展开",
143
+ enabledOn: "已启用",
144
+ enabledOff: "未启用",
145
+ saveFailed: "保存失败,请检查填写内容。",
146
+ confirm: "确认",
147
+ cancel: "取消"
148
+ };
149
+ const commonEn = {
150
+ save: "Save",
151
+ saving: "Saving…",
152
+ discard: "Discard",
153
+ unsaved: "Unsaved",
154
+ loading: "Loading…",
155
+ unavailable: "Settings service unavailable.",
156
+ readOnly: "No settings provider in this deployment; config is read-only. Edit settings.yaml.",
157
+ invalidNumber: "Enter a positive integer.",
158
+ collapse: "Collapse",
159
+ expand: "Expand",
160
+ enabledOn: "On",
161
+ enabledOff: "Off",
162
+ saveFailed: "Save failed; check the fields.",
163
+ confirm: "Confirm",
164
+ cancel: "Cancel"
165
+ };
166
+ /** own 覆盖同键合并(返回新对象,入参不可变)。 */
167
+ function mergeDict(base, own) {
168
+ return {
169
+ ...base,
170
+ ...own
171
+ };
172
+ }
173
+ //#endregion
174
+ //#region ../shared/src/client/scope.ts
175
+ /**
176
+ * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
177
+ * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
178
+ * 刷新:remote `settings/document-updated`(按命名空间过滤)与
179
+ * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
180
+ * (loopback 或 tailnet 信任域名)下行为一致。
181
+ * effect 归属调用方 fiber(label 需含插件名以便诊断)。
182
+ */
183
+ function createApiScope(api, ns, c, label) {
184
+ let state = {
185
+ status: "loading",
186
+ value: void 0,
187
+ revision: void 0,
188
+ writable: false
189
+ };
190
+ const listeners = /* @__PURE__ */ new Set();
191
+ let generation = 0;
192
+ const publish = (next) => {
193
+ state = next;
194
+ for (const listener of [...listeners]) listener();
195
+ };
196
+ const load = async () => {
197
+ const gen = ++generation;
198
+ let response;
199
+ try {
200
+ response = await api.describe({});
201
+ } catch {
202
+ return;
203
+ }
204
+ if (gen !== generation || !response.result.ok) return;
205
+ const writable = response.result.value?.writable ?? false;
206
+ const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
207
+ if (view === void 0) {
208
+ publish({
209
+ status: "unavailable",
210
+ value: void 0,
211
+ revision: void 0,
212
+ writable
213
+ });
214
+ return;
215
+ }
216
+ publish({
217
+ status: "ready",
218
+ value: view.value,
219
+ revision: view.revision,
220
+ writable
221
+ });
222
+ };
223
+ const refresh = (namespace) => {
224
+ if (namespace !== void 0 && namespace !== ns) return;
225
+ load();
226
+ };
227
+ const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
228
+ load();
229
+ })];
230
+ c.effect(() => () => {
231
+ for (const dispose of disposers) dispose();
232
+ }, label);
233
+ load();
234
+ return {
235
+ getSnapshot: () => state,
236
+ subscribe: (listener) => {
237
+ listeners.add(listener);
238
+ return () => {
239
+ listeners.delete(listener);
240
+ };
241
+ },
242
+ load
243
+ };
244
+ }
245
+ //#endregion
246
+ //#region ../shared/src/client/styles.ts
247
+ /**
248
+ * 配置卡片样式工厂(各插件 client 的公共收编版)。
249
+ * 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
250
+ * 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
251
+ * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
252
+ * - footer 按钮等宽换行、状态文字独占一行;
253
+ * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
254
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
255
+ * @module @dsh-plus/shared/client/styles
256
+ */
257
+ /** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
258
+ function cardCss(prefix, extra = "") {
259
+ return `
260
+ .${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
261
+ .${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
262
+ .${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
263
+ .${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
264
+ .${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
265
+ .${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
266
+ .${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
267
+ .${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
268
+ .${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
269
+ .${prefix}-chevronOpen{transform:rotate(180deg)}
270
+ .${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
271
+ .${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
272
+ .${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
273
+ .${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
274
+ .${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
275
+ .${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
276
+ .${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
277
+ .${prefix}-head{align-items:center;gap:8px;display:flex}
278
+ .${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
279
+ .${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
280
+ .${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
281
+ .${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
282
+ .${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
283
+ .${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
284
+ .${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
285
+ .${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
286
+ .${prefix}-select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-tertiary) 50%),linear-gradient(135deg,var(--dsw-alias-label-tertiary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;padding-right:28px}
287
+ .${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
288
+ .${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
289
+ .${prefix}-checkRow{align-items:center;gap:8px;display:flex;padding:6px 0}
290
+ .${prefix}-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
291
+ .${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
292
+ .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
293
+ .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
294
+ .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
295
+ .${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
296
+ .${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
297
+ .${prefix}-statusError{color:var(--dsw-alias-label-error)}
298
+ .${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
299
+ .${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
300
+ .${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
301
+ .${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
302
+ .${prefix}-btn:disabled{opacity:.4;cursor:default}
303
+ .${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
304
+ @media (max-width:767px){
305
+ .${prefix}-body{margin:0 10px}
306
+ .${prefix}-header{padding:12px 12px}
307
+ .${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
308
+ .${prefix}-checkRow{min-height:44px;padding:8px 0}
309
+ .${prefix}-checkRow input{width:18px;height:18px;flex:none}
310
+ .${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
311
+ .${prefix}-status{flex-basis:100%;flex:none}
312
+ .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
313
+ .${prefix}-statusBadge{display:none}
314
+ }
315
+ @media (pointer:coarse){
316
+ .${prefix}-btn{min-height:44px}
317
+ .${prefix}-checkRow{min-height:44px}
318
+ .${prefix}-input,.${prefix}-select{height:40px}
319
+ }
320
+ ${extra}
321
+ `;
322
+ }
323
+ /**
324
+ * 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
325
+ * 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
326
+ */
327
+ function injectCardStyle(pluginId, css) {
328
+ if (typeof document === "undefined") return null;
329
+ const tagId = `${pluginId}/card.css`;
330
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
331
+ const tag = document.createElement("style");
332
+ tag.dataset.plugin = pluginId;
333
+ tag.dataset.pluginCss = tagId;
334
+ tag.textContent = css;
335
+ document.head.appendChild(tag);
336
+ return tag;
337
+ }
338
+ //#endregion
9
339
  //#region src/ns.ts
10
340
  /**
11
341
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
@@ -318,8 +648,9 @@ window.__ModuleLoader__.load({
318
648
  //#endregion
319
649
  //#region src/client/fields.tsx
320
650
  /**
321
- * 配置卡片基础控件:文本/数字输入、勾选行、下拉、键值对编辑、JSON 文本框。
322
- * 视觉对齐官方卡片字段(label + hint + 控件纵列),样式类前缀 lpc-。
651
+ * llm-pi 特有卡片控件:可折叠小节、键值对编辑、JSON 文本框、带 datalist 的
652
+ * 文本字段。通用控件(TextField/CheckRow)走 @dsh-plus/shared/client;
653
+ * 本文件的 SelectField 是 llm-pi 的窄变体(readonly string[] 选项 + 未设置项)。
323
654
  * @module llm-pi/client/fields
324
655
  */
325
656
  function CollapseSection(props) {
@@ -376,21 +707,6 @@ window.__ModuleLoader__.load({
376
707
  ]
377
708
  });
378
709
  }
379
- function CheckRow(props) {
380
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
381
- className: "lpc-checkRow",
382
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
383
- id: props.id,
384
- type: "checkbox",
385
- checked: props.checked,
386
- disabled: props.disabled === true,
387
- onChange: (event) => props.onEdit(event.target.checked)
388
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
389
- htmlFor: props.id,
390
- children: props.label
391
- })]
392
- });
393
- }
394
710
  function SelectField(props) {
395
711
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
396
712
  className: `lpc-field${props.wide === true ? " lpc-wide" : ""}`,
@@ -418,17 +734,8 @@ window.__ModuleLoader__.load({
418
734
  });
419
735
  }
420
736
  function KeyValueEditor(props) {
421
- const update = (index, patch) => {
422
- props.onEdit(props.pairs.map((pair, i) => i === index ? {
423
- ...pair,
424
- ...patch
425
- } : pair));
426
- };
427
- const remove = (index) => {
428
- props.onEdit(props.pairs.filter((_, i) => i !== index));
429
- };
430
737
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
431
- className: "lpc-field lpc-wide",
738
+ className: "lpc-field",
432
739
  children: [
433
740
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
434
741
  className: "lpc-head",
@@ -442,29 +749,45 @@ window.__ModuleLoader__.load({
442
749
  className: "lpc-kvRow",
443
750
  children: [
444
751
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
445
- id: index === 0 ? props.id : void 0,
446
- className: "lpc-input",
447
- value: pair.key,
448
752
  placeholder: props.keyPlaceholder,
753
+ className: "lpc-input lpc-kvKey",
754
+ type: "text",
755
+ value: pair.key,
449
756
  disabled: props.disabled === true,
450
- onChange: (event) => update(index, { key: event.target.value })
757
+ onChange: (event) => {
758
+ const next = [...props.pairs];
759
+ next[index] = {
760
+ ...pair,
761
+ key: event.target.value
762
+ };
763
+ props.onEdit(next);
764
+ }
451
765
  }),
452
766
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
453
- className: "lpc-input",
454
- value: pair.value,
455
767
  placeholder: props.valuePlaceholder,
768
+ className: "lpc-input lpc-kvValue",
769
+ type: "text",
770
+ value: pair.value,
456
771
  disabled: props.disabled === true,
457
- onChange: (event) => update(index, { value: event.target.value })
772
+ onChange: (event) => {
773
+ const next = [...props.pairs];
774
+ next[index] = {
775
+ ...pair,
776
+ value: event.target.value
777
+ };
778
+ props.onEdit(next);
779
+ }
458
780
  }),
459
781
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
460
782
  type: "button",
461
783
  className: "lpc-btn lpc-btnGhost lpc-btnSmall",
462
784
  disabled: props.disabled === true,
463
- onClick: () => remove(index),
464
- children: props.removeLabel
785
+ "aria-label": props.removeLabel,
786
+ onClick: () => props.onEdit(props.pairs.filter((_, i) => i !== index)),
787
+ children: "✕"
465
788
  })
466
789
  ]
467
- }, index)),
790
+ }, `${props.id}-${index}`)),
468
791
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
469
792
  className: "lpc-kvAdd",
470
793
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -940,6 +1263,7 @@ window.__ModuleLoader__.load({
940
1263
  className: "lpc-label",
941
1264
  children: t("input")
942
1265
  }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1266
+ prefix: "lpc",
943
1267
  id: `${id}-input-${modality}`,
944
1268
  label: modality,
945
1269
  checked: model.input[modality],
@@ -1021,6 +1345,12 @@ window.__ModuleLoader__.load({
1021
1345
  }
1022
1346
  //#endregion
1023
1347
  //#region src/client/views/provider-fields.tsx
1348
+ /**
1349
+ * Provider route 的标量字段组:基础字段网格(继承/显示名/端点/凭据/默认容量/
1350
+ * 超时/默认模态/预算)与协议/档位/缓存/传输下拉组。compat、headers、
1351
+ * retryPolicy 与模型目录在 views/providers.tsx 的 route 小节内另行渲染。
1352
+ * @module llm-pi/client/views/provider-fields
1353
+ */
1024
1354
  function ProviderScalarFields(props) {
1025
1355
  const { draft, t } = props;
1026
1356
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1111,6 +1441,7 @@ window.__ModuleLoader__.load({
1111
1441
  className: "lpc-label",
1112
1442
  children: t("defaultInput")
1113
1443
  }), MODALITIES.map((modality) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1444
+ prefix: "lpc",
1114
1445
  id: `${props.id}-input-${modality}`,
1115
1446
  label: modality,
1116
1447
  checked: draft.input[modality],
@@ -1368,14 +1699,9 @@ window.__ModuleLoader__.load({
1368
1699
  * 顶部:enabled / catalogUrl / catalogRefreshHours / 只读状态行(kitSource、
1369
1700
  * modelsDevStatus,来自模型目录端点)+ 保存(settings.replace 全量)与
1370
1701
  * 错误/成功提示;下方为 providers 路由列表(新增/删除/字段编辑/compat/模型
1371
- * 目录,见 views/)。配置读写走官方 settingsScope 传输(scope.ts)。
1372
- * 交互对齐官方卡片与 notify-email:折叠/展开、staged draft、未保存标记。
1702
+ * 目录,见 views/)。外壳与基础控件走 @dsh-plus/shared/client 套件。
1373
1703
  * @module llm-pi/client/card
1374
1704
  */
1375
- const IDLE_STATUS = {
1376
- kind: "idle",
1377
- text: ""
1378
- };
1379
1705
  function modelsDevText(status, t) {
1380
1706
  if (status === null) return t("modelsDevEmpty");
1381
1707
  if (status.error !== null) return `${t("modelsDevError")}${status.error}`;
@@ -1501,167 +1827,141 @@ window.__ModuleLoader__.load({
1501
1827
  }).finally(() => setRefreshing(false));
1502
1828
  };
1503
1829
  const disabled = !snapshot.writable;
1504
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1505
- className: `lpc-card${open ? " lpc-cardOpen" : ""}`,
1506
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1507
- type: "button",
1508
- className: "lpc-header",
1509
- "aria-expanded": open,
1510
- "aria-label": `${t(open ? "collapse" : "expand")}: ${t("title")}`,
1511
- onClick: () => setOpen(!open),
1512
- children: [
1513
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1514
- className: "lpc-headText",
1515
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1516
- className: "lpc-name",
1517
- children: t("title")
1518
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1519
- className: "lpc-description",
1520
- children: t("description")
1521
- })]
1522
- }),
1523
- dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1524
- className: "lpc-pending",
1525
- children: t("unsaved")
1526
- }) : null,
1527
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1528
- className: `lpc-chevron${open ? " lpc-chevronOpen" : ""}`,
1529
- children: "▾"
1530
- })
1531
- ]
1532
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1533
- className: "lpc-body",
1534
- children: [
1535
- disabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1536
- className: "lpc-readOnly",
1537
- role: "status",
1538
- children: t("readOnly")
1539
- }) : null,
1540
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1541
- id: "lpc-enabled",
1542
- label: t("enabled"),
1543
- checked: draft.enabled,
1544
- disabled,
1545
- onEdit: (value) => {
1546
- setDraft({
1547
- ...draft,
1548
- enabled: value
1549
- });
1550
- setStatus(IDLE_STATUS);
1551
- }
1552
- }),
1553
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1554
- id: "lpc-catalogUrl",
1555
- label: t("catalogUrl"),
1556
- hint: t("catalogUrlHint"),
1557
- value: draft.catalogUrl,
1558
- disabled,
1559
- onEdit: (value) => {
1560
- setDraft({
1561
- ...draft,
1562
- catalogUrl: value
1563
- });
1564
- setStatus(IDLE_STATUS);
1565
- }
1566
- }),
1567
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1568
- id: "lpc-catalogRefresh",
1569
- label: t("catalogRefreshHours"),
1570
- hint: t("catalogRefreshHoursHint"),
1571
- value: draft.catalogRefreshHours,
1572
- numeric: true,
1573
- disabled,
1574
- invalid: !numTextOk(draft.catalogRefreshHours),
1575
- invalidLabel: t("invalidNumber"),
1576
- onEdit: (value) => {
1577
- setDraft({
1578
- ...draft,
1579
- catalogRefreshHours: value
1580
- });
1581
- setStatus(IDLE_STATUS);
1582
- }
1583
- }),
1584
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1585
- id: "lpc-catalogProxy",
1586
- label: t("catalogProxy"),
1587
- hint: t("catalogProxyHint"),
1588
- value: draft.catalogProxy,
1589
- disabled,
1590
- onEdit: (value) => {
1591
- setDraft({
1592
- ...draft,
1593
- catalogProxy: value
1594
- });
1595
- setStatus(IDLE_STATUS);
1596
- }
1597
- }),
1598
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1599
- className: "lpc-statusRow",
1600
- children: [
1601
- t("kitSource"),
1602
- "",
1603
- kitSource ?? ""
1604
- ]
1605
- }),
1606
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1607
- className: "lpc-statusRow",
1608
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1609
- t("modelsDevStatus"),
1610
- "",
1611
- modelsDevText(modelsDevStatus, t)
1612
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1613
- type: "button",
1614
- className: "lpc-btn lpc-btnGhost lpc-btnSmall lpc-refreshBtn",
1615
- disabled: disabled || refreshing,
1616
- onClick: onRefreshCatalog,
1617
- children: t(refreshing ? "refreshingCatalog" : "refreshCatalog")
1618
- })]
1619
- }),
1620
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
1621
- providers: draft.providers,
1622
- epoch,
1623
- disabled,
1624
- t,
1625
- onAddRoute,
1626
- onRemoveRoute,
1627
- onPatchProvider: setProvider
1628
- }),
1629
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1630
- className: "lpc-footer",
1631
- children: [
1632
- status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1633
- className: `lpc-status${status.kind === "error" ? " lpc-statusError" : ""}`,
1634
- role: "status",
1635
- children: status.text
1636
- }) : null,
1637
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1638
- type: "button",
1639
- className: "lpc-btn lpc-btnGhost",
1640
- disabled: !dirty || saving,
1641
- onClick: onDiscard,
1642
- children: t("discard")
1643
- }),
1644
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1645
- type: "button",
1646
- className: "lpc-btn lpc-btnPrimary",
1647
- disabled: !dirty || invalid || saving || disabled,
1648
- onClick: onSave,
1649
- children: t(saving ? "saving" : "save")
1650
- })
1651
- ]
1652
- })
1653
- ]
1654
- }) : null]
1830
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
1831
+ prefix: "lpc",
1832
+ title: t("title"),
1833
+ description: t("description"),
1834
+ open,
1835
+ onToggle: setOpen,
1836
+ statusBadge: {
1837
+ text: t(draft.enabled ? "enabledOn" : "enabledOff"),
1838
+ on: draft.enabled
1839
+ },
1840
+ dirty,
1841
+ dirtyLabel: t("unsaved"),
1842
+ readOnlyNotice: disabled ? t("readOnly") : void 0,
1843
+ status,
1844
+ actions: [
1845
+ {
1846
+ key: "refresh",
1847
+ label: t(refreshing ? "refreshingCatalog" : "refreshCatalog"),
1848
+ disabled: disabled || refreshing,
1849
+ onClick: onRefreshCatalog
1850
+ },
1851
+ {
1852
+ key: "discard",
1853
+ label: t("discard"),
1854
+ disabled: !dirty || saving,
1855
+ onClick: onDiscard
1856
+ },
1857
+ {
1858
+ key: "save",
1859
+ label: t(saving ? "saving" : "save"),
1860
+ variant: "primary",
1861
+ disabled: !dirty || invalid || saving || disabled,
1862
+ onClick: onSave
1863
+ }
1864
+ ],
1865
+ children: [
1866
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
1867
+ prefix: "lpc",
1868
+ id: "lpc-enabled",
1869
+ label: t("enabled"),
1870
+ checked: draft.enabled,
1871
+ disabled,
1872
+ onEdit: (value) => {
1873
+ setDraft({
1874
+ ...draft,
1875
+ enabled: value
1876
+ });
1877
+ setStatus(IDLE_STATUS);
1878
+ }
1879
+ }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
1881
+ prefix: "lpc",
1882
+ id: "lpc-catalogUrl",
1883
+ label: t("catalogUrl"),
1884
+ hint: t("catalogUrlHint"),
1885
+ value: draft.catalogUrl,
1886
+ disabled,
1887
+ onEdit: (value) => {
1888
+ setDraft({
1889
+ ...draft,
1890
+ catalogUrl: value
1891
+ });
1892
+ setStatus(IDLE_STATUS);
1893
+ }
1894
+ }),
1895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
1896
+ prefix: "lpc",
1897
+ id: "lpc-catalogRefresh",
1898
+ label: t("catalogRefreshHours"),
1899
+ hint: t("catalogRefreshHoursHint"),
1900
+ value: draft.catalogRefreshHours,
1901
+ numeric: true,
1902
+ disabled,
1903
+ invalid: !numTextOk(draft.catalogRefreshHours),
1904
+ invalidLabel: t("invalidNumber"),
1905
+ onEdit: (value) => {
1906
+ setDraft({
1907
+ ...draft,
1908
+ catalogRefreshHours: value
1909
+ });
1910
+ setStatus(IDLE_STATUS);
1911
+ }
1912
+ }),
1913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField$1, {
1914
+ prefix: "lpc",
1915
+ id: "lpc-catalogProxy",
1916
+ label: t("catalogProxy"),
1917
+ hint: t("catalogProxyHint"),
1918
+ value: draft.catalogProxy,
1919
+ disabled,
1920
+ onEdit: (value) => {
1921
+ setDraft({
1922
+ ...draft,
1923
+ catalogProxy: value
1924
+ });
1925
+ setStatus(IDLE_STATUS);
1926
+ }
1927
+ }),
1928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1929
+ className: "lpc-statusRow",
1930
+ children: [
1931
+ t("kitSource"),
1932
+ "",
1933
+ kitSource ?? ""
1934
+ ]
1935
+ }),
1936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1937
+ className: "lpc-statusRow",
1938
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1939
+ t("modelsDevStatus"),
1940
+ "",
1941
+ modelsDevText(modelsDevStatus, t)
1942
+ ] })
1943
+ }),
1944
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProvidersSection, {
1945
+ providers: draft.providers,
1946
+ epoch,
1947
+ disabled,
1948
+ t,
1949
+ onAddRoute,
1950
+ onRemoveRoute,
1951
+ onPatchProvider: setProvider
1952
+ })
1953
+ ]
1655
1954
  });
1656
1955
  }
1657
1956
  //#endregion
1658
1957
  //#region src/client/i18n.ts
1659
1958
  /**
1660
1959
  * 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用,与官方卡片同机制。
1960
+ * 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
1661
1961
  * @module llm-pi/client/i18n
1662
1962
  */
1663
1963
  const NS = "dsh-plus-llm-pi";
1664
- const zh = {
1964
+ const ownZh = {
1665
1965
  title: "LLM 路由(llm-pi)",
1666
1966
  description: "自定义 LLM 路由:协议、compat、模型目录与 models.dev 目录兜底。",
1667
1967
  enabled: "启用插件",
@@ -1738,19 +2038,10 @@ window.__ModuleLoader__.load({
1738
2038
  catalogProvider: "候选 provider",
1739
2039
  catalogLoading: "目录加载中…",
1740
2040
  catalogFailed: "目录加载失败。",
1741
- save: "保存",
1742
- saving: "保存中…",
1743
- discard: "放弃",
1744
- unsaved: "未保存",
1745
2041
  saveOk: "已保存。",
1746
- saveFailed: "保存失败:",
1747
- loading: "加载中…",
1748
- readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
1749
- expand: "展开",
1750
- collapse: "收起",
1751
- invalidNumber: "请输入有效数字"
2042
+ saveFailed: "保存失败:"
1752
2043
  };
1753
- const en = {
2044
+ const ownEn = {
1754
2045
  title: "LLM routes (llm-pi)",
1755
2046
  description: "Custom LLM routes: protocol, compat, model catalog and models.dev fallback.",
1756
2047
  enabled: "Enable plugin",
@@ -1827,67 +2118,29 @@ window.__ModuleLoader__.load({
1827
2118
  catalogProvider: "Candidate provider",
1828
2119
  catalogLoading: "Loading catalog…",
1829
2120
  catalogFailed: "Failed to load catalog.",
1830
- save: "Save",
1831
- saving: "Saving…",
1832
- discard: "Discard",
1833
- unsaved: "Unsaved",
1834
2121
  saveOk: "Saved.",
1835
- saveFailed: "Save failed: ",
1836
- loading: "Loading…",
1837
- readOnly: "No settings provider in this deployment; edit settings.yaml instead.",
1838
- expand: "Expand",
1839
- collapse: "Collapse",
1840
- invalidNumber: "Enter a valid number"
2122
+ saveFailed: "Save failed: "
1841
2123
  };
2124
+ const zh = mergeDict(commonZh, ownZh);
2125
+ const en = mergeDict(commonEn, ownEn);
1842
2126
  //#endregion
1843
2127
  //#region src/client/styles.ts
1844
2128
  /**
1845
- * 配置卡片样式:沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),
1846
- * 视觉对齐官方卡片(--dsw-alias-* 变量),不覆盖上游任何选择器。
2129
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client cardCss('lpc'),
2130
+ * 本文件补插件特有规则(route/model 折叠块、键值对行、网格、JSON 文本框)
2131
+ * 及窄屏响应式:views 的双列网格在窄屏转单列、route 头部按钮换行、
2132
+ * 键值对行纵向堆叠、文本框 16px 防 iOS 缩放。
2133
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
1847
2134
  * @module llm-pi/client/styles
1848
2135
  */
1849
2136
  const PLUGIN_ID = "@dsh-plus/llm-pi";
1850
- const STYLE_TAG_ID = `${PLUGIN_ID}/card.css`;
1851
- const cardCss = `
1852
- .lpc-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
1853
- .lpc-card:hover{border-color:var(--dsw-alias-label-dimmed)}
1854
- .lpc-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
1855
- .lpc-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
1856
- .lpc-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1857
- .lpc-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
1858
- .lpc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
1859
- .lpc-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
1860
- .lpc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;font-size:12px}
1861
- .lpc-chevronOpen{transform:rotate(180deg)}
1862
- .lpc-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
1863
- .lpc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
1864
- .lpc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex;min-width:0}
1865
- .lpc-field+.lpc-field{border-top:1px solid var(--dsw-alias-border-l2)}
1866
- .lpc-head{align-items:center;gap:8px;display:flex}
1867
- .lpc-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
1868
- .lpc-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;width:100%;box-sizing:border-box;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}
1869
- .lpc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
1870
- .lpc-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
1871
- .lpc-inputInvalid{border-color:var(--dsw-alias-label-error)}
2137
+ const cardCssAll = cardCss("lpc", `
2138
+ .lpc-input{width:100%;box-sizing:border-box}
1872
2139
  .lpc-select{appearance:none}
1873
2140
  .lpc-textarea{height:auto;min-height:72px;padding:8px 12px;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;resize:vertical}
1874
- .lpc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
1875
- .lpc-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
1876
- .lpc-checkRow{align-items:center;gap:8px;display:flex;padding:3px 0}
1877
- .lpc-checkRow input{accent-color:var(--dsw-alias-brand-primary)}
1878
- .lpc-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer}
1879
- .lpc-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:14px 0 2px;margin:0}
1880
- .lpc-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
2141
+ .lpc-checkRow{padding:3px 0}
1881
2142
  .lpc-statusRow{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;font-size:12px;line-height:1.6;word-break:break-all}
1882
- .lpc-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
1883
- .lpc-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
1884
- .lpc-statusError{color:var(--dsw-alias-label-error)}
1885
- .lpc-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;flex:none}
1886
- .lpc-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
1887
- .lpc-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
1888
- .lpc-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
1889
- .lpc-btn:disabled{opacity:.4;cursor:default}
1890
- .lpc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
2143
+ .lpc-btn{flex:none}
1891
2144
  .lpc-btnSmall{padding:2px 10px;font-size:12px}
1892
2145
  .lpc-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
1893
2146
  .lpc-gridNested{margin-top:2px}
@@ -1916,17 +2169,23 @@ window.__ModuleLoader__.load({
1916
2169
  .lpc-collapseTitle{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
1917
2170
  .lpc-collapseBody{border-top:1px dashed var(--dsw-alias-border-l2);padding-bottom:6px}
1918
2171
  .lpc-refreshBtn{margin-left:8px;vertical-align:middle}
1919
- `;
2172
+ @media (max-width:767px){
2173
+ .lpc-grid{grid-template-columns:1fr}
2174
+ .lpc-routeHead{flex-wrap:wrap}
2175
+ .lpc-routeKey{white-space:normal;word-break:break-all}
2176
+ .lpc-routeBody{margin:0 8px}
2177
+ .lpc-modelRow{padding:0 8px}
2178
+ .lpc-modelHead{flex-wrap:wrap}
2179
+ .lpc-kvRow{flex-direction:column;align-items:stretch}
2180
+ .lpc-kvRow .lpc-btn{align-self:flex-end}
2181
+ .lpc-textarea{font-size:16px}
2182
+ .lpc-statusRow{display:flex;flex-direction:column;gap:4px}
2183
+ .lpc-catalogBar .lpc-input{flex:1;min-width:140px}
2184
+ }
2185
+ `);
1920
2186
  /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
1921
2187
  function injectStyle() {
1922
- if (typeof document === "undefined") return null;
1923
- if (document.querySelector(`style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`) !== null) return null;
1924
- const tag = document.createElement("style");
1925
- tag.dataset.plugin = PLUGIN_ID;
1926
- tag.dataset.pluginCss = STYLE_TAG_ID;
1927
- tag.textContent = cardCss;
1928
- document.head.appendChild(tag);
1929
- return tag;
2188
+ return injectCardStyle(PLUGIN_ID, cardCssAll);
1930
2189
  }
1931
2190
  //#endregion
1932
2191
  //#region src/client/client.ts
@@ -1938,75 +2197,6 @@ window.__ModuleLoader__.load({
1938
2197
  "connection",
1939
2198
  "remote"
1940
2199
  ];
1941
- /**
1942
- * api 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
1943
- * 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
1944
- * 刷新:remote `settings/document-updated`(按命名空间过滤)与
1945
- * `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
1946
- * (loopback 或 tailnet 信任域名)下行为一致。
1947
- */
1948
- function createApiScope(api, ns, c) {
1949
- let state = {
1950
- status: "loading",
1951
- value: void 0,
1952
- revision: void 0,
1953
- writable: false
1954
- };
1955
- const listeners = /* @__PURE__ */ new Set();
1956
- let generation = 0;
1957
- const publish = (next) => {
1958
- state = next;
1959
- for (const listener of [...listeners]) listener();
1960
- };
1961
- const load = async () => {
1962
- const gen = ++generation;
1963
- let response;
1964
- try {
1965
- response = await api.describe({});
1966
- } catch {
1967
- return;
1968
- }
1969
- if (gen !== generation || !response.result.ok) return;
1970
- const writable = response.result.value?.writable ?? false;
1971
- const view = response.result.value?.namespaces.find((candidate) => candidate.ns === ns);
1972
- if (view === void 0) {
1973
- publish({
1974
- status: "unavailable",
1975
- value: void 0,
1976
- revision: void 0,
1977
- writable
1978
- });
1979
- return;
1980
- }
1981
- publish({
1982
- status: "ready",
1983
- value: view.value,
1984
- revision: view.revision,
1985
- writable
1986
- });
1987
- };
1988
- const refresh = (namespace) => {
1989
- if (namespace !== void 0 && namespace !== ns) return;
1990
- load();
1991
- };
1992
- const disposers = [c.get("remote").$on("settings/document-updated", refresh), c.on("connection/reset", () => {
1993
- load();
1994
- })];
1995
- c.effect(() => () => {
1996
- for (const dispose of disposers) dispose();
1997
- }, "llm-pi: settings scope");
1998
- load();
1999
- return {
2000
- getSnapshot: () => state,
2001
- subscribe: (listener) => {
2002
- listeners.add(listener);
2003
- return () => {
2004
- listeners.delete(listener);
2005
- };
2006
- },
2007
- load
2008
- };
2009
- }
2010
2200
  function apply(ctx) {
2011
2201
  const c = ctx;
2012
2202
  const tag = injectStyle();
@@ -2018,7 +2208,7 @@ window.__ModuleLoader__.load({
2018
2208
  en
2019
2209
  }), "llm-pi: locale");
2020
2210
  const api = c.get("connection").api.settings;
2021
- const scope = createApiScope(api, SETTINGS_NS, c);
2211
+ const scope = createApiScope(api, SETTINGS_NS, c, "llm-pi: settings scope");
2022
2212
  c.slots.inject("settings.plugin.item", () => c.slots.register({
2023
2213
  name: "settings.plugin.item",
2024
2214
  key: SETTINGS_NS,