@dsh-plus/image-studio 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,9 +4,175 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
+ let react = require("react");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ //#region ../shared/src/client/card.tsx
11
+ const IDLE_STATUS = {
12
+ kind: "idle",
13
+ text: ""
14
+ };
15
+ function CardChrome(props) {
16
+ const p = props.prefix;
17
+ const badge = props.statusBadge;
18
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
19
+ className: `${p}-section`,
20
+ "aria-label": props.title,
21
+ children: [
22
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23
+ className: `${p}-sectionHead`,
24
+ children: [
25
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
26
+ className: `${p}-sectionTitle`,
27
+ children: props.title
28
+ }),
29
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30
+ className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
31
+ children: badge.text
32
+ }) : null,
33
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34
+ className: `${p}-pending`,
35
+ children: props.dirtyLabel
36
+ }) : null
37
+ ]
38
+ }),
39
+ props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
40
+ className: `${p}-sectionDesc`,
41
+ children: props.description
42
+ }),
43
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44
+ className: `${p}-sectionBody`,
45
+ children: [
46
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
47
+ className: `${p}-readOnly`,
48
+ role: "status",
49
+ children: props.readOnlyNotice
50
+ }) : null,
51
+ props.children,
52
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53
+ className: `${p}-sectionFooter`,
54
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
55
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
56
+ role: "status",
57
+ children: props.status.text
58
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
59
+ type: "button",
60
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
61
+ disabled: action.disabled === true,
62
+ onClick: action.onClick,
63
+ children: action.label
64
+ }, action.key))]
65
+ })
66
+ ]
67
+ })
68
+ ]
69
+ });
70
+ }
71
+ /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
72
+ function CardLoading(props) {
73
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
74
+ className: `${props.prefix}-section`,
75
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
76
+ className: `${props.prefix}-readOnly`,
77
+ role: "status",
78
+ children: props.text
79
+ })
80
+ });
81
+ }
82
+ //#endregion
83
+ //#region ../shared/src/client/config-slots.ts
84
+ /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
85
+ const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
86
+ /**
87
+ * 把一张配置卡片注册进插件页的两个配置槽位。
88
+ * 两个槽位都由 0.2.1-alpha.1 的插件页声明,任一缺席即报错——不做版本探测。
89
+ */
90
+ function injectPluginConfigCard(slots, reg) {
91
+ const base = {
92
+ locale: reg.ns,
93
+ inject: reg.inject
94
+ };
95
+ slots.inject("plugins.row.config", () => slots.register({
96
+ name: "plugins.row.config",
97
+ key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
98
+ ...base
99
+ }, reg.component));
100
+ slots.inject("plugins.bundle.config", () => slots.register({
101
+ name: "plugins.bundle.config",
102
+ key: reg.pkg,
103
+ ...base
104
+ }, reg.component));
105
+ }
106
+ //#endregion
107
+ //#region ../shared/src/client/draft.ts
108
+ /**
109
+ * 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
110
+ * 脏判定、保存(revision fencing + 重新装载)与状态行。
111
+ *
112
+ * 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
113
+ * 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
114
+ * 依赖,必须是模块级稳定函数(不要内联箭头函数)。
115
+ * @module @dsh-plus/shared/client/draft
116
+ */
117
+ //#endregion
118
+ //#region ../shared/src/client/fetch.ts
119
+ async function parse(res, guard) {
120
+ const body = await res.json();
121
+ if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
122
+ if (guard !== void 0 && !guard(body)) throw new Error(`响应结构不符合预期(${res.url})`);
123
+ return body;
124
+ }
125
+ /** GET 同源 JSON 端点。 */
126
+ async function getJson(route, guard) {
127
+ return parse(await fetch(route, { credentials: "same-origin" }), guard);
128
+ }
129
+ /** POST 同源 JSON 端点(body 可省略)。 */
130
+ async function postJson(route, body, guard) {
131
+ return parse(await fetch(route, {
132
+ method: "POST",
133
+ credentials: "same-origin",
134
+ headers: { "content-type": "application/json" },
135
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
136
+ }), guard);
137
+ }
138
+ //#endregion
139
+ //#region ../shared/src/client/fields.tsx
140
+ function TextField(props) {
141
+ const p = props.prefix ?? "dshp";
142
+ const invalid = props.invalid === true;
143
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
144
+ className: `${p}-field`,
145
+ children: [
146
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
147
+ className: `${p}-head`,
148
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
149
+ className: `${p}-label`,
150
+ htmlFor: props.id,
151
+ children: props.label
152
+ }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
153
+ className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
154
+ children: props.badge.text
155
+ }) : null]
156
+ }),
157
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
158
+ id: props.id,
159
+ className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
160
+ type: props.password === true ? "password" : "text",
161
+ inputMode: props.numeric === true ? "numeric" : void 0,
162
+ autoComplete: props.password === true ? "off" : void 0,
163
+ "aria-invalid": invalid || void 0,
164
+ value: props.value,
165
+ disabled: props.disabled === true,
166
+ onChange: (event) => props.onEdit(event.target.value)
167
+ }),
168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
169
+ className: invalid ? `${p}-invalid` : `${p}-hint`,
170
+ children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
171
+ })
172
+ ]
173
+ });
174
+ }
175
+ //#endregion
10
176
  //#region ../../node_modules/.pnpm/lucide-react@1.38.0_react@18.2.0/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs
11
177
  /**
12
178
  * @license lucide-react v1.38.0 - ISC
@@ -272,158 +438,6 @@ window.__ModuleLoader__.load({
272
438
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Layers, { ...lucideProps(props) });
273
439
  }
274
440
  //#endregion
275
- //#region ../shared/src/client/card.tsx
276
- const IDLE_STATUS = {
277
- kind: "idle",
278
- text: ""
279
- };
280
- function CardChrome(props) {
281
- const p = props.prefix;
282
- const badge = props.statusBadge;
283
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
284
- className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
285
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
286
- type: "button",
287
- className: `${p}-header`,
288
- "aria-expanded": props.open,
289
- "aria-label": props.title,
290
- onClick: () => props.onToggle(!props.open),
291
- children: [
292
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
293
- className: `${p}-headText`,
294
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
295
- className: `${p}-name`,
296
- children: props.title
297
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
298
- className: `${p}-description`,
299
- children: props.description
300
- })]
301
- }),
302
- badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
303
- className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
304
- children: badge.text
305
- }) : null,
306
- props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
307
- className: `${p}-pending`,
308
- children: props.dirtyLabel
309
- }) : null,
310
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
311
- ]
312
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
313
- className: `${p}-body`,
314
- children: [
315
- props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
316
- className: `${p}-readOnly`,
317
- role: "status",
318
- children: props.readOnlyNotice
319
- }) : null,
320
- props.children,
321
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
322
- className: `${p}-footer`,
323
- children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
324
- className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
325
- role: "status",
326
- children: props.status.text
327
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
328
- type: "button",
329
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
330
- disabled: action.disabled === true,
331
- onClick: action.onClick,
332
- children: action.label
333
- }, action.key))]
334
- })
335
- ]
336
- }) : null]
337
- });
338
- }
339
- //#endregion
340
- //#region ../shared/src/client/config-slots.ts
341
- /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
342
- const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
343
- /**
344
- * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
345
- * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
346
- */
347
- function injectPluginConfigCard(slots, reg) {
348
- const base = {
349
- locale: reg.ns,
350
- inject: reg.inject
351
- };
352
- slots.inject("settings.plugin.item", () => slots.register({
353
- name: "settings.plugin.item",
354
- key: reg.ns,
355
- ...base
356
- }, reg.component));
357
- slots.inject("plugins.row.config", () => slots.register({
358
- name: "plugins.row.config",
359
- key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
360
- ...base
361
- }, reg.component));
362
- slots.inject("plugins.bundle.config", () => slots.register({
363
- name: "plugins.bundle.config",
364
- key: reg.pkg,
365
- ...base
366
- }, reg.component));
367
- }
368
- //#endregion
369
- //#region ../shared/src/client/fetch.ts
370
- async function parse(res, guard) {
371
- const body = await res.json();
372
- if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
373
- if (guard !== void 0 && !guard(body)) throw new Error(`响应结构不符合预期(${res.url})`);
374
- return body;
375
- }
376
- /** GET 同源 JSON 端点。 */
377
- async function getJson(route, guard) {
378
- return parse(await fetch(route, { credentials: "same-origin" }), guard);
379
- }
380
- /** POST 同源 JSON 端点(body 可省略)。 */
381
- async function postJson(route, body, guard) {
382
- return parse(await fetch(route, {
383
- method: "POST",
384
- credentials: "same-origin",
385
- headers: { "content-type": "application/json" },
386
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
387
- }), guard);
388
- }
389
- //#endregion
390
- //#region ../shared/src/client/fields.tsx
391
- function TextField(props) {
392
- const p = props.prefix ?? "dshp";
393
- const invalid = props.invalid === true;
394
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
395
- className: `${p}-field`,
396
- children: [
397
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
- className: `${p}-head`,
399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
400
- className: `${p}-label`,
401
- htmlFor: props.id,
402
- children: props.label
403
- }), props.badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
404
- className: `${p}-badge ${props.badge.set ? `${p}-badgeSet` : `${p}-badgeUnset`}`,
405
- children: props.badge.text
406
- }) : null]
407
- }),
408
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
409
- id: props.id,
410
- className: `${p}-input${invalid ? ` ${p}-inputInvalid` : ""}`,
411
- type: props.password === true ? "password" : "text",
412
- inputMode: props.numeric === true ? "numeric" : void 0,
413
- autoComplete: props.password === true ? "off" : void 0,
414
- "aria-invalid": invalid || void 0,
415
- value: props.value,
416
- disabled: props.disabled === true,
417
- onChange: (event) => props.onEdit(event.target.value)
418
- }),
419
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
420
- className: invalid ? `${p}-invalid` : `${p}-hint`,
421
- children: invalid ? props.invalidLabel ?? "" : props.hint ?? ""
422
- })
423
- ]
424
- });
425
- }
426
- //#endregion
427
441
  //#region ../shared/src/client/scope.ts
428
442
  /**
429
443
  * remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
@@ -514,33 +528,33 @@ window.__ModuleLoader__.load({
514
528
  //#endregion
515
529
  //#region ../shared/src/client/styles.ts
516
530
  /**
517
- * 配置卡片样式工厂(各插件 client 的公共收编版)。
518
- * 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
519
- * 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
531
+ * 配置分节样式工厂(各插件 client 的公共收编版)。
532
+ *
533
+ * 目标宿主 dsh 0.2.1-alpha.1:插件页把配置视图放进已有页面级内边距的容器里
534
+ * (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
535
+ * 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
536
+ * 横向零内边距、页脚非 sticky),度量对齐官方设置页
537
+ * (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
538
+ *
539
+ * 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
520
540
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
521
- * - footer 按钮等宽换行、状态文字独占一行;
522
- * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
541
+ * - 页脚按钮等宽换行、状态文字独占一行。
523
542
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
524
543
  * @module @dsh-plus/shared/client/styles
525
544
  */
526
- /** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
545
+ /** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
527
546
  function cardCss(prefix, extra = "") {
528
547
  return `
529
- .${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
530
- .${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
531
- .${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
532
- .${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
533
- .${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
534
- .${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
535
- .${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
536
- .${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
537
- .${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
538
- .${prefix}-chevronOpen{transform:rotate(180deg)}
548
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
549
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
550
+ .${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}
551
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
552
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
553
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
554
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
539
555
  .${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}
540
- .${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
541
556
  .${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
542
557
  .${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
543
- .${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
544
558
  .${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
545
559
  .${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
546
560
  .${prefix}-head{align-items:center;gap:8px;display:flex}
@@ -569,7 +583,6 @@ window.__ModuleLoader__.load({
569
583
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
570
584
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
571
585
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
572
- .${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
573
586
  .${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
574
587
  .${prefix}-statusError{color:var(--dsw-alias-label-error)}
575
588
  .${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}
@@ -579,14 +592,10 @@ window.__ModuleLoader__.load({
579
592
  .${prefix}-btn:disabled{opacity:.4;cursor:default}
580
593
  .${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
581
594
  @media (max-width:767px){
582
- .${prefix}-body{margin:0 10px}
583
- .${prefix}-header{padding:12px 12px}
584
595
  .${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
585
596
  .${prefix}-checkRow{min-height:44px;padding:8px 0}
586
- .${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
587
597
  .${prefix}-status{flex-basis:100%;flex:none}
588
598
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
589
- .${prefix}-statusBadge{display:none}
590
599
  }
591
600
  @media (pointer:coarse){
592
601
  .${prefix}-btn{min-height:44px}
@@ -617,13 +626,97 @@ ${extra}
617
626
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
618
627
  * 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 对命名空间做编译期
619
628
  * 语法校验,见 config.ts);浏览器半把它作为配置卡片的注册键
620
- * (legacy settings.plugin.item keyed 槽位按「卡片编辑的 settings 命名空间」
621
- * 分发;0.1.6-alpha.2 起改挂插件页 row/bundle config,见 shared/config-slots),
629
+ * (插件页配置卡片挂在 row/bundle config 槽位上,与命名空间解耦,
630
+ * 见 shared/config-slots),
622
631
  * 同一字面量两处共用,防止漂移。
623
632
  * @module image-studio/ns
624
633
  */
625
634
  const SETTINGS_NS = "dsh-plus-image-studio";
626
635
  //#endregion
636
+ //#region src/client/api.ts
637
+ /**
638
+ * 数据端点通道(前端预留):同源 fetch 封装,类型与 dto.ts 对齐。
639
+ * 本次仅交付通道层,不交付调用它的界面。
640
+ * @module image-studio/client/api
641
+ */
642
+ const BASE = "/dsh-plus/image-studio";
643
+ /** 提交生图任务。 */
644
+ function generate(request) {
645
+ return postJson(`${BASE}/generate`, request);
646
+ }
647
+ /** 批量任务轮询。 */
648
+ function fetchTasks() {
649
+ return getJson(`${BASE}/tasks`);
650
+ }
651
+ /** 取消任务。 */
652
+ function cancelTask(taskId) {
653
+ return postJson(`${BASE}/tasks/${taskId}/cancel`);
654
+ }
655
+ /** 画廊列表应答形状守卫:items 需为数组(渲染期直接 map)。 */
656
+ function isGalleryList(value) {
657
+ if (typeof value !== "object" || value === null) return false;
658
+ return Array.isArray(value.items);
659
+ }
660
+ /** 画廊列表。 */
661
+ function fetchGallery() {
662
+ return getJson(`${BASE}/gallery`, isGalleryList);
663
+ }
664
+ /** 删除画廊条目。 */
665
+ function deleteGalleryItem(itemId) {
666
+ return fetch(`${BASE}/gallery/${itemId}`, {
667
+ method: "DELETE",
668
+ credentials: "same-origin"
669
+ }).then((res) => res.json());
670
+ }
671
+ /** 图片 URL(<img src> 直连,非 fetch)。 */
672
+ function imageUrl(imageId) {
673
+ return `${BASE}/images/${imageId}`;
674
+ }
675
+ /** 上传原图 URL(缩略图直连,非 fetch)。 */
676
+ function uploadImageUrl(uploadId) {
677
+ return `${BASE}/uploads/${uploadId}`;
678
+ }
679
+ /** 上传本地图片:raw body 流(query 携带原文件名,仅展示用)。 */
680
+ async function uploadImage(file) {
681
+ const query = new URLSearchParams({ name: file.name });
682
+ const res = await fetch(`${BASE}/uploads?${query.toString()}`, {
683
+ method: "POST",
684
+ credentials: "same-origin",
685
+ headers: { "content-type": "application/octet-stream" },
686
+ body: file
687
+ });
688
+ const body = await res.json();
689
+ if (!res.ok) throw new Error(body.message ?? body.error ?? `HTTP ${res.status}`);
690
+ return body;
691
+ }
692
+ /** 上传列表(最新在后;界面自行倒序)。 */
693
+ async function fetchUploads() {
694
+ return getJson(`${BASE}/uploads`);
695
+ }
696
+ /** 凭据状态(describe,永不回传值)。 */
697
+ function fetchCredentialStatus(refName) {
698
+ return getJson(`${BASE}/credentials/${refName}`);
699
+ }
700
+ /** 设置凭据值。 */
701
+ function setCredential(refName, value) {
702
+ return postJson(`${BASE}/credentials/${refName}`, { value });
703
+ }
704
+ /** 删除凭据。 */
705
+ function unsetCredential(refName) {
706
+ return fetch(`${BASE}/credentials/${refName}`, {
707
+ method: "DELETE",
708
+ credentials: "same-origin"
709
+ }).then((res) => res.json());
710
+ }
711
+ /** 协议 registry + 参数目录。 */
712
+ function fetchProviders() {
713
+ return getJson(`${BASE}/providers`);
714
+ }
715
+ /** 预设只读快照(读写走官方 settings RPC)。 */
716
+ function fetchPresets() {
717
+ return getJson(`${BASE}/presets`);
718
+ }
719
+ //#endregion
627
720
  //#region src/credentials.ts
628
721
  /**
629
722
  * 凭据引用名派生与校验:提供商预设 id → IMAGE_STUDIO_PRESET_<ID>。
@@ -935,103 +1028,10 @@ ${extra}
935
1028
  if (spec.value !== void 0) item.value = validateParamValue(key, spec.value);
936
1029
  out[key] = item;
937
1030
  }
938
- return out;
939
- }
940
- //#endregion
941
- //#region src/client/api.ts
942
- /**
943
- * 数据端点通道(前端预留):同源 fetch 封装,类型与 dto.ts 对齐。
944
- * 本次仅交付通道层,不交付调用它的界面。
945
- * @module image-studio/client/api
946
- */
947
- const BASE = "/dsh-plus/image-studio";
948
- /** 提交生图任务。 */
949
- function generate(request) {
950
- return postJson(`${BASE}/generate`, request);
951
- }
952
- /** 批量任务轮询。 */
953
- function fetchTasks() {
954
- return getJson(`${BASE}/tasks`);
955
- }
956
- /** 取消任务。 */
957
- function cancelTask(taskId) {
958
- return postJson(`${BASE}/tasks/${taskId}/cancel`);
959
- }
960
- /** 画廊列表应答形状守卫:items 需为数组(渲染期直接 map)。 */
961
- function isGalleryList(value) {
962
- if (typeof value !== "object" || value === null) return false;
963
- return Array.isArray(value.items);
964
- }
965
- /** 画廊列表。 */
966
- function fetchGallery() {
967
- return getJson(`${BASE}/gallery`, isGalleryList);
968
- }
969
- /** 删除画廊条目。 */
970
- function deleteGalleryItem(itemId) {
971
- return fetch(`${BASE}/gallery/${itemId}`, {
972
- method: "DELETE",
973
- credentials: "same-origin"
974
- }).then((res) => res.json());
975
- }
976
- /** 图片 URL(<img src> 直连,非 fetch)。 */
977
- function imageUrl(imageId) {
978
- return `${BASE}/images/${imageId}`;
979
- }
980
- /** 上传原图 URL(缩略图直连,非 fetch)。 */
981
- function uploadImageUrl(uploadId) {
982
- return `${BASE}/uploads/${uploadId}`;
983
- }
984
- /** 上传本地图片:raw body 流(query 携带原文件名,仅展示用)。 */
985
- async function uploadImage(file) {
986
- const query = new URLSearchParams({ name: file.name });
987
- const res = await fetch(`${BASE}/uploads?${query.toString()}`, {
988
- method: "POST",
989
- credentials: "same-origin",
990
- headers: { "content-type": "application/octet-stream" },
991
- body: file
992
- });
993
- const body = await res.json();
994
- if (!res.ok) throw new Error(body.message ?? body.error ?? `HTTP ${res.status}`);
995
- return body;
996
- }
997
- /** 上传列表(最新在后;界面自行倒序)。 */
998
- async function fetchUploads() {
999
- return getJson(`${BASE}/uploads`);
1000
- }
1001
- /** 凭据状态(describe,永不回传值)。 */
1002
- function fetchCredentialStatus(refName) {
1003
- return getJson(`${BASE}/credentials/${refName}`);
1004
- }
1005
- /** 设置凭据值。 */
1006
- function setCredential(refName, value) {
1007
- return postJson(`${BASE}/credentials/${refName}`, { value });
1008
- }
1009
- /** 删除凭据。 */
1010
- function unsetCredential(refName) {
1011
- return fetch(`${BASE}/credentials/${refName}`, {
1012
- method: "DELETE",
1013
- credentials: "same-origin"
1014
- }).then((res) => res.json());
1015
- }
1016
- /** 协议 registry + 参数目录。 */
1017
- function fetchProviders() {
1018
- return getJson(`${BASE}/providers`);
1019
- }
1020
- /** 预设只读快照(读写走官方 settings RPC)。 */
1021
- function fetchPresets() {
1022
- return getJson(`${BASE}/presets`);
1023
- }
1024
- //#endregion
1025
- //#region src/client/card.tsx
1026
- /**
1027
- * 配置卡片(injectPluginConfigCard 三槽位,legacy key = settings 命名空间,
1028
- * 0.1.6-alpha.2 起为插件页 row/bundle config,view 分发 summary/page):
1029
- * 三组预设(提供商/参数/提示词)+ 高级项(并发/超时/代理/画廊上限)的
1030
- * staged draft 编辑;提供商预设逐条附 API Key 管理(凭据端点,值不回显)。
1031
- * JSON 字段(extraHeaders/paramSpecs)以文本镜像编辑,保存时解析 +
1032
- * validateParamSpecs 本地校验,非法即拦在保存前。
1033
- * @module image-studio/client/card
1034
- */
1031
+ return out;
1032
+ }
1033
+ //#endregion
1034
+ //#region src/client/draft.ts
1035
1035
  /** 配置值 → 编辑草稿。 */
1036
1036
  function draftOf(value) {
1037
1037
  return {
@@ -1103,6 +1103,41 @@ ${extra}
1103
1103
  uploadTtlHours: Math.max(1, Math.floor(Number(draft.uploadTtlHours) || 24))
1104
1104
  };
1105
1105
  }
1106
+ /** 空提供商预设(新增行用;协议取目录首项)。 */
1107
+ function emptyProviderDraft(protocol) {
1108
+ return {
1109
+ id: "",
1110
+ name: "",
1111
+ protocol,
1112
+ baseUrl: "",
1113
+ model: "",
1114
+ extraHeadersText: "{}"
1115
+ };
1116
+ }
1117
+ /** 空参数预设(新增行用;id 由时间戳生成,行序即身份)。 */
1118
+ function emptyParamDraft() {
1119
+ return {
1120
+ id: `param-${Date.now().toString(36)}`,
1121
+ name: "",
1122
+ endpoint: "generation",
1123
+ specsText: "{}"
1124
+ };
1125
+ }
1126
+ /** 空提示词预设(新增行用)。 */
1127
+ function emptyPromptDraft() {
1128
+ return {
1129
+ id: `prompt-${Date.now().toString(36)}`,
1130
+ name: "",
1131
+ text: ""
1132
+ };
1133
+ }
1134
+ //#endregion
1135
+ //#region src/client/preset-sections.tsx
1136
+ /**
1137
+ * 配置卡片的三组预设编辑区:提供商(含逐条 API Key 管理)、提示词、参数。
1138
+ * 从 card.tsx 拆出,避免单组件过长;草稿类型与纯转换在 ./draft.ts。
1139
+ * @module image-studio/client/preset-sections
1140
+ */
1106
1141
  /** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
1107
1142
  function CredentialRow(props) {
1108
1143
  const { t, presetId, disabled } = props;
@@ -1160,22 +1195,343 @@ ${extra}
1160
1195
  ]
1161
1196
  });
1162
1197
  }
1198
+ function ProviderPresetSection(props) {
1199
+ const { t, draft, disabled, protocols, onPatch, onPatchProvider } = props;
1200
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1202
+ className: "imsc-groupTitle",
1203
+ children: t("card.providers")
1204
+ }),
1205
+ draft.providers.map((provider, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1206
+ className: "imsc-presetBox",
1207
+ children: [
1208
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1209
+ className: "imsc-presetHead",
1210
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1211
+ className: "imsc-presetName",
1212
+ children: provider.name || provider.id || "—"
1213
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1214
+ type: "button",
1215
+ className: "imsc-btn imsc-btnGhost",
1216
+ disabled,
1217
+ onClick: () => onPatch({ providers: draft.providers.filter((_, i) => i !== index) }),
1218
+ children: t("common.delete")
1219
+ })]
1220
+ }),
1221
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1222
+ className: "imsc-grid2",
1223
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1224
+ prefix: "imsc",
1225
+ id: `imsc-pid-${index}`,
1226
+ label: t("card.field.id"),
1227
+ hint: t("card.idHint"),
1228
+ value: provider.id,
1229
+ invalid: provider.id !== "" && !isValidPresetId(provider.id),
1230
+ invalidLabel: t("card.idInvalid"),
1231
+ disabled,
1232
+ onEdit: (v) => onPatchProvider(index, { id: v })
1233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1234
+ prefix: "imsc",
1235
+ id: `imsc-pname-${index}`,
1236
+ label: t("card.field.name"),
1237
+ value: provider.name,
1238
+ disabled,
1239
+ onEdit: (v) => onPatchProvider(index, { name: v })
1240
+ })]
1241
+ }),
1242
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1243
+ className: "imsc-grid2",
1244
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1245
+ className: "imsc-field",
1246
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1247
+ className: "imsc-head",
1248
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1249
+ className: "imsc-label",
1250
+ htmlFor: `imsc-pproto-${index}`,
1251
+ children: t("card.field.protocol")
1252
+ })
1253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1254
+ id: `imsc-pproto-${index}`,
1255
+ className: "imsc-select",
1256
+ value: provider.protocol,
1257
+ disabled,
1258
+ onChange: (event) => onPatchProvider(index, { protocol: event.target.value }),
1259
+ children: (protocols.length > 0 ? protocols : [{
1260
+ id: provider.protocol,
1261
+ label: provider.protocol
1262
+ }]).map((protocol) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1263
+ value: protocol.id,
1264
+ children: protocol.label
1265
+ }, protocol.id))
1266
+ })]
1267
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1268
+ prefix: "imsc",
1269
+ id: `imsc-pmodel-${index}`,
1270
+ label: t("card.field.model"),
1271
+ value: provider.model,
1272
+ disabled,
1273
+ onEdit: (v) => onPatchProvider(index, { model: v })
1274
+ })]
1275
+ }),
1276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1277
+ prefix: "imsc",
1278
+ id: `imsc-purl-${index}`,
1279
+ label: t("card.field.baseUrl"),
1280
+ value: provider.baseUrl,
1281
+ disabled,
1282
+ onEdit: (v) => onPatchProvider(index, { baseUrl: v })
1283
+ }),
1284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1285
+ className: "imsc-field",
1286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1287
+ className: "imsc-head",
1288
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1289
+ className: "imsc-label",
1290
+ htmlFor: `imsc-pheaders-${index}`,
1291
+ children: t("card.field.extraHeaders")
1292
+ })
1293
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1294
+ id: `imsc-pheaders-${index}`,
1295
+ className: "imsc-textarea",
1296
+ value: provider.extraHeadersText,
1297
+ disabled,
1298
+ onChange: (event) => onPatchProvider(index, { extraHeadersText: event.target.value })
1299
+ })]
1300
+ }),
1301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
1302
+ t,
1303
+ presetId: provider.id,
1304
+ disabled
1305
+ })
1306
+ ]
1307
+ }, `provider-${index}`)),
1308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1309
+ type: "button",
1310
+ className: "imsc-btn imsc-btnGhost",
1311
+ disabled,
1312
+ onClick: () => onPatch({ providers: [...draft.providers, emptyProviderDraft(protocols[0]?.id ?? "openai-images")] }),
1313
+ children: [
1314
+ t("card.add"),
1315
+ " · ",
1316
+ t("card.providers")
1317
+ ]
1318
+ })
1319
+ ] });
1320
+ }
1321
+ function PromptPresetSection(props) {
1322
+ const { t, draft, disabled, onPatch, onPatchPrompt } = props;
1323
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1325
+ className: "imsc-groupTitle",
1326
+ children: t("card.prompts")
1327
+ }),
1328
+ draft.prompts.map((prompt, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1329
+ className: "imsc-presetBox",
1330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1331
+ className: "imsc-presetHead",
1332
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1333
+ className: "imsc-input",
1334
+ "aria-label": t("card.field.name"),
1335
+ value: prompt.name,
1336
+ disabled,
1337
+ onChange: (event) => onPatchPrompt(index, { name: event.target.value })
1338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1339
+ type: "button",
1340
+ className: "imsc-btn imsc-btnGhost",
1341
+ disabled,
1342
+ onClick: () => onPatch({ prompts: draft.prompts.filter((_, i) => i !== index) }),
1343
+ children: t("common.delete")
1344
+ })]
1345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1346
+ className: "imsc-textarea",
1347
+ "aria-label": t("card.field.text"),
1348
+ value: prompt.text,
1349
+ disabled,
1350
+ onChange: (event) => onPatchPrompt(index, { text: event.target.value })
1351
+ })]
1352
+ }, `prompt-${index}`)),
1353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1354
+ type: "button",
1355
+ className: "imsc-btn imsc-btnGhost",
1356
+ disabled,
1357
+ onClick: () => onPatch({ prompts: [...draft.prompts, emptyPromptDraft()] }),
1358
+ children: [
1359
+ t("card.add"),
1360
+ " · ",
1361
+ t("card.prompts")
1362
+ ]
1363
+ })
1364
+ ] });
1365
+ }
1366
+ function ParamPresetSection(props) {
1367
+ const { t, draft, disabled, onPatch, onPatchParam } = props;
1368
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1369
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1370
+ className: "imsc-groupTitle",
1371
+ children: t("card.params")
1372
+ }),
1373
+ draft.params.map((param, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1374
+ className: "imsc-presetBox",
1375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1376
+ className: "imsc-presetHead",
1377
+ children: [
1378
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1379
+ className: "imsc-input",
1380
+ "aria-label": t("card.field.name"),
1381
+ value: param.name,
1382
+ disabled,
1383
+ onChange: (event) => onPatchParam(index, { name: event.target.value })
1384
+ }),
1385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1386
+ className: "imsc-select",
1387
+ "aria-label": t("card.field.endpoint"),
1388
+ value: param.endpoint,
1389
+ disabled,
1390
+ onChange: (event) => onPatchParam(index, { endpoint: event.target.value }),
1391
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1392
+ value: "generation",
1393
+ children: t("tab.generation")
1394
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1395
+ value: "edit",
1396
+ children: t("tab.edit")
1397
+ })]
1398
+ }),
1399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1400
+ type: "button",
1401
+ className: "imsc-btn imsc-btnGhost",
1402
+ disabled,
1403
+ onClick: () => onPatch({ params: draft.params.filter((_, i) => i !== index) }),
1404
+ children: t("common.delete")
1405
+ })
1406
+ ]
1407
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1408
+ className: "imsc-textarea",
1409
+ "aria-label": t("card.field.paramSpecs"),
1410
+ placeholder: "{ \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } }",
1411
+ value: param.specsText,
1412
+ disabled,
1413
+ onChange: (event) => onPatchParam(index, { specsText: event.target.value })
1414
+ })]
1415
+ }, `param-${index}`)),
1416
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1417
+ type: "button",
1418
+ className: "imsc-btn imsc-btnGhost",
1419
+ disabled,
1420
+ onClick: () => onPatch({ params: [...draft.params, emptyParamDraft()] }),
1421
+ children: [
1422
+ t("card.add"),
1423
+ " · ",
1424
+ t("card.params")
1425
+ ]
1426
+ })
1427
+ ] });
1428
+ }
1429
+ //#endregion
1430
+ //#region src/client/card.tsx
1431
+ /**
1432
+ * 配置卡片(injectPluginConfigCard 两槽位 = 插件页 row/bundle config,
1433
+ * view 分发 summary/page):
1434
+ * 三组预设(提供商/参数/提示词)+ 高级项(并发/超时/代理/画廊上限)的
1435
+ * staged draft 编辑;提供商预设逐条附 API Key 管理(凭据端点,值不回显)。
1436
+ * JSON 字段(extraHeaders/paramSpecs)以文本镜像编辑,保存时解析 +
1437
+ * validateParamSpecs 本地校验,非法即拦在保存前。
1438
+ * 草稿类型与纯转换在 ./draft.ts,三组预设编辑区在 ./preset-sections.tsx。
1439
+ * @module image-studio/client/card
1440
+ */
1441
+ /** 协议目录(服务端 /providers):驱动提供商预设的协议下拉。 */
1442
+ function useProtocols() {
1443
+ const [protocols, setProtocols] = (0, react.useState)([]);
1444
+ (0, react.useEffect)(() => {
1445
+ fetchProviders().then((res) => setProtocols(res.protocols.map((p) => ({
1446
+ id: p.id,
1447
+ label: p.label
1448
+ })))).catch(() => {});
1449
+ }, []);
1450
+ return protocols;
1451
+ }
1452
+ /** 卡片动作:放弃(回到 Host 值并重置基线)与保存。 */
1453
+ function cardActions(options) {
1454
+ const { t } = options;
1455
+ return [{
1456
+ key: "discard",
1457
+ label: t("card.discard"),
1458
+ disabled: !options.dirty || options.saving,
1459
+ onClick: options.onDiscard
1460
+ }, {
1461
+ key: "save",
1462
+ label: t(options.saving ? "common.saving" : "card.save"),
1463
+ variant: "primary",
1464
+ disabled: !options.dirty || options.saving || options.disabled,
1465
+ onClick: options.onSave
1466
+ }];
1467
+ }
1468
+ /** 高级项:并发 / 超时 / 代理 / 画廊上限 / 上传保留时长。 */
1469
+ function AdvancedFields(props) {
1470
+ const { t, draft, disabled, onPatch } = props;
1471
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1472
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1473
+ className: "imsc-groupTitle",
1474
+ children: t("card.advanced")
1475
+ }),
1476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1477
+ className: "imsc-grid2",
1478
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1479
+ prefix: "imsc",
1480
+ id: "imsc-conc",
1481
+ label: t("card.maxConcurrent"),
1482
+ numeric: true,
1483
+ value: draft.maxConcurrent,
1484
+ disabled,
1485
+ onEdit: (v) => onPatch({ maxConcurrent: v })
1486
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1487
+ prefix: "imsc",
1488
+ id: "imsc-timeout",
1489
+ label: t("card.requestTimeoutMs"),
1490
+ numeric: true,
1491
+ value: draft.requestTimeoutMs,
1492
+ disabled,
1493
+ onEdit: (v) => onPatch({ requestTimeoutMs: v })
1494
+ })]
1495
+ }),
1496
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1497
+ className: "imsc-grid2",
1498
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1499
+ prefix: "imsc",
1500
+ id: "imsc-proxy",
1501
+ label: t("card.proxy"),
1502
+ value: draft.proxy,
1503
+ disabled,
1504
+ onEdit: (v) => onPatch({ proxy: v })
1505
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1506
+ prefix: "imsc",
1507
+ id: "imsc-gmax",
1508
+ label: t("card.galleryMaxItems"),
1509
+ numeric: true,
1510
+ value: draft.galleryMaxItems,
1511
+ disabled,
1512
+ onEdit: (v) => onPatch({ galleryMaxItems: v })
1513
+ })]
1514
+ }),
1515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1516
+ prefix: "imsc",
1517
+ id: "imsc-upload-ttl",
1518
+ label: t("card.uploadTtlHours"),
1519
+ numeric: true,
1520
+ value: draft.uploadTtlHours,
1521
+ disabled,
1522
+ onEdit: (v) => onPatch({ uploadTtlHours: v })
1523
+ })
1524
+ ] });
1525
+ }
1163
1526
  function StudioConfigCard(props) {
1164
1527
  const { t, scope, api } = props;
1165
1528
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
1166
1529
  const value = snapshot.value;
1167
- const [open, setOpen] = (0, react.useState)(props.view === "page");
1168
1530
  const [draft, setDraft] = (0, react.useState)(null);
1169
1531
  const [baseline, setBaseline] = (0, react.useState)("");
1170
1532
  const [saving, setSaving] = (0, react.useState)(false);
1171
1533
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
1172
- const [protocols, setProtocols] = (0, react.useState)([]);
1173
- (0, react.useEffect)(() => {
1174
- fetchProviders().then((res) => setProtocols(res.protocols.map((p) => ({
1175
- id: p.id,
1176
- label: p.label
1177
- })))).catch(() => {});
1178
- }, []);
1534
+ const protocols = useProtocols();
1179
1535
  (0, react.useEffect)(() => {
1180
1536
  if (value === void 0 || draft !== null) return;
1181
1537
  const initial = draftOf(value);
@@ -1184,12 +1540,9 @@ ${extra}
1184
1540
  }, [value, draft]);
1185
1541
  const dirty = (0, react.useMemo)(() => draft !== null && JSON.stringify(draft) !== baseline, [draft, baseline]);
1186
1542
  if (props.view === "summary") return t("card.summary");
1187
- if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1188
- className: "imsc-card",
1189
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1190
- className: "imsc-loading",
1191
- children: t("common.loading")
1192
- })
1543
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
1544
+ prefix: "imsc",
1545
+ text: t("common.loading")
1193
1546
  });
1194
1547
  const disabled = !snapshot.writable;
1195
1548
  const onSave = () => {
@@ -1224,24 +1577,29 @@ ${extra}
1224
1577
  });
1225
1578
  setStatus(IDLE_STATUS);
1226
1579
  };
1227
- const patchProvider = (index, next) => patch({ providers: draft.providers.map((p, i) => i === index ? {
1228
- ...p,
1229
- ...next
1230
- } : p) });
1231
- const patchParam = (index, next) => patch({ params: draft.params.map((p, i) => i === index ? {
1232
- ...p,
1233
- ...next
1234
- } : p) });
1235
- const patchPrompt = (index, next) => patch({ prompts: draft.prompts.map((p, i) => i === index ? {
1236
- ...p,
1237
- ...next
1238
- } : p) });
1580
+ const sectionProps = {
1581
+ t,
1582
+ draft,
1583
+ disabled,
1584
+ protocols,
1585
+ onPatch: patch,
1586
+ onPatchProvider: (index, next) => patch({ providers: draft.providers.map((p, i) => i === index ? {
1587
+ ...p,
1588
+ ...next
1589
+ } : p) }),
1590
+ onPatchParam: (index, next) => patch({ params: draft.params.map((p, i) => i === index ? {
1591
+ ...p,
1592
+ ...next
1593
+ } : p) }),
1594
+ onPatchPrompt: (index, next) => patch({ prompts: draft.prompts.map((p, i) => i === index ? {
1595
+ ...p,
1596
+ ...next
1597
+ } : p) })
1598
+ };
1239
1599
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
1240
1600
  prefix: "imsc",
1241
1601
  title: t("card.title"),
1242
1602
  description: t("card.description"),
1243
- open,
1244
- onToggle: setOpen,
1245
1603
  statusBadge: {
1246
1604
  text: draft.providers.length > 0 ? t("card.badgeOn").replace("{n}", String(draft.providers.length)) : t("card.badgeOff"),
1247
1605
  on: draft.providers.length > 0
@@ -1250,307 +1608,28 @@ ${extra}
1250
1608
  dirtyLabel: t("common.unsaved"),
1251
1609
  readOnlyNotice: disabled ? t("card.readOnly") : void 0,
1252
1610
  status,
1253
- actions: [{
1254
- key: "discard",
1255
- label: t("card.discard"),
1256
- disabled: !dirty || saving,
1257
- onClick: () => {
1611
+ actions: cardActions({
1612
+ t,
1613
+ dirty,
1614
+ saving,
1615
+ disabled,
1616
+ onDiscard: () => {
1258
1617
  const restored = draftOf(value);
1259
1618
  setDraft(restored);
1260
1619
  setBaseline(JSON.stringify(restored));
1261
1620
  setStatus(IDLE_STATUS);
1262
- }
1263
- }, {
1264
- key: "save",
1265
- label: t(saving ? "common.saving" : "card.save"),
1266
- variant: "primary",
1267
- disabled: !dirty || saving || disabled,
1268
- onClick: onSave
1269
- }],
1621
+ },
1622
+ onSave
1623
+ }),
1270
1624
  children: [
1271
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1272
- className: "imsc-groupTitle",
1273
- children: t("card.providers")
1274
- }),
1275
- draft.providers.map((provider, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1276
- className: "imsc-presetBox",
1277
- children: [
1278
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1279
- className: "imsc-presetHead",
1280
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1281
- className: "imsc-presetName",
1282
- children: provider.name || provider.id || "—"
1283
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1284
- type: "button",
1285
- className: "imsc-btn imsc-btnGhost",
1286
- disabled,
1287
- onClick: () => patch({ providers: draft.providers.filter((_, i) => i !== index) }),
1288
- children: t("common.delete")
1289
- })]
1290
- }),
1291
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1292
- className: "imsc-grid2",
1293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1294
- prefix: "imsc",
1295
- id: `imsc-pid-${index}`,
1296
- label: t("card.field.id"),
1297
- hint: t("card.idHint"),
1298
- value: provider.id,
1299
- invalid: provider.id !== "" && !isValidPresetId(provider.id),
1300
- invalidLabel: t("card.idInvalid"),
1301
- disabled,
1302
- onEdit: (v) => patchProvider(index, { id: v })
1303
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1304
- prefix: "imsc",
1305
- id: `imsc-pname-${index}`,
1306
- label: t("card.field.name"),
1307
- value: provider.name,
1308
- disabled,
1309
- onEdit: (v) => patchProvider(index, { name: v })
1310
- })]
1311
- }),
1312
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1313
- className: "imsc-grid2",
1314
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1315
- className: "imsc-field",
1316
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1317
- className: "imsc-head",
1318
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1319
- className: "imsc-label",
1320
- htmlFor: `imsc-pproto-${index}`,
1321
- children: t("card.field.protocol")
1322
- })
1323
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1324
- id: `imsc-pproto-${index}`,
1325
- className: "imsc-select",
1326
- value: provider.protocol,
1327
- disabled,
1328
- onChange: (event) => patchProvider(index, { protocol: event.target.value }),
1329
- children: (protocols.length > 0 ? protocols : [{
1330
- id: provider.protocol,
1331
- label: provider.protocol
1332
- }]).map((protocol) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1333
- value: protocol.id,
1334
- children: protocol.label
1335
- }, protocol.id))
1336
- })]
1337
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1338
- prefix: "imsc",
1339
- id: `imsc-pmodel-${index}`,
1340
- label: t("card.field.model"),
1341
- value: provider.model,
1342
- disabled,
1343
- onEdit: (v) => patchProvider(index, { model: v })
1344
- })]
1345
- }),
1346
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1347
- prefix: "imsc",
1348
- id: `imsc-purl-${index}`,
1349
- label: t("card.field.baseUrl"),
1350
- value: provider.baseUrl,
1351
- disabled,
1352
- onEdit: (v) => patchProvider(index, { baseUrl: v })
1353
- }),
1354
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1355
- className: "imsc-field",
1356
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1357
- className: "imsc-head",
1358
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1359
- className: "imsc-label",
1360
- htmlFor: `imsc-pheaders-${index}`,
1361
- children: t("card.field.extraHeaders")
1362
- })
1363
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1364
- id: `imsc-pheaders-${index}`,
1365
- className: "imsc-textarea",
1366
- value: provider.extraHeadersText,
1367
- disabled,
1368
- onChange: (event) => patchProvider(index, { extraHeadersText: event.target.value })
1369
- })]
1370
- }),
1371
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialRow, {
1372
- t,
1373
- presetId: provider.id,
1374
- disabled
1375
- })
1376
- ]
1377
- }, `provider-${index}`)),
1378
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1379
- type: "button",
1380
- className: "imsc-btn imsc-btnGhost",
1381
- disabled,
1382
- onClick: () => patch({ providers: [...draft.providers, {
1383
- id: "",
1384
- name: "",
1385
- protocol: protocols[0]?.id ?? "openai-images",
1386
- baseUrl: "",
1387
- model: "",
1388
- extraHeadersText: "{}"
1389
- }] }),
1390
- children: [
1391
- t("card.add"),
1392
- " · ",
1393
- t("card.providers")
1394
- ]
1395
- }),
1396
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1397
- className: "imsc-groupTitle",
1398
- children: t("card.prompts")
1399
- }),
1400
- draft.prompts.map((prompt, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1401
- className: "imsc-presetBox",
1402
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1403
- className: "imsc-presetHead",
1404
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1405
- className: "imsc-input",
1406
- "aria-label": t("card.field.name"),
1407
- value: prompt.name,
1408
- disabled,
1409
- onChange: (event) => patchPrompt(index, { name: event.target.value })
1410
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1411
- type: "button",
1412
- className: "imsc-btn imsc-btnGhost",
1413
- disabled,
1414
- onClick: () => patch({ prompts: draft.prompts.filter((_, i) => i !== index) }),
1415
- children: t("common.delete")
1416
- })]
1417
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1418
- className: "imsc-textarea",
1419
- "aria-label": t("card.field.text"),
1420
- value: prompt.text,
1421
- disabled,
1422
- onChange: (event) => patchPrompt(index, { text: event.target.value })
1423
- })]
1424
- }, `prompt-${index}`)),
1425
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1426
- type: "button",
1427
- className: "imsc-btn imsc-btnGhost",
1428
- disabled,
1429
- onClick: () => patch({ prompts: [...draft.prompts, {
1430
- id: `prompt-${Date.now().toString(36)}`,
1431
- name: "",
1432
- text: ""
1433
- }] }),
1434
- children: [
1435
- t("card.add"),
1436
- " · ",
1437
- t("card.prompts")
1438
- ]
1439
- }),
1440
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1441
- className: "imsc-groupTitle",
1442
- children: t("card.params")
1443
- }),
1444
- draft.params.map((param, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1445
- className: "imsc-presetBox",
1446
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1447
- className: "imsc-presetHead",
1448
- children: [
1449
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1450
- className: "imsc-input",
1451
- "aria-label": t("card.field.name"),
1452
- value: param.name,
1453
- disabled,
1454
- onChange: (event) => patchParam(index, { name: event.target.value })
1455
- }),
1456
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1457
- className: "imsc-select",
1458
- "aria-label": t("card.field.endpoint"),
1459
- value: param.endpoint,
1460
- disabled,
1461
- onChange: (event) => patchParam(index, { endpoint: event.target.value }),
1462
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1463
- value: "generation",
1464
- children: t("tab.generation")
1465
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1466
- value: "edit",
1467
- children: t("tab.edit")
1468
- })]
1469
- }),
1470
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1471
- type: "button",
1472
- className: "imsc-btn imsc-btnGhost",
1473
- disabled,
1474
- onClick: () => patch({ params: draft.params.filter((_, i) => i !== index) }),
1475
- children: t("common.delete")
1476
- })
1477
- ]
1478
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1479
- className: "imsc-textarea",
1480
- "aria-label": t("card.field.paramSpecs"),
1481
- placeholder: "{ \"size\": { \"enabled\": true, \"value\": \"1024x1024\" } }",
1482
- value: param.specsText,
1483
- disabled,
1484
- onChange: (event) => patchParam(index, { specsText: event.target.value })
1485
- })]
1486
- }, `param-${index}`)),
1487
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1488
- type: "button",
1489
- className: "imsc-btn imsc-btnGhost",
1490
- disabled,
1491
- onClick: () => patch({ params: [...draft.params, {
1492
- id: `param-${Date.now().toString(36)}`,
1493
- name: "",
1494
- endpoint: "generation",
1495
- specsText: "{}"
1496
- }] }),
1497
- children: [
1498
- t("card.add"),
1499
- " · ",
1500
- t("card.params")
1501
- ]
1502
- }),
1503
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1504
- className: "imsc-groupTitle",
1505
- children: t("card.advanced")
1506
- }),
1507
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1508
- className: "imsc-grid2",
1509
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1510
- prefix: "imsc",
1511
- id: "imsc-conc",
1512
- label: t("card.maxConcurrent"),
1513
- numeric: true,
1514
- value: draft.maxConcurrent,
1515
- disabled,
1516
- onEdit: (v) => patch({ maxConcurrent: v })
1517
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1518
- prefix: "imsc",
1519
- id: "imsc-timeout",
1520
- label: t("card.requestTimeoutMs"),
1521
- numeric: true,
1522
- value: draft.requestTimeoutMs,
1523
- disabled,
1524
- onEdit: (v) => patch({ requestTimeoutMs: v })
1525
- })]
1526
- }),
1527
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1528
- className: "imsc-grid2",
1529
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1530
- prefix: "imsc",
1531
- id: "imsc-proxy",
1532
- label: t("card.proxy"),
1533
- value: draft.proxy,
1534
- disabled,
1535
- onEdit: (v) => patch({ proxy: v })
1536
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1537
- prefix: "imsc",
1538
- id: "imsc-gmax",
1539
- label: t("card.galleryMaxItems"),
1540
- numeric: true,
1541
- value: draft.galleryMaxItems,
1542
- disabled,
1543
- onEdit: (v) => patch({ galleryMaxItems: v })
1544
- })]
1545
- }),
1546
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
1547
- prefix: "imsc",
1548
- id: "imsc-upload-ttl",
1549
- label: t("card.uploadTtlHours"),
1550
- numeric: true,
1551
- value: draft.uploadTtlHours,
1625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderPresetSection, { ...sectionProps }),
1626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromptPresetSection, { ...sectionProps }),
1627
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamPresetSection, { ...sectionProps }),
1628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdvancedFields, {
1629
+ t,
1630
+ draft,
1552
1631
  disabled,
1553
- onEdit: (v) => patch({ uploadTtlHours: v })
1632
+ onPatch: patch
1554
1633
  })
1555
1634
  ]
1556
1635
  });
@@ -4001,10 +4080,8 @@ ${extra}
4001
4080
  .imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
4002
4081
  .imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
4003
4082
  .imsc-presetBox {
4004
- border: 1px solid var(--dsw-alias-border-l3);
4005
- border-radius: 10px;
4006
- padding: 10px;
4007
- margin-bottom: 10px;
4083
+ border-top: 1px solid var(--dsw-alias-border-l3);
4084
+ padding: 10px 0 2px;
4008
4085
  display: grid;
4009
4086
  gap: 8px;
4010
4087
  }
@@ -4027,6 +4104,7 @@ ${extra}
4027
4104
  .imsc-credRow { display: flex; align-items: center; gap: 8px; }
4028
4105
  .imsc-credRow .imsc-input { flex: 1; min-width: 0; }
4029
4106
 
4107
+
4030
4108
  /* ── 移动端(≤767px,与 ui-mobile-fit 同断点)── */
4031
4109
  @media (max-width: 767px) {
4032
4110
  .ims-modal {