@jayyuen66/dsh-quality-gate 0.1.2 → 0.1.4

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/client.js CHANGED
@@ -1,19 +1,360 @@
1
1
  // GENERATED by buildClient() in build-client.mjs — edit src/client-entry.ts, then run `node build-client.mjs`.
2
2
  window.__ModuleLoader__.load({
3
- id: '@jayyuen66/dsh-quality-gate',
3
+ id: "@jayyuen66/dsh-quality-gate",
4
4
  factory: (require) => {
5
- var module = { exports: {} };
6
- var exports = module.exports;
5
+ 'use strict';
6
+ const module = { exports: {} };
7
+ let exports = module.exports;
7
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
9
  let react = require("react");
9
- //#region node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
10
- function e(e, t) {
11
- let n = [];
12
- for (let r of Object.keys(e)) JSON.stringify(e[r] ?? null) !== JSON.stringify(t[r] ?? null) && n.push(r);
13
- return n;
10
+ let zod = require("zod");
11
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
12
+ function e$1(t, n, r) {
13
+ if (t == null || n == null) return (t ?? null) === (n ?? null);
14
+ if (Object.is(t, n)) return !0;
15
+ if (typeof t != `object` || typeof n != `object`) return !1;
16
+ if (r.get(t) === n) return !0;
17
+ if (r.set(t, n), Array.isArray(t) || Array.isArray(n)) return !Array.isArray(t) || !Array.isArray(n) || t.length !== n.length ? !1 : t.every((t, i) => e$1(t, n[i], r));
18
+ let i = Object.keys(t), a = Object.keys(n);
19
+ return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e$1(Reflect.get(t, i), Reflect.get(n, i), r));
14
20
  }
15
- //#endregion
16
- //#region src/ui-messages.ts
21
+ function t$1(t, n) {
22
+ let r = [];
23
+ for (let i of Object.keys(t)) e$1(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
24
+ return r;
25
+ }
26
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-ui.js
27
+ const c = `
28
+ .dsu-form{display:flex;flex-direction:column}
29
+ .dsu-section{margin-top:24px;padding-top:24px;border-top:.5px solid var(--dsw-alias-border-l1)}
30
+ .dsu-form>.dsu-section:first-child{margin-top:0;padding-top:0;border-top:0}
31
+ .dsu-sectionTitle{margin:0;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
32
+ .dsu-sectionHint{margin:4px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
33
+ .dsu-statusbar{display:flex;flex-wrap:wrap;gap:4px 8px;margin-top:8px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
34
+ .dsu-field{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,360px);column-gap:32px;align-items:center;padding:12px 0;border-top:.5px solid var(--dsw-alias-border-l2)}
35
+ .dsu-fieldMeta{display:flex;flex-direction:column;gap:4px;min-width:0}
36
+ .dsu-labelRow{display:flex;align-items:center;gap:6px;min-width:0}
37
+ .dsu-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
38
+ .dsu-labelMono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
39
+ .dsu-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
40
+ .dsu-control{min-width:0;display:flex;align-items:center;gap:8px}
41
+ .dsu-fieldWide{grid-template-columns:minmax(0,1fr);align-items:start}
42
+ .dsu-fieldWide .dsu-control{width:100%;max-width:720px;margin-top:2px}
43
+ .dsu-help{display:inline-flex;align-items:center;gap:6px;min-width:0}
44
+ .dsu-helpBtn{appearance:none;border:0;background:transparent;padding:0;width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:help;border-radius:50%;flex:none}
45
+ .dsu-help:hover .dsu-helpBtn,.dsu-helpBtn:focus-visible{color:var(--dsw-alias-label-primary)}
46
+ .dsu-helpBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
47
+ .dsu-tip{position:fixed;z-index:60;box-sizing:border-box;max-width:min(340px,calc(100vw - 16px));max-height:calc(100vh - 16px);overflow:auto;overflow-wrap:anywhere;padding:10px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);box-shadow:0 12px 32px rgb(0 0 0 / 45%);font-size:12px;line-height:1.6;font-weight:400;color:var(--dsw-alias-label-secondary)}
48
+ .dsu-input{width:100%;height:34px;padding:0 12px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
49
+ .dsu-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}
50
+ .dsu-input[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
51
+ .dsu-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
52
+ .dsu-input::placeholder{color:var(--dsw-alias-label-tertiary)}
53
+ .dsu-textarea{height:auto;min-height:96px;padding:8px 12px;line-height:1.6;resize:vertical}
54
+ .dsu-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
55
+ .dsu-numberWrap{display:flex;flex-direction:column;gap:4px;width:100%;min-width:0}
56
+ .dsu-number{display:flex;align-items:center;gap:8px;min-width:0}
57
+ .dsu-number .dsu-input{min-width:0}
58
+ .dsu-unit{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}
59
+ .dsu-error{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary)}
60
+ .dsu-switch{appearance:none;width:36px;height:20px;border-radius:999px;border:0;padding:2px;background:var(--dsw-alias-border-l3);display:flex;align-items:center;cursor:pointer;flex:none;transition:background-color .16s}
61
+ .dsu-knob{width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .16s,background-color .16s}
62
+ .dsu-switch[aria-checked="true"]{background:var(--dsw-alias-label-primary)}
63
+ .dsu-switch[aria-checked="true"] .dsu-knob{transform:translateX(16px);background:var(--dsw-alias-label-primary-foreground)}
64
+ .dsu-switch:disabled{opacity:.4;cursor:not-allowed}
65
+ .dsu-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
66
+ .dsu-selectWrap{position:relative;width:100%;min-width:0}
67
+ .dsu-select{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}
68
+ .dsu-select[aria-expanded="true"]{border-color:var(--dsw-alias-state-business-primary)}
69
+ .dsu-selectValue{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
70
+ .dsu-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
71
+ .dsu-select[aria-expanded="true"] .dsu-chevron{transform:rotate(180deg)}
72
+ .dsu-menu{position:fixed;z-index:70;padding:4px;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:12px;box-shadow:0 12px 32px rgb(0 0 0 / 50%);max-height:280px;overflow:auto}
73
+ .dsu-option{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.4;width:100%;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:8px;cursor:pointer;text-align:left}
74
+ .dsu-option:hover,.dsu-option[data-active="true"]{background:var(--dsw-alias-bg-layer-3)}
75
+ .dsu-option:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
76
+ .dsu-option:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
77
+ .dsu-optionGroup{padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
78
+ .dsu-check{opacity:0;flex:none}
79
+ .dsu-option[aria-selected="true"] .dsu-check{opacity:1}
80
+ .dsu-savebar{display:flex;align-items:center;gap:8px;padding:12px 0;margin-top:24px;border-top:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base)}
81
+ .dsu-savebar[data-dirty="true"]{position:sticky;bottom:0;z-index:2;box-shadow:0 -12px 24px rgb(0 0 0 / 35%)}
82
+ .dsu-btn{appearance:none;border:.5px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:1.5;border-radius:var(--dsw-radius-md);padding:5px 14px;cursor:pointer;transition:background-color .12s,border-color .12s,color .12s,filter .12s}
83
+ .dsu-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l3)}
84
+ .dsu-btn:active:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
85
+ .dsu-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
86
+ .dsu-btn:disabled{opacity:.4;cursor:not-allowed}
87
+ .dsu-btnPrimary{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
88
+ .dsu-btnPrimary:hover:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.9)}
89
+ .dsu-btnPrimary:active:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.84)}
90
+ .dsu-btnDanger{color:var(--dsw-alias-state-error-primary)}
91
+ .dsu-btnDanger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2)}
92
+ .dsu-status{margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}
93
+ .dsu-statusError{color:var(--dsw-alias-state-error-primary)}
94
+ .dsu-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(200px,100%),1fr));gap:0 24px}
95
+ .dsu-grid>.dsu-field{display:flex;flex-direction:column;align-items:stretch;gap:6px;padding:10px 0;border-top:.5px solid var(--dsw-alias-border-l1)}
96
+ .dsu-grid>.dsu-field .dsu-control{width:100%}
97
+ @media (max-width:640px){
98
+ .dsu-field{grid-template-columns:minmax(0,1fr);align-items:start}
99
+ .dsu-control{width:100%;max-width:720px}
100
+ .dsu-section{margin-top:20px;padding-top:20px}
101
+ }
102
+ @media (prefers-reduced-motion:reduce){
103
+ .dsu-switch,.dsu-knob,.dsu-chevron,.dsu-btn{transition:none}
104
+ }
105
+ `;
106
+ function l(e, t = 30) {
107
+ return e.length <= t ? { hint: e } : { detail: e };
108
+ }
109
+ function u(t) {
110
+ return (0, react.createElement)(`div`, { className: `dsu-form` }, t.children);
111
+ }
112
+ function d(t) {
113
+ return (0, react.createElement)(`section`, { className: `dsu-section` }, (0, react.createElement)(`h2`, { className: `dsu-sectionTitle` }, t.title), t.hint === void 0 ? null : (0, react.createElement)(`p`, { className: `dsu-sectionHint` }, t.hint), t.children);
114
+ }
115
+ function m(a) {
116
+ let c = (0, react.useId)(), [l, u] = (0, react.useState)(!1), [d, f] = (0, react.useState)(!1), p = (0, react.useRef)(!1), m = (0, react.useRef)(!1), h = (0, react.useRef)(!1), g = (0, react.useRef)(null), _ = (0, react.useRef)(null), v = (0, react.useRef)(null), y = (0, react.useCallback)(() => {
117
+ v.current !== null && (clearTimeout(v.current), v.current = null);
118
+ }, []), b = (0, react.useCallback)(() => {
119
+ let e = g.current, t = _.current;
120
+ /* v8 ignore next 3 -- place 只在 open 的 layout effect 与挂载期监听器里调用;refs 为空只可能是卸载竞态 */
121
+ if (e === null || t === null) return;
122
+ let n = e.getBoundingClientRect(), r = t.offsetWidth, i = t.offsetHeight, { left: a } = n, o = n.bottom + 8;
123
+ o + i > window.innerHeight - 8 && (o = n.top - i - 8), a + r > window.innerWidth - 8 && (a = window.innerWidth - r - 8);
124
+ let s = Math.max(8, window.innerWidth - r - 8), c = Math.max(8, window.innerHeight - i - 8);
125
+ t.style.left = `${Math.min(Math.max(8, a), s)}px`, t.style.top = `${Math.min(Math.max(8, o), c)}px`;
126
+ }, []), x = (0, react.useCallback)(() => {
127
+ y(), h.current = !1, f(!1), u(!1);
128
+ }, [y]), S = (0, react.useCallback)(() => {
129
+ y(), u(!0);
130
+ }, [y]), C = (0, react.useCallback)(() => {
131
+ y(), !(p.current || m.current || h.current) && (v.current = setTimeout(() => {
132
+ /* v8 ignore next 3 -- 任何重新进入/钉住都会 cancelHide;这次复查只防未来的异步路径 */
133
+ v.current = null, p.current || m.current || h.current || u(!1);
134
+ }, 120));
135
+ }, [y]), w = (0, react.useCallback)((e) => {
136
+ h.current = e, f(e);
137
+ }, []);
138
+ (0, react.useLayoutEffect)(() => {
139
+ if (!l) return () => {};
140
+ b();
141
+ let e = (e) => {
142
+ let { target: t } = e;
143
+ t instanceof Node && _.current?.contains(t) === !0 || (h.current ? b() : x());
144
+ }, t = () => {
145
+ b();
146
+ };
147
+ return window.addEventListener(`scroll`, e, !0), window.addEventListener(`resize`, t), () => {
148
+ window.removeEventListener(`scroll`, e, !0), window.removeEventListener(`resize`, t);
149
+ };
150
+ }, [
151
+ l,
152
+ a.detail,
153
+ b,
154
+ x
155
+ ]), (0, react.useEffect)(() => {
156
+ if (!l) return () => {};
157
+ let e = (e) => {
158
+ e.key === `Escape` && x();
159
+ };
160
+ return document.addEventListener(`keydown`, e), () => {
161
+ document.removeEventListener(`keydown`, e);
162
+ };
163
+ }, [l, x]), (0, react.useEffect)(() => y, [y]);
164
+ let T = (0, react.createElement)(`button`, {
165
+ ref: g,
166
+ type: `button`,
167
+ className: `dsu-helpBtn`,
168
+ "aria-label": `${a.label} 说明`,
169
+ "aria-describedby": c,
170
+ "aria-pressed": d,
171
+ onClick: () => {
172
+ h.current ? x() : (w(!0), S());
173
+ }
174
+ }, (0, react.createElement)(`svg`, {
175
+ width: 12,
176
+ height: 12,
177
+ viewBox: `0 0 14 14`,
178
+ "aria-hidden": !0
179
+ }, (0, react.createElement)(`circle`, {
180
+ cx: 7,
181
+ cy: 7,
182
+ r: 6,
183
+ fill: `none`,
184
+ stroke: `currentColor`,
185
+ strokeWidth: 1.2
186
+ }), (0, react.createElement)(`path`, {
187
+ d: `M7 6.2v4`,
188
+ stroke: `currentColor`,
189
+ strokeWidth: 1.3,
190
+ strokeLinecap: `round`
191
+ }), (0, react.createElement)(`circle`, {
192
+ cx: 7,
193
+ cy: 4.3,
194
+ r: .8,
195
+ fill: `currentColor`
196
+ }))), E = (0, react.createElement)(`div`, {
197
+ ref: _,
198
+ id: c,
199
+ role: `tooltip`,
200
+ className: `dsu-tip`,
201
+ hidden: !l
202
+ }, a.detail), D = a.showLabel === !1 ? null : (0, react.createElement)(`span`, { className: `dsu-label${a.compact === !0 ? ` dsu-labelMono` : ``}` }, a.label);
203
+ return (0, react.createElement)(`span`, {
204
+ className: `dsu-help`,
205
+ onMouseEnter: () => {
206
+ p.current = !0, S();
207
+ },
208
+ onMouseLeave: () => {
209
+ p.current = !1, C();
210
+ },
211
+ onFocus: () => {
212
+ m.current = !0, S();
213
+ },
214
+ onBlur: (e) => {
215
+ let t = e.relatedTarget;
216
+ t instanceof Node && e.currentTarget.contains(t) || (m.current = !1, C());
217
+ }
218
+ }, D, T, E);
219
+ }
220
+ function h(t) {
221
+ let n = `dsu-label${t.compact === !0 ? ` dsu-labelMono` : ``}`, r = t.controlId === void 0 ? (0, react.createElement)(`span`, { className: n }, t.label) : (0, react.createElement)(`label`, {
222
+ className: n,
223
+ htmlFor: t.controlId
224
+ }, t.label);
225
+ return (0, react.createElement)(`div`, { className: `dsu-labelRow` }, r, t.detail === void 0 ? null : (0, react.createElement)(m, {
226
+ label: t.label,
227
+ detail: t.detail,
228
+ showLabel: !1
229
+ }));
230
+ }
231
+ function g(t) {
232
+ let n = t.controlId === void 0 ? void 0 : `${t.controlId}-hint`;
233
+ return (0, react.createElement)(`div`, { className: `dsu-field${t.wide === !0 ? ` dsu-fieldWide` : ``}` }, (0, react.createElement)(`div`, { className: `dsu-fieldMeta` }, (0, react.createElement)(h, {
234
+ label: t.label,
235
+ detail: t.detail,
236
+ compact: t.compact,
237
+ controlId: t.controlId
238
+ }), t.hint === void 0 ? null : (0, react.createElement)(`p`, {
239
+ className: `dsu-hint`,
240
+ id: n
241
+ }, t.hint)), (0, react.createElement)(`div`, { className: `dsu-control` }, t.children));
242
+ }
243
+ function _(t) {
244
+ let n = (0, react.useId)(), i = t.hint === void 0 ? void 0 : `${n}-hint`;
245
+ return (0, react.createElement)(g, {
246
+ ...t,
247
+ controlId: n
248
+ }, (0, react.createElement)(`button`, {
249
+ type: `button`,
250
+ role: `switch`,
251
+ id: n,
252
+ className: `dsu-switch`,
253
+ "aria-checked": t.checked,
254
+ "aria-describedby": i,
255
+ "data-field": t.field,
256
+ disabled: t.disabled === !0,
257
+ onClick: () => {
258
+ t.onChange(!t.checked);
259
+ }
260
+ }, (0, react.createElement)(`span`, { className: `dsu-knob` })));
261
+ }
262
+ function v(t) {
263
+ let i = typeof t.value == `number` && Number.isFinite(t.value) ? String(t.value) : ``, [a, c] = (0, react.useState)(i), [l, u] = (0, react.useState)(!1), d = (0, react.useRef)(void 0), f = (0, react.useId)(), p = (0, react.useId)(), m = [t.hint === void 0 ? void 0 : `${f}-hint`, l ? p : void 0].filter((e) => e !== void 0), h = m.length === 0 ? void 0 : m.join(` `);
264
+ (0, react.useEffect)(() => {
265
+ t.value !== d.current && (d.current = t.value, c(i), u(!1));
266
+ }, [t.value, i]);
267
+ let _ = (e) => {
268
+ let n = t.min ?? -1 / 0, r = t.max ?? 1 / 0;
269
+ if (!Number.isFinite(e) || e < n || e > r) return !1;
270
+ let { step: i } = t;
271
+ if (i === void 0 || i <= 0) return !0;
272
+ let a = (e - (t.min ?? 0)) / i;
273
+ return Math.abs(a - Math.round(a)) < 1e-9;
274
+ }, v = (e) => {
275
+ if (c(e), e === ``) {
276
+ u(!1), d.current = void 0, t.onChange(void 0);
277
+ return;
278
+ }
279
+ let n = Number(e);
280
+ _(n) ? (u(!1), d.current = n, t.onChange(n)) : u(!0);
281
+ }, y = () => {
282
+ a === `` || l || c(String(Number(a)));
283
+ };
284
+ return (0, react.createElement)(g, {
285
+ ...t,
286
+ controlId: f
287
+ }, (0, react.createElement)(`div`, { className: `dsu-numberWrap` }, (0, react.createElement)(`div`, { className: `dsu-number` }, (0, react.createElement)(`input`, {
288
+ className: `dsu-input`,
289
+ type: `number`,
290
+ inputMode: `numeric`,
291
+ id: f,
292
+ value: a,
293
+ min: t.min,
294
+ max: t.max,
295
+ step: t.step,
296
+ placeholder: t.placeholder,
297
+ "aria-invalid": l || void 0,
298
+ "aria-describedby": h,
299
+ "data-field": t.field,
300
+ disabled: t.disabled === !0,
301
+ onChange: (e) => {
302
+ v(e.target.value);
303
+ },
304
+ onBlur: y
305
+ }), t.unit === void 0 ? null : (0, react.createElement)(`span`, { className: `dsu-unit` }, t.unit)), l ? (0, react.createElement)(`p`, {
306
+ className: `dsu-error`,
307
+ id: p
308
+ }, t.invalidHint) : null));
309
+ }
310
+ function y(t) {
311
+ let [i, a] = (0, react.useState)(t.value), [o, c] = (0, react.useState)(!1), l = (0, react.useId)(), u = t.hint === void 0 ? void 0 : `${l}-hint`;
312
+ (0, react.useEffect)(() => {
313
+ o || a(t.value);
314
+ }, [t.value, o]);
315
+ let d = {
316
+ id: l,
317
+ className: `dsu-input${t.multiline === !0 ? ` dsu-textarea` : ``}${t.monospace === !0 ? ` dsu-mono` : ``}`,
318
+ value: i,
319
+ placeholder: t.placeholder,
320
+ "aria-describedby": u,
321
+ "data-field": t.field,
322
+ disabled: t.disabled === !0,
323
+ onFocus: () => {
324
+ c(!0);
325
+ },
326
+ onBlur: () => {
327
+ c(!1);
328
+ },
329
+ onChange: (e) => {
330
+ a(e.target.value), t.onChange(e.target.value);
331
+ }
332
+ };
333
+ return (0, react.createElement)(g, {
334
+ ...t,
335
+ controlId: l
336
+ }, t.multiline === !0 ? (0, react.createElement)(`textarea`, d) : (0, react.createElement)(`input`, d));
337
+ }
338
+ function S(t) {
339
+ let n = !t.writable || t.busy || !t.dirty, r;
340
+ return r = t.error === null ? t.writable ? t.dirty ? t.dirtyLabel : t.cleanLabel : t.readOnlyLabel : t.error, (0, react.createElement)(`div`, {
341
+ className: `dsu-savebar`,
342
+ "data-dirty": t.dirty
343
+ }, (0, react.createElement)(`button`, {
344
+ type: `button`,
345
+ className: `dsu-btn dsu-btnPrimary`,
346
+ "data-field": `save`,
347
+ disabled: n,
348
+ onClick: t.onSave
349
+ }, t.busy ? t.savingLabel : t.saveLabel), (0, react.createElement)(`button`, {
350
+ type: `button`,
351
+ className: `dsu-btn`,
352
+ "data-field": `discard`,
353
+ disabled: n,
354
+ onClick: t.onDiscard
355
+ }, t.revertLabel), (0, react.createElement)(`span`, { className: t.error === null ? `dsu-status` : `dsu-status dsu-statusError` }, r));
356
+ }
357
+ // src/ui-messages.ts
17
358
  const UI_MESSAGES = {
18
359
  zh: {
19
360
  cardTitle: "quality-gate 回合收口质量门禁",
@@ -27,18 +368,23 @@ const UI_MESSAGES = {
27
368
  saveFailed: "保存失败:{message}",
28
369
  saveRejected: "保存被宿主拒绝(配置校验未通过或写入被跳过),修改未生效",
29
370
  outOfRange: "超出允许范围,未保存",
371
+ gateSection: "门禁",
372
+ memorySection: "记忆反馈",
373
+ unitTimes: "次",
374
+ unitSeconds: "秒",
375
+ dirtyCount: "已修改 {count} 项",
30
376
  enabledLabel: "启用质量门禁",
31
377
  enabledHint: "关闭后回合收口不再跑门禁(模型可带病收口)",
32
378
  injectsLabel: "每回合注入配额",
33
379
  injectsHint: "门禁失败每回合最多注入几次修复指令(1-5,默认 2,防跑飞)",
34
- budgetLabel: "门禁预算(毫秒)",
380
+ budgetLabel: "门禁预算",
35
381
  budgetHint: "单次门禁最大耗时(10s-600s,默认 300s;超时按失败注入不静默放行)",
36
382
  memoryLabel: "门禁失败沉淀记忆",
37
- memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回(需同时填下面两项,任一留空即不启用);总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀",
383
+ memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回;总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀。真正决定能否投递的是网关地址能否解析出值(见下面两项)",
38
384
  gatewayUrlLabel: "记忆网关地址",
39
- gatewayUrlHint: "留空 = 不启用记忆反馈。插件不带任何默认地址:本机用户请填自己的网关地址(例如 http://127.0.0.1:8420),没有网关就让它空着",
385
+ gatewayUrlHint: "插件不带任何默认地址:本机用户请填自己的网关地址(例如 http://127.0.0.1:8420),没有网关就让它空着。此项留空时回落到环境变量 TDAI_GATEWAY_URL,两处都空才算未配置、不启用记忆反馈",
40
386
  keyRefLabel: "网关密钥引用",
41
- keyRefHint: "网关鉴权令牌的凭据引用名(环境变量名形态,默认 TDAI_GATEWAY_KEY)。这里存的只是引用名,密钥值由 dsh 凭据服务管理(Web 凭据页或 ~/.dsh/.credentials.yaml 的 refs),不会写进 settings.yaml;清空即恢复默认引用名"
387
+ keyRefHint: "网关鉴权令牌的凭据引用名(环境变量名形态,默认 TDAI_GATEWAY_KEY)。这里存的只是引用名,密钥值由 dsh 凭据服务管理(Web 凭据页或 ~/.dsh/.credentials.yaml 的 refs),不会写进 settings.yaml;清空即恢复默认引用名,而不是停用记忆反馈"
42
388
  },
43
389
  en: {
44
390
  cardTitle: "quality-gate turn-end quality gate",
@@ -52,157 +398,105 @@ const UI_MESSAGES = {
52
398
  saveFailed: "Save failed: {message}",
53
399
  saveRejected: "Save rejected by the host (validation failed or write skipped) — changes not applied",
54
400
  outOfRange: "Out of the allowed range — not saved",
401
+ gateSection: "Gate",
402
+ memorySection: "Memory feedback",
403
+ unitTimes: "times",
404
+ unitSeconds: "seconds",
405
+ dirtyCount: "{count} changed",
55
406
  enabledLabel: "Enable the quality gate",
56
407
  enabledHint: "When off: no gate runs at turn end (the model may close a turn with broken code)",
57
408
  injectsLabel: "Repair injections per turn",
58
409
  injectsHint: "How many repair instructions a failing gate may inject per turn (1-5, default 2 — runaway protection)",
59
- budgetLabel: "Gate budget (milliseconds)",
410
+ budgetLabel: "Gate budget",
60
411
  budgetHint: "Max duration of one gate run (10s-600s, default 300s; a timeout injects a failure instead of passing silently)",
61
412
  memoryLabel: "Persist gate failures to memory",
62
- memoryHint: "Off by default. When on: the evidence of code-level gate failures (command, root, full error text, untruncated) is written into the memory gateway's L0 session layer so later sessions can recall it (both fields below must be filled — leaving either empty disables it); synchronous reporting to the lesson-loop bus is unaffected by this switch. When off, repairs are injected but nothing is persisted",
413
+ memoryHint: "Off by default. When on: the evidence of code-level gate failures (command, root, full error text, untruncated) is written into the memory gateway's L0 session layer so later sessions can recall it; synchronous reporting to the lesson-loop bus is unaffected by this switch. When off, repairs are injected but nothing is persisted. Whether it can actually deliver is decided by the gateway URL resolving to a value (see the two fields below)",
63
414
  gatewayUrlLabel: "Memory gateway URL",
64
- gatewayUrlHint: "Empty = memory feedback disabled. The plugin ships no default address: fill in your own gateway URL (e.g. http://127.0.0.1:8420), or leave it empty if you have no gateway",
415
+ gatewayUrlHint: "The plugin ships no default address: fill in your own gateway URL (e.g. http://127.0.0.1:8420), or leave it empty if you have no gateway. When this is empty the TDAI_GATEWAY_URL environment variable is used instead; only when both are empty is memory feedback unconfigured and disabled",
65
416
  keyRefLabel: "Gateway credential ref",
66
- keyRefHint: "Name of the credential holding the gateway token (POSIX env-var form, default TDAI_GATEWAY_KEY). Only the ref name is stored here — the secret itself is managed by the dsh credentials service (the web credentials page or the refs in ~/.dsh/.credentials.yaml) and never lands in settings.yaml; clearing it restores the default ref name"
417
+ keyRefHint: "Name of the credential holding the gateway token (POSIX env-var form, default TDAI_GATEWAY_KEY). Only the ref name is stored here — the secret itself is managed by the dsh credentials service (the web credentials page or the refs in ~/.dsh/.credentials.yaml) and never lands in settings.yaml; clearing it restores the default ref name rather than disabling memory feedback"
67
418
  }
68
419
  };
69
- //#endregion
70
- //#region src/client-entry.ts
420
+ // src/client-entry.ts
71
421
  const NS = "quality-gate";
72
422
  const BUNDLE_PKG = "@jayyuen66/dsh-quality-gate";
73
- const CARD_CSS = [
74
- ".qgc-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}",
75
- ".qgc-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
76
- ".qgc-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
77
- ".qgc-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
78
- ".qgc-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
79
- ".qgc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
80
- ".qgc-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
81
- ".qgc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
82
- ".qgc-chevron-open{transform:rotate(180deg)}",
83
- ".qgc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:8px 0 12px}",
84
- ".qgc-row{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:12px;padding:9px 0}",
85
- ".qgc-label{font-size:13px;color:var(--dsw-alias-label-primary,inherit)}",
86
- ".qgc-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5;margin-top:2px}",
87
- ".qgc-switch{appearance:none;position:relative;width:34px;height:20px;border-radius:10px;background:var(--dsw-alias-fill-primary,#d8dbe2);transition:background .16s;cursor:pointer;border:0;flex:none}",
88
- ".qgc-switch::after{content:\"\";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1,#fff);transition:left .16s}",
89
- ".qgc-switch-on{background:var(--dsw-alias-brand-primary,#e07856)}",
90
- ".qgc-switch-on::after{left:16px}",
91
- ".qgc-switch:disabled{cursor:not-allowed;opacity:.6}",
92
- ".qgc-input{width:100%;box-sizing:border-box;font:inherit;font-size:12px;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-layer-1,transparent);border:1px solid var(--dsw-alias-border-l2,transparent);border-radius:8px;padding:6px 8px;margin-top:4px}",
93
- ".qgc-savebar{display:flex;gap:8px;align-items:center;padding:10px 0 2px;border-top:1px dashed var(--dsw-alias-border-l2);margin-top:6px;flex-wrap:wrap}",
94
- ".qgc-btn{appearance:none;font:inherit;font-size:12px;cursor:pointer;border-radius:6px;padding:4px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit)}",
95
- ".qgc-btn:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
96
- ".qgc-btn-primary{background:var(--dsw-alias-brand-primary,#e07856);border-color:var(--dsw-alias-brand-primary,#e07856);color:var(--dsw-alias-label-primary-foreground,#fff)}",
97
- ".qgc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#e07856);outline-offset:1px}",
98
- ".qgc-btn:disabled{opacity:.5;cursor:not-allowed}",
99
- ".qgc-dirty{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99)}",
100
- ".qgc-saveerr{font-size:12px;color:#c4483f}"
101
- ].join("\n");
102
- function ToggleRow(props) {
103
- return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint)), (0, react.createElement)("button", {
104
- type: "button",
105
- className: `qgc-switch${props.checked ? " qgc-switch-on" : ""}`,
106
- role: "switch",
107
- "aria-checked": props.checked,
108
- "data-field": props.field,
109
- disabled: props.disabled === true,
110
- onClick: props.onToggle
111
- }));
112
- }
113
- /** 数字输入行:settings 数字字段 ↔ UI 字符串(v7 配置面补齐)。 */
114
- function NumberInputRow(props) {
115
- const num = typeof props.value === "number" && Number.isFinite(props.value) ? String(props.value) : "";
116
- const [text, setText] = (0, react.useState)(num);
117
- const [invalid, setInvalid] = (0, react.useState)(false);
118
- (0, react.useEffect)(() => {
119
- setText(num);
120
- setInvalid(false);
121
- }, [num]);
122
- const invalidHint = props.t("outOfRange");
123
- return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint), (0, react.createElement)("input", {
124
- type: "number",
125
- className: "qgc-input",
126
- value: text,
127
- disabled: props.disabled === true,
128
- "aria-invalid": invalid || void 0,
129
- onChange: (event) => {
130
- const next = event.target.value;
131
- setText(next);
132
- if (next === "") {
133
- setInvalid(false);
134
- props.onClear(props.field);
135
- return;
136
- }
137
- const numeric = Number(next);
138
- if (!Number.isFinite(numeric)) {
139
- setInvalid(true);
140
- return;
141
- }
142
- const min = props.min ?? -Infinity;
143
- const max = props.max ?? Infinity;
144
- if (numeric >= min && numeric <= max) {
145
- setInvalid(false);
146
- props.onChange(props.field, numeric);
147
- } else setInvalid(true);
148
- }
149
- }), invalid ? (0, react.createElement)("div", {
150
- className: "qgc-hint",
151
- style: { color: "var(--dsw-alias-danger, #e5484d)" }
152
- }, invalidHint) : null));
153
- }
154
423
  /**
155
- * 文本输入行:settings 字符串字段 ↔ UI 字符串。
156
- * 清空 = unset(回到 schema 默认值)——对本卡的两个网关字段,默认值就是"留空 = 不启用"。
424
+ * 两个数值字段的闭区间只写在 zod schema 上:host schema(`Schema.natural().min/max`)
425
+ * 与卡片输入框都从这里取。zod 4.6 的 `.minValue`/`.maxValue` 直接读出折叠后的边界,
426
+ * `NumberField` 的 `min/max` 因此不再另抄一份数字。
157
427
  */
158
- function TextInputRow(props) {
159
- const text = typeof props.value === "string" ? props.value : "";
160
- const [draft, setDraft] = (0, react.useState)(text);
161
- (0, react.useEffect)(() => {
162
- setDraft(text);
163
- }, [text]);
164
- return (0, react.createElement)("div", { className: "qgc-row" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "qgc-label" }, props.label), (0, react.createElement)("div", { className: "qgc-hint" }, props.hint), (0, react.createElement)("input", {
165
- type: "text",
166
- className: "qgc-input",
167
- value: draft,
168
- placeholder: props.placeholder ?? "",
169
- disabled: props.disabled === true,
170
- onChange: (event) => {
171
- const next = event.target.value;
172
- setDraft(next);
173
- if (next.trim() === "") {
174
- props.onClear(props.field);
175
- return;
176
- }
177
- props.onChange(props.field, next.trim());
178
- }
179
- })));
428
+ const MAX_INJECTS_VALUE = zod.z.int().min(1).max(5);
429
+ const GATE_BUDGET_MS_VALUE = zod.z.int().min(1e4).max(6e5);
430
+ /** 把 zod 折叠后的显式边界摊成可选 props(null = 该方向没声明,不伪造一个上限)。 */
431
+ function boundsOf(schema) {
432
+ const bounds = {};
433
+ if (schema.minValue !== null) bounds.min = schema.minValue;
434
+ if (schema.maxValue !== null) bounds.max = schema.maxValue;
435
+ return bounds;
180
436
  }
181
- function SaveBar(props) {
182
- const { t } = props;
183
- const dis = !props.writable || props.busy;
184
- let dirtyText;
185
- if (props.writable) dirtyText = props.dirty ? t("statusDirty") : t("statusClean");
186
- else dirtyText = t("statusReadOnly");
187
- return (0, react.createElement)("div", { className: "qgc-savebar" }, (0, react.createElement)("button", {
188
- type: "button",
189
- className: "qgc-btn qgc-btn-primary",
190
- "data-field": "save",
191
- disabled: dis || !props.dirty,
192
- onClick: props.onSave
193
- }, props.busy ? t("saving") : t("save")), (0, react.createElement)("button", {
194
- type: "button",
195
- className: "qgc-btn",
196
- "data-field": "discard",
197
- disabled: dis || !props.dirty,
198
- onClick: props.onDiscard
199
- }, t("revert")), props.error === null ? (0, react.createElement)("span", { className: "qgc-dirty" }, dirtyText) : (0, react.createElement)("span", { className: "qgc-saveerr" }, props.error));
437
+ const MAX_INJECTS_BOUNDS = boundsOf(MAX_INJECTS_VALUE);
438
+ const GATE_BUDGET_MS_BOUNDS = boundsOf(GATE_BUDGET_MS_VALUE);
439
+ /** 两个数值 schema 的对外投影:测试用 `.minValue`/`.maxValue` 与 `z.toJSONSchema` 钉边界。 */
440
+ const CARD_NUMBER_SCHEMAS = {
441
+ maxInjectsPerTurn: MAX_INJECTS_VALUE,
442
+ gateBudgetMs: GATE_BUDGET_MS_VALUE
443
+ };
444
+ /** 可选整数:越界/小数/非有限值折成 undefined,只丢那一格而不是整份快照。
445
+ * `.validate()` 只回答合法性、不建 ZodError,失败路径比 `.safeParse().success` 短。
446
+ * 不引 `z.compile`:编译器给客户端 bundle 增约 28 KB,且 CSP 环境会拒绝 `new Function`;
447
+ * 卡片每帧只解析一枚快照对象,收益抵不上这两条代价。 */
448
+ const MAX_INJECTS_SCHEMA = zod.z.preprocess((value) => typeof value === "number" && MAX_INJECTS_VALUE.validate(value) ? value : void 0, MAX_INJECTS_VALUE.optional());
449
+ const GATE_BUDGET_MS_SCHEMA = zod.z.preprocess((value) => typeof value === "number" && GATE_BUDGET_MS_VALUE.validate(value) ? value : void 0, GATE_BUDGET_MS_VALUE.optional());
450
+ /** 卡片可编辑字段的值面。逐字段先把错误类型折成 undefined:宿主给了坏形状时只丢掉
451
+ * 那一格,其余字段照常渲染;整份快照不是对象时才整体回落到空表。 */
452
+ const optionalBoolean = zod.z.preprocess((value) => typeof value === "boolean" ? value : void 0, zod.z.boolean().optional());
453
+ const optionalString = zod.z.preprocess((value) => typeof value === "string" ? value : void 0, zod.z.string().optional());
454
+ /** 网关地址是 HTTP(S) URL;空串与坏文本折成未设置。本地回环/端口由 `z.url` 放行。 */
455
+ const HTTP_URL_SCHEMA = zod.z.url({ protocol: /^https?$/u });
456
+ const optionalHttpUrl = zod.z.preprocess((value) => {
457
+ let text;
458
+ if (typeof value === "string") {
459
+ const trimmed = value.trim();
460
+ if (trimmed !== "") text = trimmed;
461
+ }
462
+ return text;
463
+ }, HTTP_URL_SCHEMA.optional());
464
+ const CARD_VALUES = zod.z.object({
465
+ enabled: optionalBoolean,
466
+ maxInjectsPerTurn: MAX_INJECTS_SCHEMA,
467
+ gateBudgetMs: GATE_BUDGET_MS_SCHEMA,
468
+ memoryFeedback: optionalBoolean,
469
+ memoryGatewayUrl: optionalHttpUrl,
470
+ memoryGatewayKeyRef: optionalString
471
+ });
472
+ /** 草稿面仍按宽 string 读:`TextField` 没有 invalid 态,URL 收口只放在快照投影上,
473
+ * 用户输入中的半截地址不会被当场折成空串。 */
474
+ const CARD_FIELD_SCHEMAS = {
475
+ enabled: optionalBoolean,
476
+ maxInjectsPerTurn: MAX_INJECTS_SCHEMA,
477
+ gateBudgetMs: GATE_BUDGET_MS_SCHEMA,
478
+ memoryFeedback: optionalBoolean,
479
+ memoryGatewayUrl: optionalString,
480
+ memoryGatewayKeyRef: optionalString
481
+ };
482
+ /** 快照值投影:整份一次解析;每个字段的坏形状与越界值各自折成 undefined,其余字段照常渲染。 */
483
+ function projectCardValues(raw) {
484
+ const parsed = CARD_VALUES.safeParse(raw);
485
+ return parsed.success ? parsed.data : {};
486
+ }
487
+ /** 值语义相等(与 shared 的 diffTouched 同口径):表单值是标量与数组,引用相等会把等值数组判成改动。 */
488
+ function sameValue(left, right) {
489
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
200
490
  }
201
491
  /**
202
492
  * 逐键 await 写入并**消费受理位**(0.1.7 的 set/unset 回 Promise<boolean>:
203
493
  * true=宿主受理;false=拒绝或写入被跳过(官方语义:"refuses stale revisions";
204
494
  * 传输失败才 reject)。false 或 rejection 都**不清 touched**——否则宿主拒写时
205
495
  * 卡片仍显示"已保存",用户修改被静默丢弃。
496
+ *
497
+ * 成功后也只清**本次真正写下的那些键**:保存期间只有保存按钮被禁用,输入框仍可编辑,
498
+ * 期间产生的新改动若一并清掉就是静默丢失。这里的判据是「当前草稿值仍等于本次快照值」——
499
+ * 相等说明这条已落到宿主、可以从草稿层移除;不等说明用户又改过,保留下来重新变脏。
206
500
  */
207
501
  async function writeTouched(channels) {
208
502
  const { t, set, unset, keys, touched, setBusy, setSaveError, setTouched } = channels;
@@ -216,164 +510,131 @@ async function writeTouched(channels) {
216
510
  }
217
511
  }
218
512
  setBusy(false);
219
- setTouched({});
513
+ setTouched((prev) => {
514
+ const next = {};
515
+ for (const [key, now] of Object.entries(prev)) if (!(keys.includes(key) && sameValue(now, touched[key]))) next[key] = now;
516
+ return next;
517
+ });
220
518
  } catch (error) {
221
519
  console.error("[quality-gate] save write failed:", error);
222
520
  setBusy(false);
223
521
  setSaveError(t("saveFailed", { message: String(error instanceof Error ? error.message : error) }));
224
522
  }
225
523
  }
226
- /** 头部:渲染子树扁平化(规避 max-nested-calls)——DOM 各层拆成独立 createElement 元素
227
- * const,逐层用变量组合,避免 createElement 回调逐层内嵌。 */
228
- function buildCardHeader(props) {
229
- const { t, open, onToggle } = props;
230
- const nameEl = (0, react.createElement)("div", { className: "qgc-name" }, t("cardTitle"));
231
- const descEl = (0, react.createElement)("div", { className: "qgc-desc" }, t("cardDescription"));
232
- const headDiv = (0, react.createElement)("div", { className: "qgc-head" }, nameEl, descEl);
233
- const chevronPathData = "M3 5l4 4 4-4";
234
- const chevronPath = (0, react.createElement)("path", {
235
- ["d"]: chevronPathData,
236
- fill: "none",
237
- stroke: "currentColor",
238
- strokeWidth: 1.5,
239
- strokeLinecap: "round",
240
- strokeLinejoin: "round"
241
- });
242
- const chevron = (0, react.createElement)("svg", {
243
- width: 14,
244
- height: 14,
245
- viewBox: "0 0 14 14",
246
- "aria-hidden": true,
247
- className: `qgc-chevron${open ? " qgc-chevron-open" : ""}`
248
- }, chevronPath);
249
- return (0, react.createElement)("button", {
250
- type: "button",
251
- className: "qgc-header",
252
- "aria-expanded": open,
253
- onClick: onToggle
254
- }, headDiv, chevron);
255
- }
256
- /** 展开态的 `ul` 本体:行元素仍按从前的顺序逐个传给同一个 createElement(不折成数组
257
- * 子节点,保持宿主侧 children 形状一致)。 */
258
- function buildCardBody(props) {
259
- const { t, writable, eff, setField, clearField, dirty, busy, saveError, onSave, onDiscard } = props;
260
- const rowEnabled = (0, react.createElement)(ToggleRow, {
524
+ /** 门禁小节:总开关 + 注入配额 + 预算(毫秒按秒显示)。 */
525
+ function buildGateSection(props) {
526
+ const { t, writable, eff, setField } = props;
527
+ const maxInjects = eff("maxInjectsPerTurn");
528
+ const budgetMs = eff("gateBudgetMs");
529
+ const budgetSeconds = budgetMs === void 0 ? void 0 : budgetMs / 1e3;
530
+ const budgetMinSeconds = (GATE_BUDGET_MS_BOUNDS.min ?? 0) / 1e3;
531
+ const budgetMaxSeconds = (GATE_BUDGET_MS_BOUNDS.max ?? 0) / 1e3;
532
+ const enabled = (0, react.createElement)(_, {
261
533
  label: t("enabledLabel"),
262
- hint: t("enabledHint"),
534
+ ...l(t("enabledHint")),
263
535
  field: "enabled",
264
- checked: eff("enabled") !== false,
536
+ checked: eff("enabled") ?? true,
265
537
  disabled: !writable,
266
- onToggle: () => {
267
- setField("enabled", eff("enabled") === false);
538
+ onChange: (next) => {
539
+ setField("enabled", next);
268
540
  }
269
541
  });
270
- const rowInjects = (0, react.createElement)(NumberInputRow, {
271
- t,
542
+ const injects = (0, react.createElement)(v, {
272
543
  label: t("injectsLabel"),
273
- hint: t("injectsHint"),
544
+ ...l(t("injectsHint")),
274
545
  field: "maxInjectsPerTurn",
275
- value: eff("maxInjectsPerTurn"),
546
+ value: maxInjects,
547
+ ...MAX_INJECTS_BOUNDS,
548
+ step: 1,
549
+ unit: t("unitTimes"),
550
+ invalidHint: t("outOfRange"),
276
551
  disabled: !writable,
277
- onChange: (field, val) => {
278
- setField(field, val);
279
- },
280
- onClear: (field) => {
281
- clearField(field);
282
- },
283
- min: 1,
284
- max: 5
552
+ onChange: (next) => {
553
+ setField("maxInjectsPerTurn", next);
554
+ }
285
555
  });
286
- const rowBudget = (0, react.createElement)(NumberInputRow, {
287
- t,
556
+ const budget = (0, react.createElement)(v, {
288
557
  label: t("budgetLabel"),
289
- hint: t("budgetHint"),
558
+ ...l(t("budgetHint")),
290
559
  field: "gateBudgetMs",
291
- value: eff("gateBudgetMs"),
560
+ value: budgetSeconds,
561
+ min: budgetMinSeconds,
562
+ max: budgetMaxSeconds,
563
+ step: 1,
564
+ unit: t("unitSeconds"),
565
+ invalidHint: t("outOfRange"),
292
566
  disabled: !writable,
293
- onChange: (field, val) => {
294
- setField(field, val);
295
- },
296
- onClear: (field) => {
297
- clearField(field);
298
- },
299
- min: 1e4,
300
- max: 6e5
567
+ onChange: (next) => {
568
+ setField("gateBudgetMs", next === void 0 ? void 0 : next * 1e3);
569
+ }
301
570
  });
302
- const rowMemory = (0, react.createElement)(ToggleRow, {
571
+ return (0, react.createElement)(d, { title: t("gateSection") }, enabled, injects, budget);
572
+ }
573
+ /** 记忆反馈小节:开关 + 网关地址 + 密钥引用。 */
574
+ function buildMemorySection(props) {
575
+ const { t, writable, eff, setField } = props;
576
+ const gatewayUrl = eff("memoryGatewayUrl") ?? "";
577
+ const keyRef = eff("memoryGatewayKeyRef") ?? "";
578
+ const memory = (0, react.createElement)(_, {
303
579
  label: t("memoryLabel"),
304
- hint: t("memoryHint"),
580
+ ...l(t("memoryHint")),
305
581
  field: "memoryFeedback",
306
582
  checked: eff("memoryFeedback") === true,
307
583
  disabled: !writable,
308
- onToggle: () => {
309
- setField("memoryFeedback", eff("memoryFeedback") !== true);
584
+ onChange: (next) => {
585
+ setField("memoryFeedback", next);
310
586
  }
311
587
  });
312
- const rowGatewayUrl = (0, react.createElement)(TextInputRow, {
588
+ const url = (0, react.createElement)(y, {
313
589
  label: t("gatewayUrlLabel"),
314
- hint: t("gatewayUrlHint"),
590
+ ...l(t("gatewayUrlHint")),
315
591
  field: "memoryGatewayUrl",
316
- value: eff("memoryGatewayUrl"),
592
+ value: gatewayUrl,
317
593
  placeholder: "http://127.0.0.1:8420",
594
+ monospace: true,
318
595
  disabled: !writable,
319
- onChange: (field, val) => {
320
- setField(field, val);
321
- },
322
- onClear: (field) => {
323
- clearField(field);
596
+ onChange: (next) => {
597
+ setField("memoryGatewayUrl", next.trim() === "" ? void 0 : next.trim());
324
598
  }
325
599
  });
326
- const rowGatewayKeyRef = (0, react.createElement)(TextInputRow, {
600
+ const keyRefField = (0, react.createElement)(y, {
327
601
  label: t("keyRefLabel"),
328
- hint: t("keyRefHint"),
602
+ ...l(t("keyRefHint")),
329
603
  field: "memoryGatewayKeyRef",
330
- value: eff("memoryGatewayKeyRef"),
604
+ value: keyRef,
331
605
  placeholder: "TDAI_GATEWAY_KEY",
606
+ monospace: true,
332
607
  disabled: !writable,
333
- onChange: (field, val) => {
334
- setField(field, val);
335
- },
336
- onClear: (field) => {
337
- clearField(field);
608
+ onChange: (next) => {
609
+ setField("memoryGatewayKeyRef", next.trim() === "" ? void 0 : next.trim());
338
610
  }
339
611
  });
340
- const rowSave = (0, react.createElement)(SaveBar, {
341
- t,
342
- dirty,
343
- writable,
344
- busy,
345
- error: saveError,
346
- onSave,
347
- onDiscard
348
- });
349
- return (0, react.createElement)("ul", { className: "qgc-body" }, rowEnabled, rowInjects, rowBudget, rowMemory, rowGatewayUrl, rowGatewayKeyRef, rowSave);
612
+ return (0, react.createElement)(d, { title: t("memorySection") }, memory, url, keyRefField);
350
613
  }
351
614
  function DgCard(props) {
352
615
  const { t } = props;
353
- const [open, setOpen] = (0, react.useState)(false);
354
616
  const snap = props.useCard((cardSnap) => cardSnap);
355
- const { value } = snap;
617
+ const value = projectCardValues(snap.value);
356
618
  const writable = snap.status === "ready" && snap.writable;
357
619
  const [touched, setTouched] = (0, react.useState)({});
358
620
  const [busy, setBusy] = (0, react.useState)(false);
359
621
  const [saveError, setSaveError] = (0, react.useState)(null);
360
- /** 渲染取值:touched 优先,快照兜底(未触碰字段实时跟随外部快照)。 */
361
- const eff = (field) => field in touched ? touched[field] : value[field];
362
- const dirty = e(touched, value).length > 0;
622
+ /** 渲染取值:touched 优先(经 zod 收窄),快照兜底(未触碰字段实时跟随外部快照)。 */
623
+ const eff = (field) => {
624
+ if (!(field in touched)) return value[field];
625
+ const parsedTouched = CARD_FIELD_SCHEMAS[field].safeParse(touched[field]);
626
+ return parsedTouched.success ? parsedTouched.data : void 0;
627
+ };
628
+ const dirtyKeys = t$1(touched, value);
629
+ const dirty = dirtyKeys.length > 0;
363
630
  const setField = (field, val) => {
364
631
  setTouched((prev) => ({
365
632
  ...prev,
366
633
  [field]: val
367
634
  }));
368
635
  };
369
- const clearField = (field) => {
370
- setTouched((prev) => ({
371
- ...prev,
372
- [field]: void 0
373
- }));
374
- };
375
636
  const save = () => {
376
- const keys = e(touched, value);
637
+ const keys = t$1(touched, value);
377
638
  if (keys.length === 0) return;
378
639
  setBusy(true);
379
640
  setSaveError(null);
@@ -392,26 +653,29 @@ function DgCard(props) {
392
653
  setTouched({});
393
654
  setSaveError(null);
394
655
  };
395
- const header = buildCardHeader({
396
- t,
397
- open,
398
- onToggle: () => {
399
- setOpen(!open);
400
- }
401
- });
402
- const body = open ? buildCardBody({
656
+ const inputs = {
403
657
  t,
404
658
  writable,
405
659
  eff,
406
- setField,
407
- clearField,
660
+ setField
661
+ };
662
+ const gate = buildGateSection(inputs);
663
+ const memory = buildMemorySection(inputs);
664
+ const saveBar = (0, react.createElement)(S, {
408
665
  dirty,
409
666
  busy,
410
- saveError,
667
+ writable,
668
+ error: saveError,
669
+ saveLabel: t("save"),
670
+ savingLabel: t("saving"),
671
+ revertLabel: t("revert"),
672
+ dirtyLabel: t("dirtyCount", { count: dirtyKeys.length }),
673
+ cleanLabel: t("statusClean"),
674
+ readOnlyLabel: t("statusReadOnly"),
411
675
  onSave: save,
412
676
  onDiscard: discard
413
- }) : null;
414
- return (0, react.createElement)("li", { className: `qgc-card${open ? " qgc-card-open" : ""}` }, header, body);
677
+ });
678
+ return (0, react.createElement)(u, null, gate, memory, saveBar);
415
679
  }
416
680
  function cardStore(scope) {
417
681
  let cachedSnap = null;
@@ -447,8 +711,8 @@ const inject = [
447
711
  function apply(ctx) {
448
712
  ctx.effect(() => {
449
713
  const tag = document.createElement("style");
450
- tag.id = "quality-gate-card-css";
451
- tag.textContent = CARD_CSS;
714
+ tag.id = "dsh-card-ui-css-quality-gate";
715
+ tag.textContent = c;
452
716
  document.head.append(tag);
453
717
  return () => {
454
718
  tag.remove();
@@ -471,9 +735,11 @@ function apply(ctx) {
471
735
  }, DgCard);
472
736
  });
473
737
  }
474
- //#endregion
738
+ exports.CARD_NUMBER_SCHEMAS = CARD_NUMBER_SCHEMAS;
475
739
  exports.apply = apply;
476
740
  exports.inject = inject;
741
+ exports.projectCardValues = projectCardValues;
742
+ exports.writeTouched = writeTouched;
477
743
  return module.exports;
478
744
  },
479
745
  });