@dsh-plus/image-studio 0.1.35 → 0.1.36
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 +1383 -394
- package/lib/index.d.ts +2 -2
- package/lib/index.js +13 -8
- package/package.json +2 -2
- package/src/client/card.tsx +21 -25
- package/src/client/i18n.ts +27 -4
- package/src/client/preset-sections.tsx +140 -174
- package/src/client/styles.ts +0 -24
- package/src/credentials.ts +9 -5
- package/src/uploads/store.ts +6 -3
package/lib/client.js
CHANGED
|
@@ -4,11 +4,36 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
8
29
|
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
32
|
+
let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
|
|
33
|
+
_deepseek_ai_schemastery = __toESM(_deepseek_ai_schemastery, 1);
|
|
9
34
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
35
|
//#region ../shared/src/client/fields.tsx
|
|
11
|
-
/** 卡片按钮(与
|
|
36
|
+
/** 卡片按钮(与 ConfigShell 页脚同一套类名与视觉)。 */
|
|
12
37
|
function Button(props) {
|
|
13
38
|
const p = props.prefix ?? "dshp";
|
|
14
39
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -54,13 +79,102 @@ window.__ModuleLoader__.load({
|
|
|
54
79
|
]
|
|
55
80
|
});
|
|
56
81
|
}
|
|
82
|
+
function SelectField(props) {
|
|
83
|
+
const p = props.prefix ?? "dshp";
|
|
84
|
+
const invalid = props.invalid === true;
|
|
85
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
86
|
+
className: `${p}-field`,
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
89
|
+
className: `${p}-head`,
|
|
90
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
91
|
+
className: `${p}-label`,
|
|
92
|
+
htmlFor: props.id,
|
|
93
|
+
children: props.label
|
|
94
|
+
})
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
97
|
+
id: props.id,
|
|
98
|
+
className: `${p}-select${invalid ? ` ${p}-inputInvalid` : ""}`,
|
|
99
|
+
"aria-invalid": invalid || void 0,
|
|
100
|
+
value: props.value,
|
|
101
|
+
disabled: props.disabled === true,
|
|
102
|
+
onChange: (event) => props.onEdit(event.target.value),
|
|
103
|
+
children: props.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
104
|
+
value: option.value,
|
|
105
|
+
children: option.label
|
|
106
|
+
}, option.value))
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
109
|
+
className: invalid ? `${p}-invalid` : `${p}-hint`,
|
|
110
|
+
children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
|
|
111
|
+
})
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 数值字段:文本承载(不静默改写用户输入),越界/非法由调用方判定并传 invalid;
|
|
117
|
+
* 空值与 `-`/`0` 等中间态不在这里过滤,避免编辑过程被打断。
|
|
118
|
+
*/
|
|
119
|
+
function NumberField(props) {
|
|
120
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
121
|
+
...props,
|
|
122
|
+
numeric: true
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function CodeField(props) {
|
|
126
|
+
const p = props.prefix ?? "dshp";
|
|
127
|
+
const invalid = props.invalid === true;
|
|
128
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
129
|
+
className: `${p}-field`,
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
132
|
+
className: `${p}-head`,
|
|
133
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
134
|
+
className: `${p}-label`,
|
|
135
|
+
htmlFor: props.id,
|
|
136
|
+
children: props.label
|
|
137
|
+
})
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
140
|
+
id: props.id,
|
|
141
|
+
className: `${p}-input ${p}-textarea${invalid ? ` ${p}-inputInvalid` : ""}`,
|
|
142
|
+
rows: props.rows ?? 6,
|
|
143
|
+
spellCheck: props.spellCheck ?? false,
|
|
144
|
+
"aria-invalid": invalid || void 0,
|
|
145
|
+
value: props.value,
|
|
146
|
+
disabled: props.disabled === true,
|
|
147
|
+
onChange: (event) => props.onEdit(event.target.value)
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
150
|
+
className: invalid ? `${p}-invalid` : `${p}-hint`,
|
|
151
|
+
children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
|
|
152
|
+
})
|
|
153
|
+
]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
57
156
|
//#endregion
|
|
58
157
|
//#region ../shared/src/client/card.tsx
|
|
158
|
+
/**
|
|
159
|
+
* 配置卡片外壳与排版件:`ConfigShell`(卡片级无边框分节)、`Section`(卡片内二级
|
|
160
|
+
* 分组)、`FieldGrid`(字段排布)、`Note`(只读说明)、`ListEditor`(数组型资源
|
|
161
|
+
* 统一编辑)、`CardLoading`(取值未到达占位)。
|
|
162
|
+
*
|
|
163
|
+
* 宿主页面(`plugins.row.config` / `plugins.bundle.config` 的 page 视图)自己画
|
|
164
|
+
* 标题、图标、面包屑与页面级内边距,视图只填内容区——所以这里不再自绘带边框
|
|
165
|
+
* 卡片(会形成卡片套卡片,把移动端可用宽度逐层扣掉),也不做折叠:page 视图
|
|
166
|
+
* 本身就是表单落地页。度量对齐官方设置页(dsh-client-ui-primitives 的
|
|
167
|
+
* settings-form):小节标题 13px/600、字段区零横向内边距、页脚非 sticky。
|
|
168
|
+
*
|
|
169
|
+
* 各插件 card.tsx 只填 children 字段区与 actions;类名前缀与样式由
|
|
170
|
+
* cardCss(prefix) 配套。
|
|
171
|
+
* @module @dsh-plus/shared/client/card
|
|
172
|
+
*/
|
|
59
173
|
const IDLE_STATUS = {
|
|
60
174
|
kind: "idle",
|
|
61
175
|
text: ""
|
|
62
176
|
};
|
|
63
|
-
function
|
|
177
|
+
function ConfigShell(props) {
|
|
64
178
|
const p = props.prefix;
|
|
65
179
|
const badge = props.statusBadge;
|
|
66
180
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
@@ -116,7 +230,160 @@ window.__ModuleLoader__.load({
|
|
|
116
230
|
]
|
|
117
231
|
});
|
|
118
232
|
}
|
|
119
|
-
|
|
233
|
+
function Section(props) {
|
|
234
|
+
const p = props.prefix;
|
|
235
|
+
const [open, setOpen] = (0, react.useState)(props.toggle?.defaultOpen ?? true);
|
|
236
|
+
const collapsible = props.toggle !== void 0;
|
|
237
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
238
|
+
className: `${p}-sub`,
|
|
239
|
+
children: [
|
|
240
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
241
|
+
className: `${p}-subHead`,
|
|
242
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
243
|
+
className: `${p}-subTitle`,
|
|
244
|
+
children: props.title
|
|
245
|
+
}), collapsible ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
246
|
+
type: "button",
|
|
247
|
+
className: `${p}-subToggle`,
|
|
248
|
+
onClick: () => setOpen(!open),
|
|
249
|
+
children: open ? props.toggle?.close : props.toggle?.open
|
|
250
|
+
}) : null]
|
|
251
|
+
}),
|
|
252
|
+
props.description === void 0 || props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
253
|
+
className: `${p}-subDesc`,
|
|
254
|
+
children: props.description
|
|
255
|
+
}),
|
|
256
|
+
open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
257
|
+
className: `${p}-subBody`,
|
|
258
|
+
children: props.children
|
|
259
|
+
}) : null
|
|
260
|
+
]
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** 字段排布:单列(默认)或宽屏两列(窄屏恒单列)。 */
|
|
264
|
+
function FieldGrid(props) {
|
|
265
|
+
const p = props.prefix;
|
|
266
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
267
|
+
className: `${p}-grid${props.columns === 2 ? ` ${p}-grid2` : ""}`,
|
|
268
|
+
children: props.children
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 数组型资源的统一编辑器:条目头(主标识 + 折叠/上移/下移/复制/删除)+
|
|
273
|
+
* 条目体;新增按钮固定在列表底部;空态统一文案。所有数组字段一律走它,
|
|
274
|
+
* 插件不再自绘「+ 添加一行」。
|
|
275
|
+
*/
|
|
276
|
+
function ListEditor(props) {
|
|
277
|
+
const p = props.prefix;
|
|
278
|
+
const disabled = props.disabled === true;
|
|
279
|
+
/** 条目数不超过该值时默认展开(默认 3)。 */
|
|
280
|
+
const collapseOver = props.collapseOver ?? 3;
|
|
281
|
+
const defaultOpen = props.items.length <= collapseOver;
|
|
282
|
+
const [collapsed, setCollapsed] = (0, react.useState)({});
|
|
283
|
+
const isOpen = (index) => collapsed[index] ?? defaultOpen;
|
|
284
|
+
const move = (from, to) => {
|
|
285
|
+
if (to < 0 || to >= props.items.length) return;
|
|
286
|
+
const next = [...props.items];
|
|
287
|
+
const [item] = next.splice(from, 1);
|
|
288
|
+
if (item === void 0) return;
|
|
289
|
+
next.splice(to, 0, item);
|
|
290
|
+
props.onChange(next);
|
|
291
|
+
setCollapsed({});
|
|
292
|
+
};
|
|
293
|
+
const remove = (index) => {
|
|
294
|
+
props.onChange(props.items.filter((_, at) => at !== index));
|
|
295
|
+
setCollapsed({});
|
|
296
|
+
};
|
|
297
|
+
const duplicate = (index) => {
|
|
298
|
+
const item = props.items[index];
|
|
299
|
+
if (item === void 0) return;
|
|
300
|
+
const next = [...props.items];
|
|
301
|
+
next.splice(index + 1, 0, structuredClone(item));
|
|
302
|
+
props.onChange(next);
|
|
303
|
+
setCollapsed({});
|
|
304
|
+
};
|
|
305
|
+
const toggle = (index) => {
|
|
306
|
+
setCollapsed((prev) => ({
|
|
307
|
+
...prev,
|
|
308
|
+
[index]: !(prev[index] ?? defaultOpen)
|
|
309
|
+
}));
|
|
310
|
+
};
|
|
311
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
312
|
+
className: `${p}-list`,
|
|
313
|
+
children: [
|
|
314
|
+
props.items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
315
|
+
className: `${p}-listEmpty`,
|
|
316
|
+
children: props.labels.empty
|
|
317
|
+
}) : props.items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
318
|
+
className: `${p}-listItem`,
|
|
319
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
320
|
+
className: `${p}-listItemHead`,
|
|
321
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
322
|
+
className: `${p}-listItemTitle`,
|
|
323
|
+
children: props.titleOf(item, index)
|
|
324
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
325
|
+
className: `${p}-listActions`,
|
|
326
|
+
children: [
|
|
327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
328
|
+
type: "button",
|
|
329
|
+
className: `${p}-iconBtn`,
|
|
330
|
+
onClick: () => toggle(index),
|
|
331
|
+
"aria-expanded": isOpen(index),
|
|
332
|
+
children: isOpen(index) ? props.labels.collapse : props.labels.expand
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
335
|
+
type: "button",
|
|
336
|
+
className: `${p}-iconBtn`,
|
|
337
|
+
disabled: disabled || index === 0,
|
|
338
|
+
onClick: () => move(index, index - 1),
|
|
339
|
+
children: props.labels.up
|
|
340
|
+
}),
|
|
341
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
342
|
+
type: "button",
|
|
343
|
+
className: `${p}-iconBtn`,
|
|
344
|
+
disabled: disabled || index === props.items.length - 1,
|
|
345
|
+
onClick: () => move(index, index + 1),
|
|
346
|
+
children: props.labels.down
|
|
347
|
+
}),
|
|
348
|
+
props.duplicate === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
349
|
+
type: "button",
|
|
350
|
+
className: `${p}-iconBtn`,
|
|
351
|
+
disabled,
|
|
352
|
+
onClick: () => duplicate(index),
|
|
353
|
+
children: props.labels.duplicate
|
|
354
|
+
}),
|
|
355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
356
|
+
type: "button",
|
|
357
|
+
className: `${p}-iconBtn`,
|
|
358
|
+
disabled,
|
|
359
|
+
onClick: () => remove(index),
|
|
360
|
+
children: props.labels.remove
|
|
361
|
+
})
|
|
362
|
+
]
|
|
363
|
+
})]
|
|
364
|
+
}), isOpen(index) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
365
|
+
className: `${p}-listItemBody`,
|
|
366
|
+
children: props.renderItem(item, index)
|
|
367
|
+
}) : null]
|
|
368
|
+
}, index)),
|
|
369
|
+
props.addControl === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
370
|
+
className: `${p}-listAddRow`,
|
|
371
|
+
children: props.addControl
|
|
372
|
+
}),
|
|
373
|
+
props.addControl ?? (props.create === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
374
|
+
type: "button",
|
|
375
|
+
className: `${p}-btn ${p}-btnGhost ${p}-listAdd`,
|
|
376
|
+
disabled,
|
|
377
|
+
onClick: () => {
|
|
378
|
+
props.onChange([...props.items, props.create()]);
|
|
379
|
+
setCollapsed({});
|
|
380
|
+
},
|
|
381
|
+
children: props.labels.add
|
|
382
|
+
}))
|
|
383
|
+
]
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** 取值未到达时的占位:与 ConfigShell 同形态,避免宿主页里闪一次卡片外壳。 */
|
|
120
387
|
function CardLoading(props) {
|
|
121
388
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
122
389
|
className: `${props.prefix}-section`,
|
|
@@ -128,14 +395,744 @@ window.__ModuleLoader__.load({
|
|
|
128
395
|
});
|
|
129
396
|
}
|
|
130
397
|
//#endregion
|
|
398
|
+
//#region ../shared/src/client/code-ops.ts
|
|
399
|
+
/** 是否为普通对象(配置文档的合法节点)。 */
|
|
400
|
+
function isPlainObject(value) {
|
|
401
|
+
if (value === null || typeof value !== "object") return false;
|
|
402
|
+
const proto = Object.getPrototypeOf(value);
|
|
403
|
+
return proto === Object.prototype || proto === null;
|
|
404
|
+
}
|
|
405
|
+
/** 从 V8 语法错误消息中取行列;新版带 `(line L column C)`,旧版只有偏移量。 */
|
|
406
|
+
function positionOf(message, text) {
|
|
407
|
+
const located = /\(line (\d+) column (\d+)\)/.exec(message);
|
|
408
|
+
if (located !== null) return {
|
|
409
|
+
line: Number(located[1]),
|
|
410
|
+
column: Number(located[2])
|
|
411
|
+
};
|
|
412
|
+
const offset = /at position (\d+)/.exec(message);
|
|
413
|
+
if (offset === null) return {
|
|
414
|
+
line: null,
|
|
415
|
+
column: null
|
|
416
|
+
};
|
|
417
|
+
const index = Math.min(Number(offset[1]), text.length);
|
|
418
|
+
const head = text.slice(0, index);
|
|
419
|
+
return {
|
|
420
|
+
line: head.split("\n").length,
|
|
421
|
+
column: index - head.lastIndexOf("\n")
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* 解析配置文档:必须是 JSON 对象(配置根节点为行级 Config 对象)。
|
|
426
|
+
* @param text - 编辑器全文。
|
|
427
|
+
* @returns 成功给出对象;失败给出原因与可定位的行列。
|
|
428
|
+
*/
|
|
429
|
+
function parseConfigJson(text) {
|
|
430
|
+
if (text.trim() === "") return {
|
|
431
|
+
ok: false,
|
|
432
|
+
reason: "empty",
|
|
433
|
+
message: "",
|
|
434
|
+
line: null,
|
|
435
|
+
column: null
|
|
436
|
+
};
|
|
437
|
+
let parsed;
|
|
438
|
+
try {
|
|
439
|
+
parsed = JSON.parse(text);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
reason: "syntax",
|
|
445
|
+
message,
|
|
446
|
+
...positionOf(message, text)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (!isPlainObject(parsed)) return {
|
|
450
|
+
ok: false,
|
|
451
|
+
reason: "root",
|
|
452
|
+
message: "",
|
|
453
|
+
line: 1,
|
|
454
|
+
column: 1
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
ok: true,
|
|
458
|
+
value: parsed
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/** 两空格缩进格式化(解析失败时原样返回输入与失败信息)。 */
|
|
462
|
+
function formatConfigJson(text) {
|
|
463
|
+
const parsed = parseConfigJson(text);
|
|
464
|
+
if (!parsed.ok) return {
|
|
465
|
+
ok: false,
|
|
466
|
+
result: parsed
|
|
467
|
+
};
|
|
468
|
+
return {
|
|
469
|
+
ok: true,
|
|
470
|
+
text: `${JSON.stringify(parsed.value, null, 2)}\n`
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
/** 差分:对象删键 → unset;数组删元素 → 下标降序 unset(保持其余下标有效)。 */
|
|
474
|
+
function diffToOps(before, after) {
|
|
475
|
+
const ops = [];
|
|
476
|
+
walk(before, after, [], ops);
|
|
477
|
+
return ops;
|
|
478
|
+
}
|
|
479
|
+
function walk(before, after, path, ops) {
|
|
480
|
+
if (Array.isArray(after)) {
|
|
481
|
+
walkArray(before, after, path, ops);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (isPlainObject(after)) {
|
|
485
|
+
walkObject(before, after, path, ops);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (!Object.is(before, after)) ops.push({
|
|
489
|
+
op: "set",
|
|
490
|
+
path: [...path],
|
|
491
|
+
value: after
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function walkObject(before, after, path, ops) {
|
|
495
|
+
if (!isPlainObject(before)) {
|
|
496
|
+
ops.push({
|
|
497
|
+
op: "set",
|
|
498
|
+
path: [...path],
|
|
499
|
+
value: after
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
for (const [key, value] of Object.entries(after)) {
|
|
504
|
+
if (!Object.hasOwn(before, key)) {
|
|
505
|
+
ops.push({
|
|
506
|
+
op: "set",
|
|
507
|
+
path: [...path, key],
|
|
508
|
+
value
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
walk(before[key], value, [...path, key], ops);
|
|
513
|
+
}
|
|
514
|
+
for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) ops.push({
|
|
515
|
+
op: "unset",
|
|
516
|
+
path: [...path, key]
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function walkArray(before, after, path, ops) {
|
|
520
|
+
if (!Array.isArray(before)) {
|
|
521
|
+
ops.push({
|
|
522
|
+
op: "set",
|
|
523
|
+
path: [...path],
|
|
524
|
+
value: after
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const shared = Math.min(before.length, after.length);
|
|
529
|
+
for (let index = 0; index < shared; index += 1) walk(before[index], after[index], [...path, String(index)], ops);
|
|
530
|
+
for (let index = before.length - 1; index >= after.length; index -= 1) ops.push({
|
|
531
|
+
op: "unset",
|
|
532
|
+
path: [...path, String(index)]
|
|
533
|
+
});
|
|
534
|
+
for (let index = before.length; index < after.length; index += 1) ops.push({
|
|
535
|
+
op: "set",
|
|
536
|
+
path: [...path, String(index)],
|
|
537
|
+
value: after[index]
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/** schema JSON 里的密钥字段路径(`meta.role === 'secret'`):源码编辑器不呈现它们。 */
|
|
541
|
+
function secretPathsOf(schemaJson) {
|
|
542
|
+
const refs = refsOf(schemaJson);
|
|
543
|
+
if (refs === null) return [];
|
|
544
|
+
const root = schemaJson.uid;
|
|
545
|
+
if (typeof root !== "number") return [];
|
|
546
|
+
const out = [];
|
|
547
|
+
collectSecretPaths(refs, root, [], out, /* @__PURE__ */ new Set());
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* 文档里 schema 未声明的键路径。
|
|
552
|
+
*
|
|
553
|
+
* schemastery 对未声明键**透传忽略**(不报错、不剥离),所以这类键会被原样写进
|
|
554
|
+
* 用户层并永久留存——典型来源是历史配置键与手写拼写错误。源码编辑器把它作为
|
|
555
|
+
* 非阻断警告列出来(迁移期新旧键并存时不至于挡住应用)。
|
|
556
|
+
*/
|
|
557
|
+
function unknownKeysOf(schemaJson, value) {
|
|
558
|
+
const refs = refsOf(schemaJson);
|
|
559
|
+
if (refs === null || !isPlainObject(value)) return [];
|
|
560
|
+
const root = schemaJson.uid;
|
|
561
|
+
if (typeof root !== "number") return [];
|
|
562
|
+
return [...unknownKeysAt(refs, root, value)].sort();
|
|
563
|
+
}
|
|
564
|
+
/** 返回「该节点下所有未被 schema 声明的键路径」的集合。 */
|
|
565
|
+
function unknownKeysAt(refs, index, value) {
|
|
566
|
+
const node = refs[String(index)];
|
|
567
|
+
if (node === void 0) return /* @__PURE__ */ new Set();
|
|
568
|
+
if (node.list !== void 0) return intersectBranches(refs, node.list, value);
|
|
569
|
+
if (node.inner !== void 0) {
|
|
570
|
+
if (Array.isArray(value)) {
|
|
571
|
+
const out = /* @__PURE__ */ new Set();
|
|
572
|
+
for (const [at, item] of value.entries()) for (const path of unknownKeysAt(refs, node.inner, item)) out.add(`${at}.${path}`);
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
return unknownKeysAt(refs, node.inner, value);
|
|
576
|
+
}
|
|
577
|
+
if (node.dict === void 0 || !isPlainObject(value)) return /* @__PURE__ */ new Set();
|
|
578
|
+
const out = /* @__PURE__ */ new Set();
|
|
579
|
+
for (const [key, child] of Object.entries(value)) {
|
|
580
|
+
const next = node.dict[key];
|
|
581
|
+
if (next === void 0) {
|
|
582
|
+
out.add(key);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
for (const path of unknownKeysAt(refs, next, child)) out.add(`${key}.${path}`);
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
/** 联合类型:只在「对象型分支」之间取交集(标量分支不否决键判定)。 */
|
|
590
|
+
function intersectBranches(refs, list, value) {
|
|
591
|
+
const objectBranches = list.filter((branch) => refs[String(branch)]?.dict !== void 0);
|
|
592
|
+
if (objectBranches.length === 0) return /* @__PURE__ */ new Set();
|
|
593
|
+
let result = null;
|
|
594
|
+
for (const branch of objectBranches) {
|
|
595
|
+
const keys = unknownKeysAt(refs, branch, value);
|
|
596
|
+
if (result === null) {
|
|
597
|
+
result = keys;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
for (const key of [...result]) if (!keys.has(key)) result.delete(key);
|
|
601
|
+
}
|
|
602
|
+
return result ?? /* @__PURE__ */ new Set();
|
|
603
|
+
}
|
|
604
|
+
function refsOf(schemaJson) {
|
|
605
|
+
if (!isPlainObject(schemaJson)) return null;
|
|
606
|
+
const refs = schemaJson.refs;
|
|
607
|
+
return isPlainObject(refs) ? refs : null;
|
|
608
|
+
}
|
|
609
|
+
function collectSecretPaths(refs, index, path, out, seen) {
|
|
610
|
+
const node = refs[String(index)];
|
|
611
|
+
if (node === void 0) return;
|
|
612
|
+
const marker = `${index}:${path.join(".")}`;
|
|
613
|
+
if (seen.has(marker)) return;
|
|
614
|
+
seen.add(marker);
|
|
615
|
+
if (node.meta?.role === "secret") {
|
|
616
|
+
out.push([...path]);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
for (const [key, child] of Object.entries(node.dict ?? {})) collectSecretPaths(refs, child, [...path, key], out, seen);
|
|
620
|
+
if (typeof node.inner === "number") collectSecretPaths(refs, node.inner, path, out, seen);
|
|
621
|
+
for (const child of node.list ?? []) collectSecretPaths(refs, child, path, out, seen);
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region ../shared/src/client/code-validate.ts
|
|
625
|
+
/**
|
|
626
|
+
* 配置源码编辑的模式校验:用宿主 `describe` 回传的 schema JSON 在浏览器本地
|
|
627
|
+
* 重建同一套 schemastery 校验(库 23KB 纯 JS,随 client bundle 打苞——浏览器端
|
|
628
|
+
* 没有宿主实例,不存在跨实例符号身份问题,见 docs/reference/官方机制参照.md
|
|
629
|
+
* 「客户端平台」)。
|
|
630
|
+
*
|
|
631
|
+
* 校验与宿主写入路径同源:`new Schema(json)` 后按同一份 volatile 投影调用,
|
|
632
|
+
* 因此本地拦下的错误与宿主 `settings/rejected` 的判据一致。
|
|
633
|
+
* @module @dsh-plus/shared/client/code-validate
|
|
634
|
+
*/
|
|
635
|
+
/** 重建结果按 schema JSON 对象身份缓存(describe 每次应答一个新对象,无泄漏风险)。 */
|
|
636
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
637
|
+
function rebuild(schemaJson) {
|
|
638
|
+
if (!isPlainObject(schemaJson)) return null;
|
|
639
|
+
const cached = cache.get(schemaJson);
|
|
640
|
+
if (cached !== void 0) return cached;
|
|
641
|
+
let built = null;
|
|
642
|
+
try {
|
|
643
|
+
built = new _deepseek_ai_schemastery.default(schemaJson);
|
|
644
|
+
} catch {
|
|
645
|
+
built = null;
|
|
646
|
+
}
|
|
647
|
+
cache.set(schemaJson, built);
|
|
648
|
+
return built;
|
|
649
|
+
}
|
|
650
|
+
/** 从 schemastery 错误消息里取路径前缀:`$.a.b expected number but got x` → `a.b`。 */
|
|
651
|
+
function pathOf(message) {
|
|
652
|
+
const located = /^\$\.([^\s]+)/.exec(message);
|
|
653
|
+
return located === null ? "" : located[1] ?? "";
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 按宿主 schema 校验一份配置文档。
|
|
657
|
+
* @param schemaJson - `describe` 应答里的 `schema`(schemastery `toJSON()` 形态)。
|
|
658
|
+
* @param value - 编辑器解析出的对象。
|
|
659
|
+
* @returns 通过(含「未校验」标记)或问题清单。
|
|
660
|
+
*/
|
|
661
|
+
function validateAgainstSchema(schemaJson, value) {
|
|
662
|
+
const validate = rebuild(schemaJson);
|
|
663
|
+
if (validate === null) return {
|
|
664
|
+
ok: true,
|
|
665
|
+
validated: false
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
validate(value);
|
|
669
|
+
return {
|
|
670
|
+
ok: true,
|
|
671
|
+
validated: true
|
|
672
|
+
};
|
|
673
|
+
} catch (error) {
|
|
674
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
validated: true,
|
|
678
|
+
issues: [{
|
|
679
|
+
path: pathOf(message),
|
|
680
|
+
message
|
|
681
|
+
}]
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region ../shared/src/client/styles.ts
|
|
687
|
+
/**
|
|
688
|
+
* 配置分节样式工厂(各插件 client 的公共收编版)。
|
|
689
|
+
*
|
|
690
|
+
* 目标宿主 dsh 0.2.1-alpha.2:插件页把配置视图放进已有页面级内边距的容器里
|
|
691
|
+
* (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
|
|
692
|
+
* 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
|
|
693
|
+
* 横向零内边距、页脚非 sticky),度量对齐官方设置页
|
|
694
|
+
* (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
|
|
695
|
+
*
|
|
696
|
+
* 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
697
|
+
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
698
|
+
* - 页脚按钮等宽换行、状态文字独占一行。
|
|
699
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
700
|
+
* @module @dsh-plus/shared/client/styles
|
|
701
|
+
*/
|
|
702
|
+
/** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
703
|
+
function cardCss(prefix, extra = "") {
|
|
704
|
+
return `
|
|
705
|
+
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
706
|
+
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
707
|
+
.${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}
|
|
708
|
+
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
709
|
+
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
710
|
+
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
711
|
+
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
712
|
+
.${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}
|
|
713
|
+
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
714
|
+
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
715
|
+
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
716
|
+
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
717
|
+
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
718
|
+
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
719
|
+
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
720
|
+
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
721
|
+
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
722
|
+
.${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;transition:border-color .18s,background .18s}
|
|
723
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
724
|
+
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
725
|
+
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
726
|
+
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
727
|
+
.${prefix}-textarea{box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;height:auto;line-height:1.6;min-height:120px;padding:8px 12px;resize:vertical;white-space:pre;width:100%}
|
|
728
|
+
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
729
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
730
|
+
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
731
|
+
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
732
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
733
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
734
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
735
|
+
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
736
|
+
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
737
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
738
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
739
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
740
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
741
|
+
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
742
|
+
/* 分节(Section):卡片内的二级分组,与一级分节同度量、无边框 */
|
|
743
|
+
.${prefix}-sub{flex-direction:column;padding:12px 0 0;display:flex}
|
|
744
|
+
.${prefix}-subHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
745
|
+
.${prefix}-subTitle{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;font-weight:600;line-height:1.5}
|
|
746
|
+
.${prefix}-subDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
747
|
+
.${prefix}-subBody{flex-direction:column;min-width:0;display:flex}
|
|
748
|
+
.${prefix}-subToggle{appearance:none;background:0 0;border:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;font-size:12px;padding:2px 6px;border-radius:6px}
|
|
749
|
+
.${prefix}-subToggle:hover{color:var(--dsw-alias-label-primary)}
|
|
750
|
+
/* 字段排布(FieldGrid):窄屏单列,宽屏可两列 */
|
|
751
|
+
.${prefix}-grid{display:grid;grid-template-columns:1fr;min-width:0}
|
|
752
|
+
@media (min-width:768px){
|
|
753
|
+
.${prefix}-grid2{grid-template-columns:1fr 1fr;column-gap:16px}
|
|
754
|
+
}
|
|
755
|
+
/* 只读说明块(Note) */
|
|
756
|
+
.${prefix}-note{background:var(--dsw-alias-bg-module-platform);border-radius:8px;margin:12px 0 0;padding:10px 12px}
|
|
757
|
+
.${prefix}-noteTitle{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;font-weight:600;line-height:1.5}
|
|
758
|
+
.${prefix}-noteBody{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5;white-space:pre-wrap}
|
|
759
|
+
/* 列表编辑器(ListEditor):条目头 + 折叠 + 行内操作 */
|
|
760
|
+
.${prefix}-list{flex-direction:column;gap:8px;padding:8px 0 0;display:flex}
|
|
761
|
+
.${prefix}-listItem{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:8px 12px}
|
|
762
|
+
.${prefix}-listItemHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
763
|
+
.${prefix}-listItemTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
764
|
+
.${prefix}-listItemBody{flex-direction:column;min-width:0;display:flex;padding-top:4px}
|
|
765
|
+
.${prefix}-listActions{align-items:center;gap:4px;display:flex;flex-wrap:wrap}
|
|
766
|
+
.${prefix}-iconBtn{appearance:none;background:0 0;border:1px solid #0000;border-radius:6px;color:var(--dsw-alias-label-tertiary);cursor:pointer;font:inherit;font-size:12px;line-height:1.5;padding:2px 8px}
|
|
767
|
+
.${prefix}-iconBtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
|
|
768
|
+
.${prefix}-iconBtn:disabled{opacity:.4;cursor:default}
|
|
769
|
+
.${prefix}-listAdd{align-self:flex-start}
|
|
770
|
+
.${prefix}-listAddRow{align-items:center;align-self:flex-start;gap:8px;display:flex;flex-wrap:wrap}
|
|
771
|
+
.${prefix}-listEmpty{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5;padding:4px 0}
|
|
772
|
+
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
773
|
+
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
774
|
+
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
775
|
+
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
776
|
+
.${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}
|
|
777
|
+
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
778
|
+
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
779
|
+
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
780
|
+
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
781
|
+
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
782
|
+
@media (max-width:767px){
|
|
783
|
+
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
784
|
+
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
785
|
+
.${prefix}-status{flex-basis:100%;flex:none}
|
|
786
|
+
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
787
|
+
}
|
|
788
|
+
@media (pointer:coarse){
|
|
789
|
+
.${prefix}-btn{min-height:44px}
|
|
790
|
+
.${prefix}-checkRow{min-height:44px}
|
|
791
|
+
.${prefix}-input,.${prefix}-select{height:40px}
|
|
792
|
+
}
|
|
793
|
+
${extra}
|
|
794
|
+
`;
|
|
795
|
+
}
|
|
796
|
+
/** 源码编辑节的固定类名前缀。 */
|
|
797
|
+
const CODE_SECTION_PREFIX = "dshc";
|
|
798
|
+
/**
|
|
799
|
+
* 源码编辑节(ConfigCodeSection)专用样式:前缀固定 `dshc`,由 shared 自行注入
|
|
800
|
+
* (插件卡片不必知道它的存在)。基础规则复用 cardCss,只追加 details/摘要与
|
|
801
|
+
* 代码文本域的度量。
|
|
802
|
+
*/
|
|
803
|
+
function codeSectionCss() {
|
|
804
|
+
return cardCss(CODE_SECTION_PREFIX, `
|
|
805
|
+
.${CODE_SECTION_PREFIX}-details{padding:4px 0 0}
|
|
806
|
+
.${CODE_SECTION_PREFIX}-summary{color:var(--dsw-alias-label-primary);cursor:pointer;font-size:13px;font-weight:600;line-height:1.5;list-style:none;padding:8px 0}
|
|
807
|
+
.${CODE_SECTION_PREFIX}-summary::-webkit-details-marker{display:none}
|
|
808
|
+
.${CODE_SECTION_PREFIX}-summary::before{content:"▸";color:var(--dsw-alias-label-tertiary);display:inline-block;margin-right:6px;transition:transform .18s}
|
|
809
|
+
.${CODE_SECTION_PREFIX}-details[open] .${CODE_SECTION_PREFIX}-summary::before{transform:rotate(90deg)}
|
|
810
|
+
.${CODE_SECTION_PREFIX}-desc{color:var(--dsw-alias-label-tertiary);margin:0 0 8px;font-size:12px;line-height:1.5}
|
|
811
|
+
.${CODE_SECTION_PREFIX}-input{box-sizing:border-box;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;height:auto;line-height:1.6;min-height:240px;padding:10px 12px;resize:vertical;tab-size:2;white-space:pre;width:100%}
|
|
812
|
+
.${CODE_SECTION_PREFIX}-foot{align-items:center;gap:8px;display:flex;flex-wrap:wrap;padding-top:12px}
|
|
813
|
+
.${CODE_SECTION_PREFIX}-foot .${CODE_SECTION_PREFIX}-btn:last-child{margin-left:auto}
|
|
814
|
+
@media (max-width:767px){
|
|
815
|
+
.${CODE_SECTION_PREFIX}-input{font-size:16px}
|
|
816
|
+
}
|
|
817
|
+
`);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
821
|
+
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
822
|
+
*/
|
|
823
|
+
function injectCardStyle(pluginId, css) {
|
|
824
|
+
if (typeof document === "undefined") return null;
|
|
825
|
+
const tagId = `${pluginId}/card.css`;
|
|
826
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
827
|
+
const tag = document.createElement("style");
|
|
828
|
+
tag.dataset.plugin = pluginId;
|
|
829
|
+
tag.dataset.pluginCss = tagId;
|
|
830
|
+
tag.textContent = css;
|
|
831
|
+
document.head.appendChild(tag);
|
|
832
|
+
return tag;
|
|
833
|
+
}
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region ../shared/src/client/code-section.ts
|
|
836
|
+
/**
|
|
837
|
+
* 配置源码编辑节(自动追加到每张插件配置卡片页)。
|
|
838
|
+
*
|
|
839
|
+
* 形态:无边框分节,与 ConfigShell/Section 的小节度量一致;文档=宿主 `describe` 回传的
|
|
840
|
+
* volatile 投影(JSON,两空格缩进),校验=本地 JSON 语法 + schemastery 模式,
|
|
841
|
+
* 写回=官方 `settings.mutate` 路径 ops(密钥字段不进文档,也不会被写坏)。
|
|
842
|
+
*
|
|
843
|
+
* 本文件刻意不写 JSX:`@dsh-plus/shared` 的 client 纯逻辑测试用 `node --test`
|
|
844
|
+
* 直跑 .ts(Node 原生类型擦除不支持 JSX),而 `config-slots.ts` 会被测试导入,
|
|
845
|
+
* 因此这条导入链上的模块一律用 `createElement`。React 只经默认导入取用
|
|
846
|
+
* (react 是 CJS,Node ESM 下具名导入不保证可解析)。
|
|
847
|
+
* @module @dsh-plus/shared/client/code-section
|
|
848
|
+
*/
|
|
849
|
+
const { createElement: h$1, useEffect: useEffect$6, useMemo: useMemo$4, useState: useState$9, useSyncExternalStore: useSyncExternalStore$4 } = react.default;
|
|
850
|
+
const IDLE$1 = {
|
|
851
|
+
kind: "idle",
|
|
852
|
+
text: ""
|
|
853
|
+
};
|
|
854
|
+
/** 文档播种文本:对象 → 两空格缩进 JSON。 */
|
|
855
|
+
function seedText(value) {
|
|
856
|
+
return value === void 0 ? "{}\n" : `${JSON.stringify(value, null, 2)}\n`;
|
|
857
|
+
}
|
|
858
|
+
/** 解析/校验失败的原因文案(逐条给出行列或字段路径)。 */
|
|
859
|
+
function problemLines(text, snapshot, t) {
|
|
860
|
+
const parsed = parseConfigJson(text);
|
|
861
|
+
if (!parsed.ok) {
|
|
862
|
+
if (parsed.reason === "empty") return {
|
|
863
|
+
problems: [t("codeInvalidEmpty")],
|
|
864
|
+
validated: false
|
|
865
|
+
};
|
|
866
|
+
if (parsed.reason === "root") return {
|
|
867
|
+
problems: [t("codeInvalidRoot")],
|
|
868
|
+
validated: false
|
|
869
|
+
};
|
|
870
|
+
const line = t("codeLine").replace("{line}", String(parsed.line ?? 0)).replace("{column}", String(parsed.column ?? 0));
|
|
871
|
+
return {
|
|
872
|
+
problems: [`${t("codeInvalidJson")}(${line})${parsed.message}`],
|
|
873
|
+
validated: false
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
const result = validateAgainstSchema(snapshot.schema, parsed.value);
|
|
877
|
+
if (result.ok) return {
|
|
878
|
+
problems: [],
|
|
879
|
+
validated: result.validated
|
|
880
|
+
};
|
|
881
|
+
return {
|
|
882
|
+
problems: result.issues.map((issue) => `${t("codeInvalidSchema")}:${issue.path === "" ? issue.message : `${issue.path} — ${issue.message}`}`),
|
|
883
|
+
validated: true
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
/** 与当前快照的差分(无改动时为空数组)。 */
|
|
887
|
+
function opsOf(text, snapshot) {
|
|
888
|
+
const parsed = parseConfigJson(text);
|
|
889
|
+
return parsed.ok ? diffToOps(snapshot.value, parsed.value) : [];
|
|
890
|
+
}
|
|
891
|
+
/** 应用:官方路径 ops 写回,成功后重载快照(revision 变化会重新播种文档)。 */
|
|
892
|
+
function applyOps(options) {
|
|
893
|
+
const { api, scope, ops, revision, t, setStatus, setApplying } = options;
|
|
894
|
+
setApplying(true);
|
|
895
|
+
api.mutate(ops, revision).then(() => {
|
|
896
|
+
setStatus({
|
|
897
|
+
kind: "ok",
|
|
898
|
+
text: t("codeApplied")
|
|
899
|
+
});
|
|
900
|
+
return scope.load();
|
|
901
|
+
}).catch((error) => {
|
|
902
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
903
|
+
setStatus({
|
|
904
|
+
kind: "error",
|
|
905
|
+
text: `${t("codeApplyFailed")}${message}`
|
|
906
|
+
});
|
|
907
|
+
}).finally(() => setApplying(false));
|
|
908
|
+
}
|
|
909
|
+
/** 草稿动作:应用(官方路径 ops 写回)、格式化、编辑与 Tab 缩进。 */
|
|
910
|
+
function draftHandlers(options) {
|
|
911
|
+
const { api, scope, t, snapshot, text, ops, setText, setStatus, setApplying } = options;
|
|
912
|
+
return {
|
|
913
|
+
onApply: () => {
|
|
914
|
+
applyOps({
|
|
915
|
+
api,
|
|
916
|
+
scope,
|
|
917
|
+
ops,
|
|
918
|
+
revision: snapshot.revision,
|
|
919
|
+
t,
|
|
920
|
+
setStatus,
|
|
921
|
+
setApplying
|
|
922
|
+
});
|
|
923
|
+
},
|
|
924
|
+
onFormat: () => {
|
|
925
|
+
const formatted = formatConfigJson(text);
|
|
926
|
+
if (formatted.ok) setText(formatted.text);
|
|
927
|
+
},
|
|
928
|
+
onChange: (event) => {
|
|
929
|
+
setText(event.target.value);
|
|
930
|
+
setStatus(IDLE$1);
|
|
931
|
+
},
|
|
932
|
+
onKeyDown: (event) => {
|
|
933
|
+
if (event.key !== "Tab") return;
|
|
934
|
+
event.preventDefault();
|
|
935
|
+
const target = event.currentTarget;
|
|
936
|
+
const { selectionStart, selectionEnd, value } = target;
|
|
937
|
+
setText(`${value.slice(0, selectionStart)} ${value.slice(selectionEnd)}`);
|
|
938
|
+
queueMicrotask(() => {
|
|
939
|
+
target.selectionStart = selectionStart + 2;
|
|
940
|
+
target.selectionEnd = selectionStart + 2;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
/** 校验派生量:逐次编辑重算(problems 供提示与禁用判定,validated 区分「模式外」)。 */
|
|
946
|
+
function useCodeChecks(text, snapshot, t) {
|
|
947
|
+
const { problems, validated } = useMemo$4(() => problemLines(text, snapshot, t), [
|
|
948
|
+
text,
|
|
949
|
+
snapshot,
|
|
950
|
+
t
|
|
951
|
+
]);
|
|
952
|
+
return {
|
|
953
|
+
problems,
|
|
954
|
+
validated,
|
|
955
|
+
ops: useMemo$4(() => opsOf(text, snapshot), [text, snapshot]),
|
|
956
|
+
secrets: useMemo$4(() => secretPathsOf(snapshot.schema), [snapshot.schema]),
|
|
957
|
+
unknown: useMemo$4(() => {
|
|
958
|
+
const parsed = parseConfigJson(text);
|
|
959
|
+
return parsed.ok ? unknownKeysOf(snapshot.schema, parsed.value) : [];
|
|
960
|
+
}, [text, snapshot.schema])
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
/** 分节状态:快照订阅、文档文本、校验结果与草稿动作(主组件只做组装)。 */
|
|
964
|
+
function useCodeSection(props) {
|
|
965
|
+
const { ns, t, scope, api } = props;
|
|
966
|
+
const snapshot = useSyncExternalStore$4((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
967
|
+
const [text, setText] = useState$9(() => seedText(snapshot.value));
|
|
968
|
+
const [status, setStatus] = useState$9(IDLE$1);
|
|
969
|
+
const [applying, setApplying] = useState$9(false);
|
|
970
|
+
const [open, setOpen] = useState$9(false);
|
|
971
|
+
const revision = snapshot.revision;
|
|
972
|
+
const snapshotStatus = snapshot.status;
|
|
973
|
+
useEffect$6(() => {
|
|
974
|
+
setText(seedText(snapshot.value));
|
|
975
|
+
setStatus(IDLE$1);
|
|
976
|
+
}, [revision, snapshotStatus]);
|
|
977
|
+
useEffect$6(() => {
|
|
978
|
+
injectCardStyle("@dsh-plus/shared", codeSectionCss());
|
|
979
|
+
}, []);
|
|
980
|
+
const { problems, validated, ops, secrets, unknown } = useCodeChecks(text, snapshot, t);
|
|
981
|
+
const ready = snapshot.status === "ready";
|
|
982
|
+
const disabled = !ready || !snapshot.writable || applying;
|
|
983
|
+
return {
|
|
984
|
+
ready,
|
|
985
|
+
ns,
|
|
986
|
+
t,
|
|
987
|
+
scope,
|
|
988
|
+
snapshot,
|
|
989
|
+
text,
|
|
990
|
+
setText,
|
|
991
|
+
status,
|
|
992
|
+
setStatus,
|
|
993
|
+
applying,
|
|
994
|
+
setApplying,
|
|
995
|
+
open,
|
|
996
|
+
setOpen,
|
|
997
|
+
problems,
|
|
998
|
+
validated,
|
|
999
|
+
ops,
|
|
1000
|
+
secrets,
|
|
1001
|
+
unknown,
|
|
1002
|
+
disabled,
|
|
1003
|
+
canApply: !disabled && problems.length === 0 && ops.length > 0,
|
|
1004
|
+
...draftHandlers({
|
|
1005
|
+
api,
|
|
1006
|
+
scope,
|
|
1007
|
+
t,
|
|
1008
|
+
snapshot,
|
|
1009
|
+
text,
|
|
1010
|
+
ops,
|
|
1011
|
+
setText,
|
|
1012
|
+
setStatus,
|
|
1013
|
+
setApplying
|
|
1014
|
+
})
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/** 提示行:无改动 / 未校验 / 密钥计数 / 模式外键 / 不可用或只读。 */
|
|
1018
|
+
function noteLines(state) {
|
|
1019
|
+
const { t, ready, problems, validated, ops, secrets, unknown, snapshot } = state;
|
|
1020
|
+
const notes = [];
|
|
1021
|
+
if (problems.length === 0 && ops.length === 0 && ready) notes.push(h$1("p", {
|
|
1022
|
+
className: "dshc-hint",
|
|
1023
|
+
key: "no-change"
|
|
1024
|
+
}, t("codeNoChanges")));
|
|
1025
|
+
if (problems.length === 0 && !validated) notes.push(h$1("p", {
|
|
1026
|
+
className: "dshc-hint",
|
|
1027
|
+
key: "unvalidated"
|
|
1028
|
+
}, t("codeUnvalidated")));
|
|
1029
|
+
if (secrets.length > 0) notes.push(h$1("p", {
|
|
1030
|
+
className: "dshc-hint",
|
|
1031
|
+
key: "secrets"
|
|
1032
|
+
}, t("codeSecretsNote").replace("{n}", String(secrets.length))));
|
|
1033
|
+
if (unknown.length > 0) notes.push(h$1("p", {
|
|
1034
|
+
className: "dshc-hint",
|
|
1035
|
+
key: "unknown"
|
|
1036
|
+
}, t("codeUnknownKeys").replace("{n}", String(unknown.length)).replace("{keys}", unknown.slice(0, 8).join("、"))));
|
|
1037
|
+
if (snapshot.status === "unavailable") notes.push(h$1("p", {
|
|
1038
|
+
className: "dshc-hint",
|
|
1039
|
+
key: "unavailable"
|
|
1040
|
+
}, t("codeUnavailable")));
|
|
1041
|
+
else if (!snapshot.writable) notes.push(h$1("p", {
|
|
1042
|
+
className: "dshc-hint",
|
|
1043
|
+
key: "readonly"
|
|
1044
|
+
}, t("codeReadOnly")));
|
|
1045
|
+
return notes;
|
|
1046
|
+
}
|
|
1047
|
+
/** 页脚:状态行 + 格式化 / 应用按钮。 */
|
|
1048
|
+
function codeFooter(state) {
|
|
1049
|
+
const { t, status, disabled, canApply, applying, onFormat, onApply } = state;
|
|
1050
|
+
return h$1("div", { className: "dshc-foot" }, status.kind === "idle" ? null : h$1("p", {
|
|
1051
|
+
className: `dshc-status${status.kind === "error" ? " dshc-statusError" : ""}`,
|
|
1052
|
+
role: "status"
|
|
1053
|
+
}, status.text), h$1("button", {
|
|
1054
|
+
type: "button",
|
|
1055
|
+
className: "dshc-btn dshc-btnGhost",
|
|
1056
|
+
disabled,
|
|
1057
|
+
onClick: onFormat
|
|
1058
|
+
}, t("codeFormat")), h$1("button", {
|
|
1059
|
+
type: "button",
|
|
1060
|
+
className: "dshc-btn dshc-btnPrimary",
|
|
1061
|
+
disabled: !canApply,
|
|
1062
|
+
onClick: onApply
|
|
1063
|
+
}, applying ? t("codeApplying") : t("codeApply")));
|
|
1064
|
+
}
|
|
1065
|
+
function ConfigCodeSection(props) {
|
|
1066
|
+
const state = useCodeSection(props);
|
|
1067
|
+
const { ns, t, text, disabled, open, setOpen, status, problems, onChange, onKeyDown } = state;
|
|
1068
|
+
return h$1("section", {
|
|
1069
|
+
className: "dshc-section",
|
|
1070
|
+
"aria-label": `${ns} ${t("codeSectionTitle")}`
|
|
1071
|
+
}, h$1("details", {
|
|
1072
|
+
className: "dshc-details",
|
|
1073
|
+
open: open || status.kind === "error",
|
|
1074
|
+
onToggle: (event) => {
|
|
1075
|
+
setOpen(event.currentTarget.open);
|
|
1076
|
+
}
|
|
1077
|
+
}, h$1("summary", { className: "dshc-summary" }, t("codeSectionTitle")), h$1("p", { className: "dshc-desc" }, t("codeSectionDesc")), h$1("textarea", {
|
|
1078
|
+
className: `dshc-input${problems.length > 0 ? " dshc-inputInvalid" : ""}`,
|
|
1079
|
+
rows: 16,
|
|
1080
|
+
spellCheck: false,
|
|
1081
|
+
"aria-invalid": problems.length > 0 ? true : void 0,
|
|
1082
|
+
value: text,
|
|
1083
|
+
disabled,
|
|
1084
|
+
onChange,
|
|
1085
|
+
onKeyDown
|
|
1086
|
+
}), problems.map((problem) => h$1("p", {
|
|
1087
|
+
className: "dshc-invalid",
|
|
1088
|
+
role: "alert",
|
|
1089
|
+
key: problem
|
|
1090
|
+
}, problem)), ...noteLines(state), codeFooter(state)));
|
|
1091
|
+
}
|
|
1092
|
+
//#endregion
|
|
131
1093
|
//#region ../shared/src/client/config-slots.ts
|
|
1094
|
+
/**
|
|
1095
|
+
* 配置卡片的槽位注册助手(目标宿主:dsh 0.2.1-alpha.2)。
|
|
1096
|
+
*
|
|
1097
|
+
* 官方插件页(`dsh-client-ui-plugin-manager`)为第三方配置视图声明两个 keyed 槽位:
|
|
1098
|
+
* - `plugins.row.config`(key = `<bundle 包名>#<row id>`):bundle 详情页对应行出现
|
|
1099
|
+
* 「配置」入口,行配置页渲染 `view: 'summary'`(一行简介)与 `view: 'page'`
|
|
1100
|
+
* (带保存控件的表单);
|
|
1101
|
+
* - `plugins.bundle.config`(key = 包名):包详情页本体配置,仅渲染 `view: 'page'`。
|
|
1102
|
+
*
|
|
1103
|
+
* 页面自己画标题、图标、面包屑与页面级内边距(`detailSections` 内无边框容器),
|
|
1104
|
+
* 故 page 视图输出无边框分节(见 card.tsx),summary 视图只返回一行文本。
|
|
1105
|
+
* 旧 `settings.plugin.item` 槽位已随官方插件页迁移删除,不再注册。
|
|
1106
|
+
*
|
|
1107
|
+
* **源码编辑节自动追加**:注册时把插件组件包一层,page 视图在原卡片之后追加
|
|
1108
|
+
* `ConfigCodeSection`(复用同一份 scope/api),summary 视图原样透传。因此「只要
|
|
1109
|
+
* 插件存在可配置项」就有源码编辑入口,插件侧零接线;命名空间没有 volatile 字段时
|
|
1110
|
+
* 它本身不出现在 describe 视图里,该节自然显示为不可用。
|
|
1111
|
+
*
|
|
1112
|
+
* @module @dsh-plus/shared/client/config-slots
|
|
1113
|
+
*/
|
|
1114
|
+
const { createElement: h, Fragment } = react.default;
|
|
132
1115
|
/** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
|
|
133
1116
|
const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
|
|
1117
|
+
/** page 视图:插件卡片 + 自动追加的源码编辑节(summary 视图原样透传)。 */
|
|
1118
|
+
function withCodeSection(component, ns) {
|
|
1119
|
+
return function AutoConfigView(props) {
|
|
1120
|
+
const card = h(component, props);
|
|
1121
|
+
if (props.view !== "page") return card;
|
|
1122
|
+
return h(Fragment, null, card, h(ConfigCodeSection, {
|
|
1123
|
+
ns,
|
|
1124
|
+
t: props.t,
|
|
1125
|
+
scope: props.scope,
|
|
1126
|
+
api: props.api
|
|
1127
|
+
}));
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
134
1130
|
/**
|
|
135
1131
|
* 把一张配置卡片注册进插件页的两个配置槽位。
|
|
136
1132
|
* 两个槽位都由 0.2.1-alpha.2 的插件页声明,任一缺席即报错——不做版本探测。
|
|
137
1133
|
*/
|
|
138
1134
|
function injectPluginConfigCard(slots, reg) {
|
|
1135
|
+
const component = withCodeSection(reg.component, reg.ns);
|
|
139
1136
|
const base = {
|
|
140
1137
|
locale: reg.ns,
|
|
141
1138
|
inject: reg.inject
|
|
@@ -144,12 +1141,12 @@ window.__ModuleLoader__.load({
|
|
|
144
1141
|
name: "plugins.row.config",
|
|
145
1142
|
key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
|
|
146
1143
|
...base
|
|
147
|
-
},
|
|
1144
|
+
}, component));
|
|
148
1145
|
slots.inject("plugins.bundle.config", () => slots.register({
|
|
149
1146
|
name: "plugins.bundle.config",
|
|
150
1147
|
key: reg.pkg,
|
|
151
1148
|
...base
|
|
152
|
-
},
|
|
1149
|
+
}, component));
|
|
153
1150
|
}
|
|
154
1151
|
//#endregion
|
|
155
1152
|
//#region ../shared/src/client/credentials.tsx
|
|
@@ -196,6 +1193,101 @@ window.__ModuleLoader__.load({
|
|
|
196
1193
|
}), guard);
|
|
197
1194
|
}
|
|
198
1195
|
//#endregion
|
|
1196
|
+
//#region ../shared/src/client/i18n.ts
|
|
1197
|
+
/** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
|
|
1198
|
+
const commonZh = {
|
|
1199
|
+
save: "保存",
|
|
1200
|
+
saving: "保存中…",
|
|
1201
|
+
discard: "放弃",
|
|
1202
|
+
unsaved: "未保存",
|
|
1203
|
+
loading: "加载中…",
|
|
1204
|
+
unavailable: "配置服务不可用。",
|
|
1205
|
+
readOnly: "当前部署无 settings provider,配置为只读;请在 profile 的 cordis.patch.yml 行级配置。",
|
|
1206
|
+
invalidNumber: "请输入正整数。",
|
|
1207
|
+
collapse: "收起",
|
|
1208
|
+
expand: "展开",
|
|
1209
|
+
enabledOn: "已启用",
|
|
1210
|
+
enabledOff: "未启用",
|
|
1211
|
+
saveFailed: "保存失败,请检查填写内容。",
|
|
1212
|
+
confirm: "确认",
|
|
1213
|
+
cancel: "取消",
|
|
1214
|
+
listAdd: "新增",
|
|
1215
|
+
listRemove: "删除",
|
|
1216
|
+
listMoveUp: "上移",
|
|
1217
|
+
listMoveDown: "下移",
|
|
1218
|
+
listDuplicate: "复制",
|
|
1219
|
+
listEmpty: "暂无条目。",
|
|
1220
|
+
codeSectionTitle: "源码编辑(JSON)",
|
|
1221
|
+
codeSectionDesc: "直接编辑本插件的可配置字段(仅 .volatile() 字段)。「应用」按官方路径编辑写回用户层,密钥字段不在此编辑器内。",
|
|
1222
|
+
codeFormat: "格式化",
|
|
1223
|
+
codeApply: "应用",
|
|
1224
|
+
codeApplying: "应用中…",
|
|
1225
|
+
codeApplied: "已应用。",
|
|
1226
|
+
codeNoChanges: "与当前配置一致,无需应用。",
|
|
1227
|
+
codeInvalidJson: "JSON 语法错误",
|
|
1228
|
+
codeInvalidEmpty: "配置不能为空。",
|
|
1229
|
+
codeInvalidRoot: "根节点必须是对象。",
|
|
1230
|
+
codeInvalidSchema: "模式校验未通过",
|
|
1231
|
+
codeUnvalidated: "宿主未回传 schema,已跳过本地校验(写入仍由宿主把关)。",
|
|
1232
|
+
codeSecretsNote: "有 {n} 个密钥字段不在此编辑器内(在凭据区或行级配置中管理)。",
|
|
1233
|
+
codeUnknownKeys: "有 {n} 个键不在 schema 里({keys}):会被原样保留并被运行时忽略,建议删除。",
|
|
1234
|
+
codeUnavailable: "该行未激活或命名空间不可用,源码编辑不可用。",
|
|
1235
|
+
codeReadOnly: "当前部署无 settings provider,源码编辑只读。",
|
|
1236
|
+
codeApplyFailed: "应用失败:",
|
|
1237
|
+
codeLine: "第 {line} 行第 {column} 列"
|
|
1238
|
+
};
|
|
1239
|
+
const commonEn = {
|
|
1240
|
+
save: "Save",
|
|
1241
|
+
saving: "Saving…",
|
|
1242
|
+
discard: "Discard",
|
|
1243
|
+
unsaved: "Unsaved",
|
|
1244
|
+
loading: "Loading…",
|
|
1245
|
+
unavailable: "Settings service unavailable.",
|
|
1246
|
+
readOnly: "No settings provider in this deployment; config is read-only. Configure it at the profile row in cordis.patch.yml.",
|
|
1247
|
+
invalidNumber: "Enter a positive integer.",
|
|
1248
|
+
collapse: "Collapse",
|
|
1249
|
+
expand: "Expand",
|
|
1250
|
+
enabledOn: "On",
|
|
1251
|
+
enabledOff: "Off",
|
|
1252
|
+
saveFailed: "Save failed; check the fields.",
|
|
1253
|
+
confirm: "Confirm",
|
|
1254
|
+
cancel: "Cancel",
|
|
1255
|
+
listAdd: "Add",
|
|
1256
|
+
listRemove: "Remove",
|
|
1257
|
+
listMoveUp: "Move up",
|
|
1258
|
+
listMoveDown: "Move down",
|
|
1259
|
+
listDuplicate: "Duplicate",
|
|
1260
|
+
listEmpty: "No entries yet.",
|
|
1261
|
+
codeSectionTitle: "Source editor (JSON)",
|
|
1262
|
+
codeSectionDesc: "Edit this plugin’s configurable fields directly (only .volatile() fields). Apply writes back through the official path edits; secret fields never appear here.",
|
|
1263
|
+
codeFormat: "Format",
|
|
1264
|
+
codeApply: "Apply",
|
|
1265
|
+
codeApplying: "Applying…",
|
|
1266
|
+
codeApplied: "Applied.",
|
|
1267
|
+
codeNoChanges: "Identical to the current config; nothing to apply.",
|
|
1268
|
+
codeInvalidJson: "JSON syntax error",
|
|
1269
|
+
codeInvalidEmpty: "Config must not be empty.",
|
|
1270
|
+
codeInvalidRoot: "The root node must be an object.",
|
|
1271
|
+
codeInvalidSchema: "Schema validation failed",
|
|
1272
|
+
codeUnvalidated: "The host returned no schema, so local validation was skipped (writes are still validated by the host).",
|
|
1273
|
+
codeSecretsNote: "{n} secret field(s) are not editable here (manage them in the credentials section or at the row level).",
|
|
1274
|
+
codeUnknownKeys: "{n} key(s) are absent from the schema ({keys}): they are preserved verbatim and ignored at runtime; remove them.",
|
|
1275
|
+
codeUnavailable: "This row is inactive or its namespace is unavailable; the source editor is disabled.",
|
|
1276
|
+
codeReadOnly: "No settings provider in this deployment; the source editor is read-only.",
|
|
1277
|
+
codeApplyFailed: "Apply failed: ",
|
|
1278
|
+
codeLine: "line {line}, column {column}"
|
|
1279
|
+
};
|
|
1280
|
+
/**
|
|
1281
|
+
* own 覆盖同键合并(返回新对象,入参不可变)。
|
|
1282
|
+
* 返回类型保留两侧键集合,调用方据此派生 DictKey。
|
|
1283
|
+
*/
|
|
1284
|
+
function mergeDict(base, own) {
|
|
1285
|
+
return {
|
|
1286
|
+
...base,
|
|
1287
|
+
...own
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
//#endregion
|
|
199
1291
|
//#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
|
|
200
1292
|
/**
|
|
201
1293
|
* @license lucide-react v1.38.0 - ISC
|
|
@@ -475,6 +1567,8 @@ window.__ModuleLoader__.load({
|
|
|
475
1567
|
let state = {
|
|
476
1568
|
status: "loading",
|
|
477
1569
|
value: void 0,
|
|
1570
|
+
schema: void 0,
|
|
1571
|
+
secrets: [],
|
|
478
1572
|
revision: void 0,
|
|
479
1573
|
writable: false
|
|
480
1574
|
};
|
|
@@ -500,6 +1594,8 @@ window.__ModuleLoader__.load({
|
|
|
500
1594
|
publish({
|
|
501
1595
|
status: "unavailable",
|
|
502
1596
|
value: void 0,
|
|
1597
|
+
schema: void 0,
|
|
1598
|
+
secrets: [],
|
|
503
1599
|
revision: void 0,
|
|
504
1600
|
writable: view.writable
|
|
505
1601
|
});
|
|
@@ -508,6 +1604,8 @@ window.__ModuleLoader__.load({
|
|
|
508
1604
|
publish({
|
|
509
1605
|
status: "ready",
|
|
510
1606
|
value: namespace.value,
|
|
1607
|
+
schema: namespace.schema,
|
|
1608
|
+
secrets: namespace.secrets,
|
|
511
1609
|
revision: namespace.revision,
|
|
512
1610
|
writable: view.writable
|
|
513
1611
|
});
|
|
@@ -541,6 +1639,10 @@ window.__ModuleLoader__.load({
|
|
|
541
1639
|
const response = await remote.update(ns, patch, expectedRevision);
|
|
542
1640
|
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
543
1641
|
},
|
|
1642
|
+
mutate: async (ops, expectedRevision) => {
|
|
1643
|
+
const response = await remote.mutate(ns, ops, expectedRevision);
|
|
1644
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings mutate failed");
|
|
1645
|
+
},
|
|
544
1646
|
describe: async () => {
|
|
545
1647
|
const response = await remote.describe();
|
|
546
1648
|
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
@@ -549,101 +1651,6 @@ window.__ModuleLoader__.load({
|
|
|
549
1651
|
};
|
|
550
1652
|
}
|
|
551
1653
|
//#endregion
|
|
552
|
-
//#region ../shared/src/client/styles.ts
|
|
553
|
-
/**
|
|
554
|
-
* 配置分节样式工厂(各插件 client 的公共收编版)。
|
|
555
|
-
*
|
|
556
|
-
* 目标宿主 dsh 0.2.1-alpha.2:插件页把配置视图放进已有页面级内边距的容器里
|
|
557
|
-
* (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
|
|
558
|
-
* 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
|
|
559
|
-
* 横向零内边距、页脚非 sticky),度量对齐官方设置页
|
|
560
|
-
* (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
|
|
561
|
-
*
|
|
562
|
-
* 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
563
|
-
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
564
|
-
* - 页脚按钮等宽换行、状态文字独占一行。
|
|
565
|
-
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
566
|
-
* @module @dsh-plus/shared/client/styles
|
|
567
|
-
*/
|
|
568
|
-
/** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
569
|
-
function cardCss(prefix, extra = "") {
|
|
570
|
-
return `
|
|
571
|
-
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
572
|
-
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
573
|
-
.${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}
|
|
574
|
-
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
575
|
-
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
576
|
-
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
577
|
-
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
578
|
-
.${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}
|
|
579
|
-
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
580
|
-
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
581
|
-
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
582
|
-
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
583
|
-
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
584
|
-
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
585
|
-
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
586
|
-
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
587
|
-
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
588
|
-
.${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;transition:border-color .18s,background .18s}
|
|
589
|
-
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
590
|
-
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
591
|
-
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
592
|
-
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
593
|
-
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
594
|
-
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
595
|
-
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
596
|
-
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
597
|
-
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
598
|
-
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
599
|
-
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
600
|
-
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
601
|
-
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
602
|
-
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
603
|
-
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
604
|
-
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
605
|
-
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
606
|
-
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
607
|
-
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
608
|
-
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
609
|
-
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
610
|
-
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
611
|
-
.${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}
|
|
612
|
-
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
613
|
-
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
614
|
-
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
615
|
-
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
616
|
-
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
617
|
-
@media (max-width:767px){
|
|
618
|
-
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
619
|
-
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
620
|
-
.${prefix}-status{flex-basis:100%;flex:none}
|
|
621
|
-
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
622
|
-
}
|
|
623
|
-
@media (pointer:coarse){
|
|
624
|
-
.${prefix}-btn{min-height:44px}
|
|
625
|
-
.${prefix}-checkRow{min-height:44px}
|
|
626
|
-
.${prefix}-input,.${prefix}-select{height:40px}
|
|
627
|
-
}
|
|
628
|
-
${extra}
|
|
629
|
-
`;
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
633
|
-
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
634
|
-
*/
|
|
635
|
-
function injectCardStyle(pluginId, css) {
|
|
636
|
-
if (typeof document === "undefined") return null;
|
|
637
|
-
const tagId = `${pluginId}/card.css`;
|
|
638
|
-
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
639
|
-
const tag = document.createElement("style");
|
|
640
|
-
tag.dataset.plugin = pluginId;
|
|
641
|
-
tag.dataset.pluginCss = tagId;
|
|
642
|
-
tag.textContent = css;
|
|
643
|
-
document.head.appendChild(tag);
|
|
644
|
-
return tag;
|
|
645
|
-
}
|
|
646
|
-
//#endregion
|
|
647
1654
|
//#region src/ns.ts
|
|
648
1655
|
/**
|
|
649
1656
|
* settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
|
|
@@ -740,15 +1747,62 @@ ${extra}
|
|
|
740
1747
|
return getJson(`${BASE}/presets`);
|
|
741
1748
|
}
|
|
742
1749
|
//#endregion
|
|
1750
|
+
//#region ../shared/lib/index.js
|
|
1751
|
+
/**
|
|
1752
|
+
* 凭据引用名(`credentialRef`)的构造与校验:官方 seam 的引用是 POSIX 风格
|
|
1753
|
+
* 环境变量名(`DEEPSEEK_API_KEY` 形态),本模块提供 dsh-plus 的命名约定与
|
|
1754
|
+
* 派生助手,避免各插件各写一份「kebab → 大写下划线」的私有实现。
|
|
1755
|
+
*
|
|
1756
|
+
* 约定(见《插件存储规范》):
|
|
1757
|
+
* - 前缀段为属主(插件 id 大写下划线化),后接业务段:`<OWNER>_<SEGMENT>...`;
|
|
1758
|
+
* - 全大写字母 / 数字 / 下划线,首字符不能是数字;
|
|
1759
|
+
* - 引用名与官方 CLI 原生读取的环境变量同名时优先沿用(如 `ACTUAL_PASSWORD`),
|
|
1760
|
+
* 这样凭据文件里的一处配置既能被 seam 解析,又能被 CLI 自己的环境读取命中。
|
|
1761
|
+
*
|
|
1762
|
+
* 本模块只做命名,不接触值:值的读写一律走官方 credentials seam。
|
|
1763
|
+
* @module @dsh-plus/shared/credential-refs
|
|
1764
|
+
*/
|
|
1765
|
+
/** 引用名文法:全大写字母/数字/下划线,首字符为字母。 */
|
|
1766
|
+
const REF_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
1767
|
+
/** 段内非法字符(kebab / 点 / 空格等一律折叠为下划线)。 */
|
|
1768
|
+
const SEGMENT_SEPARATOR = /[^A-Za-z0-9]+/g;
|
|
1769
|
+
/** 是否是合法的引用名(消费外部输入前先问它,避免对非法名抛错)。 */
|
|
1770
|
+
function isCredentialRefName(value) {
|
|
1771
|
+
return value.length > 0 && value.length <= 128 && REF_PATTERN.test(value);
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* 把一段自由文本归一为引用名段:kebab/点/空格 → 下划线,转大写。
|
|
1775
|
+
* @param segment - 原始段(如预设 id `my-preset`)。
|
|
1776
|
+
* @returns 归一后的段;归一后为空或首字符为数字时抛错。
|
|
1777
|
+
*/
|
|
1778
|
+
function credentialRefSegment(segment) {
|
|
1779
|
+
const normalized = segment.trim().replace(SEGMENT_SEPARATOR, "_").toUpperCase();
|
|
1780
|
+
if (!isCredentialRefName(normalized)) throw new Error(`invalid credential ref segment: ${JSON.stringify(segment)}`);
|
|
1781
|
+
return normalized;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* 构造引用名:属主段 + 业务段依次归一后以下划线相连。
|
|
1785
|
+
* @param owner - 属主段(插件 id,如 `image-studio`)。
|
|
1786
|
+
* @param segments - 业务段(如预设 id)。
|
|
1787
|
+
* @returns 归一后的引用名(如 `IMAGE_STUDIO_PRESET_MY_PRESET`)。
|
|
1788
|
+
*/
|
|
1789
|
+
function credentialRefName(owner, ...segments) {
|
|
1790
|
+
const name = [credentialRefSegment(owner), ...segments.map(credentialRefSegment)].join("_");
|
|
1791
|
+
if (name.length > 128) throw new Error(`credential ref too long (${name.length}): ${JSON.stringify(name)}`);
|
|
1792
|
+
return name;
|
|
1793
|
+
}
|
|
1794
|
+
//#endregion
|
|
743
1795
|
//#region src/credentials.ts
|
|
744
1796
|
/**
|
|
745
1797
|
* 凭据引用名派生与校验:提供商预设 id → IMAGE_STUDIO_PRESET_<ID>。
|
|
746
|
-
*
|
|
747
|
-
*
|
|
1798
|
+
* 命名与归一走 `@dsh-plus/shared` 的公共助手,本模块只补预设 id 的形态校验。
|
|
1799
|
+
* 纯函数,零依赖(浏览器半亦可安全 import)。
|
|
748
1800
|
* @module image-studio/credentials
|
|
749
1801
|
*/
|
|
750
|
-
/**
|
|
751
|
-
const
|
|
1802
|
+
/** 属主段(插件 id)。 */
|
|
1803
|
+
const REF_OWNER = "image-studio";
|
|
1804
|
+
/** 业务段前缀(与属主一起构成 IMAGE_STUDIO_PRESET_<ID>)。 */
|
|
1805
|
+
const REF_KIND = "preset";
|
|
752
1806
|
/** 预设 id 是否合法(kebab-case,用于引用名派生)。 */
|
|
753
1807
|
function isValidPresetId(id) {
|
|
754
1808
|
return /^[a-z][a-z0-9-]*$/.test(id) && id.length <= 64;
|
|
@@ -756,7 +1810,7 @@ ${extra}
|
|
|
756
1810
|
/** 预设 id → 凭据引用名(kebab → SNAKE 大写)。 */
|
|
757
1811
|
function credentialRefNameOf(presetId) {
|
|
758
1812
|
if (!isValidPresetId(presetId)) throw new Error(`非法提供商预设 id:${JSON.stringify(presetId)}`);
|
|
759
|
-
return
|
|
1813
|
+
return credentialRefName(REF_OWNER, REF_KIND, presetId);
|
|
760
1814
|
}
|
|
761
1815
|
/** 只读目录(冻结防运行期篡改)。 */
|
|
762
1816
|
const PARAM_CATALOG = Object.freeze([
|
|
@@ -1159,6 +2213,9 @@ ${extra}
|
|
|
1159
2213
|
/**
|
|
1160
2214
|
* 配置卡片的三组预设编辑区:提供商(含逐条 API Key 管理)、提示词、参数。
|
|
1161
2215
|
* 从 card.tsx 拆出,避免单组件过长;草稿类型与纯转换在 ./draft.ts。
|
|
2216
|
+
*
|
|
2217
|
+
* 排版与控件一律走 @dsh-plus/shared/client 套件(Section / ListEditor / 字段控件),
|
|
2218
|
+
* 与其它插件卡片保持一致:条目头提供折叠/上移/下移/删除,新增固定在列表底部。
|
|
1162
2219
|
* @module image-studio/client/preset-sections
|
|
1163
2220
|
*/
|
|
1164
2221
|
/** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
|
|
@@ -1201,100 +2258,90 @@ ${extra}
|
|
|
1201
2258
|
className: `imsc-badge ${configured ? "imsc-badgeSet" : "imsc-badgeUnset"}`,
|
|
1202
2259
|
children: t(configured ? "card.credential.configured" : "card.credential.unconfigured")
|
|
1203
2260
|
}) : null,
|
|
1204
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1205
|
-
|
|
1206
|
-
|
|
2261
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2262
|
+
prefix: "imsc",
|
|
2263
|
+
label: t("card.credential.set"),
|
|
1207
2264
|
disabled: disabled || busy || value === "" || !isValidPresetId(presetId),
|
|
1208
|
-
onClick: () => act(() => setCredential(presetId, value))
|
|
1209
|
-
children: t("card.credential.set")
|
|
2265
|
+
onClick: () => act(() => setCredential(presetId, value))
|
|
1210
2266
|
}),
|
|
1211
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1212
|
-
|
|
1213
|
-
|
|
2267
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
|
|
2268
|
+
prefix: "imsc",
|
|
2269
|
+
label: t("card.credential.unset"),
|
|
1214
2270
|
disabled: disabled || busy || configured !== true || !isValidPresetId(presetId),
|
|
1215
|
-
onClick: () => act(() => unsetCredential(presetId))
|
|
1216
|
-
children: t("card.credential.unset")
|
|
2271
|
+
onClick: () => act(() => unsetCredential(presetId))
|
|
1217
2272
|
})
|
|
1218
2273
|
]
|
|
1219
2274
|
});
|
|
1220
2275
|
}
|
|
2276
|
+
/** 列表编辑器文案(公共字典键,各插件一致)。 */
|
|
2277
|
+
function listLabels(t) {
|
|
2278
|
+
return {
|
|
2279
|
+
add: t("listAdd"),
|
|
2280
|
+
remove: t("listRemove"),
|
|
2281
|
+
up: t("listMoveUp"),
|
|
2282
|
+
down: t("listMoveDown"),
|
|
2283
|
+
duplicate: t("listDuplicate"),
|
|
2284
|
+
collapse: t("collapse"),
|
|
2285
|
+
expand: t("expand"),
|
|
2286
|
+
empty: t("listEmpty")
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
1221
2289
|
function ProviderPresetSection(props) {
|
|
1222
2290
|
const { t, draft, disabled, protocols, onPatch, onPatchProvider } = props;
|
|
1223
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
2291
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
2292
|
+
prefix: "imsc",
|
|
2293
|
+
title: t("card.providers"),
|
|
2294
|
+
description: t("card.providersHint"),
|
|
2295
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListEditor, {
|
|
2296
|
+
prefix: "imsc",
|
|
2297
|
+
items: draft.providers,
|
|
2298
|
+
labels: listLabels(t),
|
|
2299
|
+
disabled,
|
|
2300
|
+
titleOf: (provider) => provider.name || provider.id || "—",
|
|
2301
|
+
create: () => emptyProviderDraft(protocols[0]?.id ?? "openai-images"),
|
|
2302
|
+
onChange: (providers) => onPatch({ providers }),
|
|
2303
|
+
renderItem: (provider, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2305
|
+
prefix: "imsc",
|
|
2306
|
+
id: `imsc-pid-${index}`,
|
|
2307
|
+
label: t("card.field.id"),
|
|
2308
|
+
hint: t("card.idHint"),
|
|
2309
|
+
value: provider.id,
|
|
2310
|
+
invalid: provider.id !== "" && !isValidPresetId(provider.id),
|
|
2311
|
+
invalidLabel: t("card.idInvalid"),
|
|
2312
|
+
disabled,
|
|
2313
|
+
onEdit: (v) => onPatchProvider(index, { id: v })
|
|
1243
2314
|
}),
|
|
1244
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
value: provider.id,
|
|
1252
|
-
invalid: provider.id !== "" && !isValidPresetId(provider.id),
|
|
1253
|
-
invalidLabel: t("card.idInvalid"),
|
|
1254
|
-
disabled,
|
|
1255
|
-
onEdit: (v) => onPatchProvider(index, { id: v })
|
|
1256
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1257
|
-
prefix: "imsc",
|
|
1258
|
-
id: `imsc-pname-${index}`,
|
|
1259
|
-
label: t("card.field.name"),
|
|
1260
|
-
value: provider.name,
|
|
1261
|
-
disabled,
|
|
1262
|
-
onEdit: (v) => onPatchProvider(index, { name: v })
|
|
1263
|
-
})]
|
|
2315
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2316
|
+
prefix: "imsc",
|
|
2317
|
+
id: `imsc-pname-${index}`,
|
|
2318
|
+
label: t("card.field.name"),
|
|
2319
|
+
value: provider.name,
|
|
2320
|
+
disabled,
|
|
2321
|
+
onEdit: (v) => onPatchProvider(index, { name: v })
|
|
1264
2322
|
}),
|
|
1265
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
children: protocol.label
|
|
1288
|
-
}, protocol.id))
|
|
1289
|
-
})]
|
|
1290
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1291
|
-
prefix: "imsc",
|
|
1292
|
-
id: `imsc-pmodel-${index}`,
|
|
1293
|
-
label: t("card.field.model"),
|
|
1294
|
-
value: provider.model,
|
|
1295
|
-
disabled,
|
|
1296
|
-
onEdit: (v) => onPatchProvider(index, { model: v })
|
|
1297
|
-
})]
|
|
2323
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
|
|
2324
|
+
prefix: "imsc",
|
|
2325
|
+
id: `imsc-pproto-${index}`,
|
|
2326
|
+
label: t("card.field.protocol"),
|
|
2327
|
+
value: provider.protocol,
|
|
2328
|
+
options: protocols.length > 0 ? protocols.map((protocol) => ({
|
|
2329
|
+
value: protocol.id,
|
|
2330
|
+
label: protocol.label
|
|
2331
|
+
})) : [{
|
|
2332
|
+
value: provider.protocol,
|
|
2333
|
+
label: provider.protocol
|
|
2334
|
+
}],
|
|
2335
|
+
disabled,
|
|
2336
|
+
onEdit: (v) => onPatchProvider(index, { protocol: v })
|
|
2337
|
+
}),
|
|
2338
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2339
|
+
prefix: "imsc",
|
|
2340
|
+
id: `imsc-pmodel-${index}`,
|
|
2341
|
+
label: t("card.field.model"),
|
|
2342
|
+
value: provider.model,
|
|
2343
|
+
disabled,
|
|
2344
|
+
onEdit: (v) => onPatchProvider(index, { model: v })
|
|
1298
2345
|
}),
|
|
1299
2346
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1300
2347
|
prefix: "imsc",
|
|
@@ -1304,150 +2351,105 @@ ${extra}
|
|
|
1304
2351
|
disabled,
|
|
1305
2352
|
onEdit: (v) => onPatchProvider(index, { baseUrl: v })
|
|
1306
2353
|
}),
|
|
1307
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
children: t("card.field.extraHeaders")
|
|
1315
|
-
})
|
|
1316
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1317
|
-
id: `imsc-pheaders-${index}`,
|
|
1318
|
-
className: "imsc-textarea",
|
|
1319
|
-
value: provider.extraHeadersText,
|
|
1320
|
-
disabled,
|
|
1321
|
-
onChange: (event) => onPatchProvider(index, { extraHeadersText: event.target.value })
|
|
1322
|
-
})]
|
|
2354
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeField, {
|
|
2355
|
+
prefix: "imsc",
|
|
2356
|
+
id: `imsc-pheaders-${index}`,
|
|
2357
|
+
label: t("card.field.extraHeaders"),
|
|
2358
|
+
value: provider.extraHeadersText,
|
|
2359
|
+
disabled,
|
|
2360
|
+
onEdit: (v) => onPatchProvider(index, { extraHeadersText: v })
|
|
1323
2361
|
}),
|
|
1324
2362
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
|
|
1325
2363
|
t,
|
|
1326
2364
|
presetId: provider.id,
|
|
1327
2365
|
disabled
|
|
1328
2366
|
})
|
|
1329
|
-
]
|
|
1330
|
-
}, `provider-${index}`)),
|
|
1331
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1332
|
-
type: "button",
|
|
1333
|
-
className: "imsc-btn imsc-btnGhost",
|
|
1334
|
-
disabled,
|
|
1335
|
-
onClick: () => onPatch({ providers: [...draft.providers, emptyProviderDraft(protocols[0]?.id ?? "openai-images")] }),
|
|
1336
|
-
children: [
|
|
1337
|
-
t("card.add"),
|
|
1338
|
-
" · ",
|
|
1339
|
-
t("card.providers")
|
|
1340
|
-
]
|
|
2367
|
+
] })
|
|
1341
2368
|
})
|
|
1342
|
-
|
|
2369
|
+
});
|
|
1343
2370
|
}
|
|
1344
2371
|
function PromptPresetSection(props) {
|
|
1345
2372
|
const { t, draft, disabled, onPatch, onPatchPrompt } = props;
|
|
1346
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
className: "imsc-textarea",
|
|
1370
|
-
"aria-label": t("card.field.text"),
|
|
2373
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
2374
|
+
prefix: "imsc",
|
|
2375
|
+
title: t("card.prompts"),
|
|
2376
|
+
description: t("card.promptsHint"),
|
|
2377
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListEditor, {
|
|
2378
|
+
prefix: "imsc",
|
|
2379
|
+
items: draft.prompts,
|
|
2380
|
+
labels: listLabels(t),
|
|
2381
|
+
disabled,
|
|
2382
|
+
titleOf: (prompt) => prompt.name || "—",
|
|
2383
|
+
create: () => emptyPromptDraft(),
|
|
2384
|
+
onChange: (prompts) => onPatch({ prompts }),
|
|
2385
|
+
renderItem: (prompt, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2386
|
+
prefix: "imsc",
|
|
2387
|
+
id: `imsc-prname-${index}`,
|
|
2388
|
+
label: t("card.field.name"),
|
|
2389
|
+
value: prompt.name,
|
|
2390
|
+
disabled,
|
|
2391
|
+
onEdit: (v) => onPatchPrompt(index, { name: v })
|
|
2392
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeField, {
|
|
2393
|
+
prefix: "imsc",
|
|
2394
|
+
id: `imsc-prtext-${index}`,
|
|
2395
|
+
label: t("card.field.text"),
|
|
1371
2396
|
value: prompt.text,
|
|
1372
2397
|
disabled,
|
|
1373
|
-
|
|
1374
|
-
})]
|
|
1375
|
-
}, `prompt-${index}`)),
|
|
1376
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1377
|
-
type: "button",
|
|
1378
|
-
className: "imsc-btn imsc-btnGhost",
|
|
1379
|
-
disabled,
|
|
1380
|
-
onClick: () => onPatch({ prompts: [...draft.prompts, emptyPromptDraft()] }),
|
|
1381
|
-
children: [
|
|
1382
|
-
t("card.add"),
|
|
1383
|
-
" · ",
|
|
1384
|
-
t("card.prompts")
|
|
1385
|
-
]
|
|
2398
|
+
onEdit: (v) => onPatchPrompt(index, { text: v })
|
|
2399
|
+
})] })
|
|
1386
2400
|
})
|
|
1387
|
-
|
|
2401
|
+
});
|
|
1388
2402
|
}
|
|
1389
2403
|
function ParamPresetSection(props) {
|
|
1390
2404
|
const { t, draft, disabled, onPatch, onPatchParam } = props;
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
onChange: (event) => onPatchParam(index, { name: event.target.value })
|
|
1407
|
-
}),
|
|
1408
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1409
|
-
className: "imsc-select",
|
|
1410
|
-
"aria-label": t("card.field.endpoint"),
|
|
1411
|
-
value: param.endpoint,
|
|
1412
|
-
disabled,
|
|
1413
|
-
onChange: (event) => onPatchParam(index, { endpoint: event.target.value }),
|
|
1414
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1415
|
-
value: "generation",
|
|
1416
|
-
children: t("tab.generation")
|
|
1417
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1418
|
-
value: "edit",
|
|
1419
|
-
children: t("tab.edit")
|
|
1420
|
-
})]
|
|
1421
|
-
}),
|
|
1422
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1423
|
-
type: "button",
|
|
1424
|
-
className: "imsc-btn imsc-btnGhost",
|
|
1425
|
-
disabled,
|
|
1426
|
-
onClick: () => onPatch({ params: draft.params.filter((_, i) => i !== index) }),
|
|
1427
|
-
children: t("common.delete")
|
|
1428
|
-
})
|
|
1429
|
-
]
|
|
1430
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1431
|
-
className: "imsc-textarea",
|
|
1432
|
-
"aria-label": t("card.field.paramSpecs"),
|
|
1433
|
-
placeholder: "{ \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } }",
|
|
1434
|
-
value: param.specsText,
|
|
1435
|
-
disabled,
|
|
1436
|
-
onChange: (event) => onPatchParam(index, { specsText: event.target.value })
|
|
1437
|
-
})]
|
|
1438
|
-
}, `param-${index}`)),
|
|
1439
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1440
|
-
type: "button",
|
|
1441
|
-
className: "imsc-btn imsc-btnGhost",
|
|
2405
|
+
const endpoints = [{
|
|
2406
|
+
value: "generation",
|
|
2407
|
+
label: t("tab.generation")
|
|
2408
|
+
}, {
|
|
2409
|
+
value: "edit",
|
|
2410
|
+
label: t("tab.edit")
|
|
2411
|
+
}];
|
|
2412
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
2413
|
+
prefix: "imsc",
|
|
2414
|
+
title: t("card.params"),
|
|
2415
|
+
description: t("card.paramsHint"),
|
|
2416
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ListEditor, {
|
|
2417
|
+
prefix: "imsc",
|
|
2418
|
+
items: draft.params,
|
|
2419
|
+
labels: listLabels(t),
|
|
1442
2420
|
disabled,
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
2421
|
+
titleOf: (param) => param.name || "—",
|
|
2422
|
+
create: () => emptyParamDraft(),
|
|
2423
|
+
onChange: (params) => onPatch({ params }),
|
|
2424
|
+
renderItem: (param, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
2425
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2426
|
+
prefix: "imsc",
|
|
2427
|
+
id: `imsc-paname-${index}`,
|
|
2428
|
+
label: t("card.field.name"),
|
|
2429
|
+
value: param.name,
|
|
2430
|
+
disabled,
|
|
2431
|
+
onEdit: (v) => onPatchParam(index, { name: v })
|
|
2432
|
+
}),
|
|
2433
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectField, {
|
|
2434
|
+
prefix: "imsc",
|
|
2435
|
+
id: `imsc-paendpoint-${index}`,
|
|
2436
|
+
label: t("card.field.endpoint"),
|
|
2437
|
+
value: param.endpoint,
|
|
2438
|
+
options: endpoints,
|
|
2439
|
+
disabled,
|
|
2440
|
+
onEdit: (v) => onPatchParam(index, { endpoint: v })
|
|
2441
|
+
}),
|
|
2442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeField, {
|
|
2443
|
+
prefix: "imsc",
|
|
2444
|
+
id: `imsc-paspecs-${index}`,
|
|
2445
|
+
label: t("card.field.paramSpecs"),
|
|
2446
|
+
value: param.specsText,
|
|
2447
|
+
disabled,
|
|
2448
|
+
onEdit: (v) => onPatchParam(index, { specsText: v })
|
|
2449
|
+
})
|
|
2450
|
+
] })
|
|
1449
2451
|
})
|
|
1450
|
-
|
|
2452
|
+
});
|
|
1451
2453
|
}
|
|
1452
2454
|
//#endregion
|
|
1453
2455
|
//#region src/client/card.tsx
|
|
@@ -1491,60 +2493,57 @@ ${extra}
|
|
|
1491
2493
|
/** 高级项:并发 / 超时 / 代理 / 画廊上限 / 上传保留时长。 */
|
|
1492
2494
|
function AdvancedFields(props) {
|
|
1493
2495
|
const { t, draft, disabled, onPatch } = props;
|
|
1494
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1500
|
-
className: "imsc-grid2",
|
|
1501
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1502
|
-
prefix: "imsc",
|
|
1503
|
-
id: "imsc-conc",
|
|
1504
|
-
label: t("card.maxConcurrent"),
|
|
1505
|
-
numeric: true,
|
|
1506
|
-
value: draft.maxConcurrent,
|
|
1507
|
-
disabled,
|
|
1508
|
-
onEdit: (v) => onPatch({ maxConcurrent: v })
|
|
1509
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1510
|
-
prefix: "imsc",
|
|
1511
|
-
id: "imsc-timeout",
|
|
1512
|
-
label: t("card.requestTimeoutMs"),
|
|
1513
|
-
numeric: true,
|
|
1514
|
-
value: draft.requestTimeoutMs,
|
|
1515
|
-
disabled,
|
|
1516
|
-
onEdit: (v) => onPatch({ requestTimeoutMs: v })
|
|
1517
|
-
})]
|
|
1518
|
-
}),
|
|
1519
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1520
|
-
className: "imsc-grid2",
|
|
1521
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1522
|
-
prefix: "imsc",
|
|
1523
|
-
id: "imsc-proxy",
|
|
1524
|
-
label: t("card.proxy"),
|
|
1525
|
-
value: draft.proxy,
|
|
1526
|
-
disabled,
|
|
1527
|
-
onEdit: (v) => onPatch({ proxy: v })
|
|
1528
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
1529
|
-
prefix: "imsc",
|
|
1530
|
-
id: "imsc-gmax",
|
|
1531
|
-
label: t("card.galleryMaxItems"),
|
|
1532
|
-
numeric: true,
|
|
1533
|
-
value: draft.galleryMaxItems,
|
|
1534
|
-
disabled,
|
|
1535
|
-
onEdit: (v) => onPatch({ galleryMaxItems: v })
|
|
1536
|
-
})]
|
|
1537
|
-
}),
|
|
1538
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2496
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
|
|
2497
|
+
prefix: "imsc",
|
|
2498
|
+
title: t("card.advanced"),
|
|
2499
|
+
description: t("card.advancedHint"),
|
|
2500
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(FieldGrid, {
|
|
1539
2501
|
prefix: "imsc",
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2502
|
+
columns: 2,
|
|
2503
|
+
children: [
|
|
2504
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberField, {
|
|
2505
|
+
prefix: "imsc",
|
|
2506
|
+
id: "imsc-conc",
|
|
2507
|
+
label: t("card.maxConcurrent"),
|
|
2508
|
+
value: draft.maxConcurrent,
|
|
2509
|
+
disabled,
|
|
2510
|
+
onEdit: (v) => onPatch({ maxConcurrent: v })
|
|
2511
|
+
}),
|
|
2512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberField, {
|
|
2513
|
+
prefix: "imsc",
|
|
2514
|
+
id: "imsc-timeout",
|
|
2515
|
+
label: t("card.requestTimeoutMs"),
|
|
2516
|
+
value: draft.requestTimeoutMs,
|
|
2517
|
+
disabled,
|
|
2518
|
+
onEdit: (v) => onPatch({ requestTimeoutMs: v })
|
|
2519
|
+
}),
|
|
2520
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
|
|
2521
|
+
prefix: "imsc",
|
|
2522
|
+
id: "imsc-proxy",
|
|
2523
|
+
label: t("card.proxy"),
|
|
2524
|
+
value: draft.proxy,
|
|
2525
|
+
disabled,
|
|
2526
|
+
onEdit: (v) => onPatch({ proxy: v })
|
|
2527
|
+
}),
|
|
2528
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberField, {
|
|
2529
|
+
prefix: "imsc",
|
|
2530
|
+
id: "imsc-gmax",
|
|
2531
|
+
label: t("card.galleryMaxItems"),
|
|
2532
|
+
value: draft.galleryMaxItems,
|
|
2533
|
+
disabled,
|
|
2534
|
+
onEdit: (v) => onPatch({ galleryMaxItems: v })
|
|
2535
|
+
}),
|
|
2536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberField, {
|
|
2537
|
+
prefix: "imsc",
|
|
2538
|
+
id: "imsc-upload-ttl",
|
|
2539
|
+
label: t("card.uploadTtlHours"),
|
|
2540
|
+
value: draft.uploadTtlHours,
|
|
2541
|
+
disabled,
|
|
2542
|
+
onEdit: (v) => onPatch({ uploadTtlHours: v })
|
|
2543
|
+
})
|
|
2544
|
+
]
|
|
1546
2545
|
})
|
|
1547
|
-
|
|
2546
|
+
});
|
|
1548
2547
|
}
|
|
1549
2548
|
function StudioConfigCard(props) {
|
|
1550
2549
|
const { t, scope, api } = props;
|
|
@@ -1619,7 +2618,7 @@ ${extra}
|
|
|
1619
2618
|
...next
|
|
1620
2619
|
} : p) })
|
|
1621
2620
|
};
|
|
1622
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
2621
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ConfigShell, {
|
|
1623
2622
|
prefix: "imsc",
|
|
1624
2623
|
title: t("card.title"),
|
|
1625
2624
|
description: t("card.description"),
|
|
@@ -1662,11 +2661,14 @@ ${extra}
|
|
|
1662
2661
|
/**
|
|
1663
2662
|
* i18n 字典(zh 为键域事实源,en 全量对译);NS 与 settings 命名空间分离,
|
|
1664
2663
|
* 仅承载前端文案 key。面板 / 入口 / 配置卡片共用同一命名空间。
|
|
2664
|
+
*
|
|
2665
|
+
* 公共键(卡片套件的 save/unsaved/code* 等)来自 shared 的 common 字典并在此合并,
|
|
2666
|
+
* 本文件只维护业务键。
|
|
1665
2667
|
* @module image-studio/client/i18n
|
|
1666
2668
|
*/
|
|
1667
2669
|
/** locale 命名空间。 */
|
|
1668
2670
|
const NS = "dsh-plus-image-studio";
|
|
1669
|
-
const
|
|
2671
|
+
const ownZh = {
|
|
1670
2672
|
"entry.label": "图像",
|
|
1671
2673
|
"panel.title": "图像工作室",
|
|
1672
2674
|
"tab.generation": "文生图",
|
|
@@ -1748,9 +2750,13 @@ ${extra}
|
|
|
1748
2750
|
"card.description": "文生图/图生图:提供商与 API Key、参数/提示词预设",
|
|
1749
2751
|
"card.summary": "文生图/图生图:提供商与 API Key、参数/提示词预设。",
|
|
1750
2752
|
"card.providers": "提供商预设",
|
|
2753
|
+
"card.providersHint": "按序尝试的提供商预设:条目内 API Key 走官方 credentials(引用名由预设 ID 派生),保存后不回显。",
|
|
1751
2754
|
"card.prompts": "提示词预设",
|
|
2755
|
+
"card.promptsHint": "提示词模板列表:生成/编辑面板按名称选用,{prompt} 等占位符由面板注入。",
|
|
1752
2756
|
"card.params": "参数预设",
|
|
2757
|
+
"card.paramsHint": "参数预设列表:适用端点决定出现在「文生图」还是「图生图」,参数表为 JSON(如 {\"size\": {\"enabled\": true, \"value\": \"1024x1024\"}})。",
|
|
1753
2758
|
"card.advanced": "高级设置",
|
|
2759
|
+
"card.advancedHint": "并发、超时、代理与本地画廊保留策略;留空/0 表示沿用内置默认(并发 2、超时 60000ms、画廊 200 张、上传保留 168 小时)。",
|
|
1754
2760
|
"card.add": "添加",
|
|
1755
2761
|
"card.field.id": "ID",
|
|
1756
2762
|
"card.field.name": "名称",
|
|
@@ -1783,7 +2789,7 @@ ${extra}
|
|
|
1783
2789
|
"card.badgeOn": "{n} 个提供商",
|
|
1784
2790
|
"card.badgeOff": "未配置"
|
|
1785
2791
|
};
|
|
1786
|
-
const
|
|
2792
|
+
const ownEn = {
|
|
1787
2793
|
"entry.label": "Image",
|
|
1788
2794
|
"panel.title": "Image Studio",
|
|
1789
2795
|
"tab.generation": "Text to Image",
|
|
@@ -1865,9 +2871,13 @@ ${extra}
|
|
|
1865
2871
|
"card.description": "Text/Image-to-image: providers & API keys, param/prompt presets",
|
|
1866
2872
|
"card.summary": "Text/Image-to-image: providers & API keys, param/prompt presets.",
|
|
1867
2873
|
"card.providers": "Provider presets",
|
|
2874
|
+
"card.providersHint": "Providers tried in order; each entry keeps its API key in official credentials (reference derived from the preset id) and never echoes it back.",
|
|
1868
2875
|
"card.prompts": "Prompt presets",
|
|
2876
|
+
"card.promptsHint": "Prompt templates; the generation/edit panels pick them by name and fill placeholders such as {prompt}.",
|
|
1869
2877
|
"card.params": "Parameter presets",
|
|
2878
|
+
"card.paramsHint": "Parameter presets; the endpoint decides whether it shows up in Text to Image or Image to Image, and the spec is JSON such as {\"size\": {\"enabled\": true, \"value\": \"1024x1024\"}}.",
|
|
1870
2879
|
"card.advanced": "Advanced",
|
|
2880
|
+
"card.advancedHint": "Concurrency, timeout, proxy and local gallery retention; empty/0 keeps the built-in defaults (concurrency 2, timeout 60000ms, gallery 200, uploads 168h).",
|
|
1871
2881
|
"card.add": "Add",
|
|
1872
2882
|
"card.field.id": "ID",
|
|
1873
2883
|
"card.field.name": "Name",
|
|
@@ -1900,6 +2910,9 @@ ${extra}
|
|
|
1900
2910
|
"card.badgeOn": "{n} providers",
|
|
1901
2911
|
"card.badgeOff": "Not configured"
|
|
1902
2912
|
};
|
|
2913
|
+
/** 卡片公共文案 + 本插件文案(own 覆盖同键):配置卡片经共享套件渲染,公共键只维护一份。 */
|
|
2914
|
+
const zh = mergeDict(commonZh, ownZh);
|
|
2915
|
+
const en = mergeDict(commonEn, ownEn);
|
|
1903
2916
|
//#endregion
|
|
1904
2917
|
//#region src/client/panel/controller.ts
|
|
1905
2918
|
/** 创建一个模块级控制器(apply 内调用一次,inject 给两个 slot 入口)。 */
|
|
@@ -4101,29 +5114,6 @@ ${extra}
|
|
|
4101
5114
|
|
|
4102
5115
|
/* ── 配置卡片附加件(imsc- 基础类由 cardCss('imsc') 生成)── */
|
|
4103
5116
|
.imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
4104
|
-
.imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
|
|
4105
|
-
.imsc-presetBox {
|
|
4106
|
-
border-top: 1px solid var(--dsw-alias-border-l3);
|
|
4107
|
-
padding: 10px 0 2px;
|
|
4108
|
-
display: grid;
|
|
4109
|
-
gap: 8px;
|
|
4110
|
-
}
|
|
4111
|
-
.imsc-presetHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
4112
|
-
.imsc-presetName { font-size: 13px; font-weight: 600; }
|
|
4113
|
-
.imsc-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
4114
|
-
.imsc-textarea {
|
|
4115
|
-
box-sizing: border-box;
|
|
4116
|
-
width: 100%;
|
|
4117
|
-
min-height: 64px;
|
|
4118
|
-
padding: 6px 10px;
|
|
4119
|
-
border: 1px solid var(--dsw-alias-border-l2);
|
|
4120
|
-
border-radius: 8px;
|
|
4121
|
-
background: var(--dsw-alias-bg-layer-3);
|
|
4122
|
-
color: var(--dsw-alias-label-primary);
|
|
4123
|
-
font-family: var(--ds-font-family-code);
|
|
4124
|
-
font-size: 12px;
|
|
4125
|
-
resize: vertical;
|
|
4126
|
-
}
|
|
4127
5117
|
.imsc-credRow { display: flex; align-items: center; gap: 8px; }
|
|
4128
5118
|
.imsc-credRow .imsc-input { flex: 1; min-width: 0; }
|
|
4129
5119
|
|
|
@@ -4159,7 +5149,6 @@ ${extra}
|
|
|
4159
5149
|
.ims-paramControl { width: 108px; }
|
|
4160
5150
|
.ims-btn { min-height: 40px; }
|
|
4161
5151
|
.ims-btnSmall { min-height: 32px; }
|
|
4162
|
-
.imsc-grid2 { grid-template-columns: 1fr; }
|
|
4163
5152
|
}
|
|
4164
5153
|
`;
|
|
4165
5154
|
/** 注入面板 + 入口 + 卡片附加样式(重复调用幂等:已存在则跳过)。 */
|