@jayyuen66/dsh-quality-gate 0.1.3 → 0.1.5

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
@@ -7,209 +7,562 @@ window.__ModuleLoader__.load({
7
7
  let exports = module.exports;
8
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  let react = require("react");
10
+ let zod = require("zod");
10
11
  // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
11
- function e(t, n, r) {
12
+ function e$1(t, n, r) {
12
13
  if (t == null || n == null) return (t ?? null) === (n ?? null);
13
14
  if (Object.is(t, n)) return !0;
14
15
  if (typeof t != `object` || typeof n != `object`) return !1;
15
16
  if (r.get(t) === n) return !0;
16
- 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(t, n[i], r));
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));
17
18
  let i = Object.keys(t), a = Object.keys(n);
18
- return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e(Reflect.get(t, i), Reflect.get(n, i), r));
19
+ return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e$1(Reflect.get(t, i), Reflect.get(n, i), r));
19
20
  }
20
- function t(t, n) {
21
+ function t$1(t, n) {
21
22
  let r = [];
22
- for (let i of Object.keys(t)) e(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
23
+ for (let i of Object.keys(t)) e$1(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
23
24
  return r;
24
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
+ }
25
357
  // src/ui-messages.ts
26
358
  const UI_MESSAGES = {
27
359
  zh: {
28
- cardTitle: "quality-gate 回合收口质量门禁",
29
- cardDescription: "回合收口质量门禁(ECC stop-format-typecheck 移植):本回合编辑过代码文件时,收口前自动跑项目门禁(根 package.json 声明了 check 才用 pnpm check,否则 cargo check / ruff / tsc),失败自动开新回合注入修复指令;失败事实同步 lesson-loop 总线,记忆网关沉淀默认关闭",
30
360
  statusReadOnly: "当前作用域只读",
31
- statusDirty: "有未保存的修改,点「保存」生效",
32
361
  statusClean: "无未保存的修改",
33
362
  save: "保存",
34
363
  saving: "保存中…",
35
364
  revert: "撤销",
36
365
  saveFailed: "保存失败:{message}",
37
366
  saveRejected: "保存被宿主拒绝(配置校验未通过或写入被跳过),修改未生效",
367
+ invalidGatewayUrl: "网关地址不接受:不能带用户名/口令;远端地址必须是 https,明文 http 只允许本机回环",
368
+ invalidKeyRef: "凭据引用名不接受:只能由字母、数字、下划线组成,且不能以数字开头",
38
369
  outOfRange: "超出允许范围,未保存",
370
+ gateSection: "门禁",
371
+ memorySection: "记忆反馈",
372
+ unitTimes: "次",
373
+ unitSeconds: "秒",
374
+ dirtyCount: "已修改 {count} 项",
39
375
  enabledLabel: "启用质量门禁",
40
376
  enabledHint: "关闭后回合收口不再跑门禁(模型可带病收口)",
41
- injectsLabel: "每回合注入配额",
42
- injectsHint: "门禁失败每回合最多注入几次修复指令(1-5,默认 2,防跑飞)",
43
- budgetLabel: "门禁预算(毫秒)",
44
- budgetHint: "单次门禁最大耗时(10s-600s,默认 300s;超时按失败注入不静默放行)",
377
+ injectsLabel: "每会话注入配额",
378
+ injectsHint: "门禁失败后最多注入几次修复指令;配额按会话累计,门禁通过后重置",
379
+ budgetLabel: "门禁总预算",
380
+ budgetHint: "一次回合内所有门禁共享的总耗时预算(多根均分);超时按「未能执行」注入,不静默放行",
45
381
  memoryLabel: "门禁失败沉淀记忆",
46
- memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回;总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀。真正决定能否投递的是网关地址能否解析出值(见下面两项)",
382
+ memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回;总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀。投递要求网关地址与凭据引用都可用(见下面两项)",
47
383
  gatewayUrlLabel: "记忆网关地址",
48
- gatewayUrlHint: "插件不带任何默认地址:本机用户请填自己的网关地址(例如 http://127.0.0.1:8420),没有网关就让它空着。此项留空时回落到环境变量 TDAI_GATEWAY_URL,两处都空才算未配置、不启用记忆反馈",
384
+ gatewayUrlHint: "插件不带任何默认地址:本机用户请填自己的网关地址(例如 http://127.0.0.1:8420),没有网关就让它空着。此项留空时回落到环境变量 TDAI_GATEWAY_URL,两处都空才算未配置、不启用记忆反馈。远端地址必须 https,明文 http 只允许本机回环(127.0.0.0/8、::1、localhost)",
49
385
  keyRefLabel: "网关密钥引用",
50
386
  keyRefHint: "网关鉴权令牌的凭据引用名(环境变量名形态,默认 TDAI_GATEWAY_KEY)。这里存的只是引用名,密钥值由 dsh 凭据服务管理(Web 凭据页或 ~/.dsh/.credentials.yaml 的 refs),不会写进 settings.yaml;清空即恢复默认引用名,而不是停用记忆反馈"
51
387
  },
52
388
  en: {
53
- cardTitle: "quality-gate turn-end quality gate",
54
- cardDescription: "Turn-end quality gate (ported from ECC stop-format-typecheck): when this turn edited code files, the project gate runs automatically before the turn closes (pnpm check only if the root package.json declares a check script, otherwise cargo check / ruff / tsc); a failure opens a new turn with repair instructions. Failure facts are also reported to the lesson-loop bus; memory-gateway persistence is off by default",
55
389
  statusReadOnly: "This scope is read-only",
56
- statusDirty: "Unsaved changes — press Save to apply",
57
390
  statusClean: "No unsaved changes",
58
391
  save: "Save",
59
392
  saving: "Saving…",
60
393
  revert: "Revert",
61
394
  saveFailed: "Save failed: {message}",
62
395
  saveRejected: "Save rejected by the host (validation failed or write skipped) — changes not applied",
396
+ invalidGatewayUrl: "Gateway URL rejected: it must not embed credentials; remote addresses must use https, plain http is only allowed for loopback",
397
+ invalidKeyRef: "Credential reference rejected: use letters, digits and underscores only, and do not start with a digit",
63
398
  outOfRange: "Out of the allowed range — not saved",
399
+ gateSection: "Gate",
400
+ memorySection: "Memory feedback",
401
+ unitTimes: "times",
402
+ unitSeconds: "seconds",
403
+ dirtyCount: "{count} changed",
64
404
  enabledLabel: "Enable the quality gate",
65
405
  enabledHint: "When off: no gate runs at turn end (the model may close a turn with broken code)",
66
- injectsLabel: "Repair injections per turn",
67
- injectsHint: "How many repair instructions a failing gate may inject per turn (1-5, default 2 — runaway protection)",
68
- budgetLabel: "Gate budget (milliseconds)",
69
- budgetHint: "Max duration of one gate run (10s-600s, default 300s; a timeout injects a failure instead of passing silently)",
406
+ injectsLabel: "Repair injections per session",
407
+ injectsHint: "How many repair instructions a failing gate may inject; the quota accumulates across turns in one session and resets after a clean gate pass",
408
+ budgetLabel: "Gate total budget",
409
+ budgetHint: "Shared time budget for all gates in one turn (split across project roots); a timeout is injected as \"could not run\", never as a silent pass",
70
410
  memoryLabel: "Persist gate failures to memory",
71
- 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)",
411
+ 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. Delivery requires both the gateway URL and the credential ref to be usable (see the two fields below)",
72
412
  gatewayUrlLabel: "Memory gateway URL",
73
- 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",
413
+ 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. Remote addresses must use https; plain http is only allowed for loopback (127.0.0.0/8, ::1, localhost)",
74
414
  keyRefLabel: "Gateway credential ref",
75
415
  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"
76
416
  }
77
417
  };
418
+ // lib/credential-url.ts
419
+ /** IPv4 单段。 */
420
+ const IPV4_OCTET = /^\d{1,3}$/u;
421
+ /** IPv4-mapped IPv6 的字面前缀。 */
422
+ const IPV4_MAPPED_PREFIX = "::ffff:";
423
+ /**
424
+ * 是否为 IPv4-mapped 的 IPv4 回环(`::ffff:127.0.0.0/8`,URL 会归一成 `::ffff:7fxx:xxxx`)。
425
+ *
426
+ * 必须按末 32 位的**实际数值**判,不能按字符串前缀 `::ffff:7f`:前缀命中的
427
+ * `::ffff:7fa:0` 末 32 位是 0x07fa0000 = 7.250.0.0,并不在 127.0.0.0/8 里。按前缀放行
428
+ * 等于把 Bearer 令牌发往远端明文 http,与本函数的安全承诺相反(fail-open 比不判更糟)。
429
+ * 非规范的展开写法(如 `::ffff:0:7f00:1`)判否——fail-closed,可接受。
430
+ *
431
+ * @param host - `URL.hostname` 去掉方括号后的主机名
432
+ * @returns 是否为 IPv4 回环的映射写法
433
+ */
434
+ function isIpv4MappedLoopback(host) {
435
+ if (!host.startsWith(IPV4_MAPPED_PREFIX)) return false;
436
+ const segments = host.slice(7).split(":");
437
+ if (segments.length !== 2) return false;
438
+ const high = Number.parseInt(String(segments[0]), 16);
439
+ return high >= 32512 && high <= 32767;
440
+ }
441
+ /**
442
+ * 是否为 IPv4 回环(127.0.0.0/8 整段,不只是 127.0.0.1)。
443
+ *
444
+ * @param host - `URL.hostname` 去掉方括号后的主机名
445
+ * @returns 是否落在 127.0.0.0/8
446
+ */
447
+ function isIpv4Loopback(host) {
448
+ const parts = host.split(".");
449
+ if (parts.length !== 4 || parts[0] !== "127") return false;
450
+ return parts.every((part) => IPV4_OCTET.test(part) && Number(part) <= 255);
451
+ }
452
+ /**
453
+ * 凭据只允许发往 https 或回环地址:明文 http 到远端会把 Bearer 令牌暴露给同网段
454
+ * (设置卡默认值就是本机 http,属有意用法)。地址里带 userinfo 一律判否——那是另一条
455
+ * 会随 URL 一起写进设置文件的凭据形态,与本插件把密钥交给凭据服务的设计冲突。
456
+ *
457
+ * 回环按整段判定而不是三个字面量:IPv4 回环是 127.0.0.0/8,网关绑在 127.0.0.2 /
458
+ * 127.0.1.1 时同样只在本机,判成"不安全"会让记忆反馈静默停掉、而用户从提示里看不出
459
+ * 自己其实就在本机;IPv6 的 IPv4-mapped 写法(URL 把 `[::ffff:127.0.0.1]` 归一成
460
+ * `[::ffff:7f00:1]`)同理。
461
+ *
462
+ * `localhost` 这一支是**有意保留的 fail-open**:它依赖 hosts/DNS 解析,hosts 被指向远端时
463
+ * 本函数仍判安全。保留它是为了让设置卡的默认写法可用;要彻底关掉这个口子只能改成只认 IP
464
+ * 字面量,那会让"本机网关"这条最常见用法需要用户自己去查 IP。
465
+ *
466
+ * @param url - 待判定的完整 URL
467
+ * @returns 是否可以携带凭据发往该地址
468
+ */
469
+ function isCredentialSafeUrl(url) {
470
+ let parsed;
471
+ try {
472
+ parsed = new URL(url);
473
+ } catch {
474
+ return false;
475
+ }
476
+ if (parsed.username !== "" || parsed.password !== "") return false;
477
+ if (parsed.protocol === "https:") return true;
478
+ if (parsed.protocol !== "http:") return false;
479
+ const host = parsed.hostname.replaceAll(/^\[|\]$/gu, "");
480
+ return host === "localhost" || isIpv4Loopback(host) || host === "::1" || isIpv4MappedLoopback(host);
481
+ }
78
482
  // src/client-entry.ts
79
483
  const NS = "quality-gate";
80
484
  const BUNDLE_PKG = "@jayyuen66/dsh-quality-gate";
81
- const CARD_CSS = [
82
- ".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}",
83
- ".qgc-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
84
- ".qgc-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
85
- ".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}",
86
- ".qgc-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
87
- ".qgc-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
88
- ".qgc-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
89
- ".qgc-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
90
- ".qgc-chevron-open{transform:rotate(180deg)}",
91
- ".qgc-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:8px 0 12px}",
92
- ".qgc-row{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:12px;padding:9px 0}",
93
- ".qgc-label{font-size:13px;color:var(--dsw-alias-label-primary,inherit)}",
94
- ".qgc-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5;margin-top:2px}",
95
- ".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}",
96
- ".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}",
97
- ".qgc-switch-on{background:var(--dsw-alias-brand-primary,#e07856)}",
98
- ".qgc-switch-on::after{left:16px}",
99
- ".qgc-switch:disabled{cursor:not-allowed;opacity:.6}",
100
- ".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}",
101
- ".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}",
102
- ".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)}",
103
- ".qgc-btn:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
104
- ".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)}",
105
- ".qgc-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#e07856);outline-offset:1px}",
106
- ".qgc-btn:disabled{opacity:.5;cursor:not-allowed}",
107
- ".qgc-dirty{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99)}",
108
- ".qgc-saveerr{font-size:12px;color:#c4483f}"
109
- ].join("\n");
110
- function ToggleRow(props) {
111
- 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", {
112
- type: "button",
113
- className: `qgc-switch${props.checked ? " qgc-switch-on" : ""}`,
114
- role: "switch",
115
- "aria-checked": props.checked,
116
- "data-field": props.field,
117
- disabled: props.disabled === true,
118
- onClick: props.onToggle
119
- }));
120
- }
121
- /** 数字输入行:settings 数字字段 ↔ UI 字符串(v7 配置面补齐)。 */
122
- function NumberInputRow(props) {
123
- const num = typeof props.value === "number" && Number.isFinite(props.value) ? String(props.value) : "";
124
- const [text, setText] = (0, react.useState)(num);
125
- const [invalid, setInvalid] = (0, react.useState)(false);
126
- (0, react.useEffect)(() => {
127
- setText(num);
128
- setInvalid(false);
129
- }, [num]);
130
- const invalidHint = props.t("outOfRange");
131
- 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", {
132
- type: "number",
133
- className: "qgc-input",
134
- value: text,
135
- disabled: props.disabled === true,
136
- "aria-invalid": invalid || void 0,
137
- onChange: (event) => {
138
- const next = event.target.value;
139
- setText(next);
140
- if (next === "") {
141
- setInvalid(false);
142
- props.onClear(props.field);
143
- return;
144
- }
145
- const numeric = Number(next);
146
- if (!Number.isFinite(numeric)) {
147
- setInvalid(true);
148
- return;
149
- }
150
- const min = props.min ?? -Infinity;
151
- const max = props.max ?? Infinity;
152
- if (numeric >= min && numeric <= max) {
153
- setInvalid(false);
154
- props.onChange(props.field, numeric);
155
- } else setInvalid(true);
156
- }
157
- }), invalid ? (0, react.createElement)("div", {
158
- className: "qgc-hint",
159
- style: { color: "var(--dsw-alias-danger, #e5484d)" }
160
- }, invalidHint) : null));
161
- }
162
485
  /**
163
- * 文本输入行:settings 字符串字段 ↔ UI 字符串。
164
- * 清空 = unset(回到 schema 默认值)——对本卡的两个网关字段,默认值就是"留空 = 不启用"。
486
+ * 两个数值字段的闭区间只写在 zod schema 上:host schema(`Schema.natural().min/max`)
487
+ * 与卡片输入框都从这里取。zod 4.6 的 `.minValue`/`.maxValue` 直接读出折叠后的边界,
488
+ * `NumberField` 的 `min/max` 因此不再另抄一份数字。
165
489
  */
166
- function TextInputRow(props) {
167
- const text = typeof props.value === "string" ? props.value : "";
168
- const [draft, setDraft] = (0, react.useState)(text);
169
- (0, react.useEffect)(() => {
170
- setDraft(text);
171
- }, [text]);
172
- 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", {
173
- type: "text",
174
- className: "qgc-input",
175
- value: draft,
176
- placeholder: props.placeholder ?? "",
177
- disabled: props.disabled === true,
178
- onChange: (event) => {
179
- const next = event.target.value;
180
- setDraft(next);
181
- if (next.trim() === "") {
182
- props.onClear(props.field);
183
- return;
184
- }
185
- props.onChange(props.field, next.trim());
186
- }
187
- })));
490
+ const MAX_INJECTS_VALUE = zod.z.int().min(1).max(5);
491
+ const GATE_BUDGET_MS_VALUE = zod.z.int().min(1e4).max(6e5);
492
+ /** 把 zod 折叠后的显式边界摊成可选 props(null = 该方向没声明,不伪造一个上限)。 */
493
+ function boundsOf(schema) {
494
+ const bounds = {};
495
+ if (schema.minValue !== null) bounds.min = schema.minValue;
496
+ if (schema.maxValue !== null) bounds.max = schema.maxValue;
497
+ return bounds;
188
498
  }
189
- function SaveBar(props) {
190
- const { t } = props;
191
- const dis = !props.writable || props.busy;
192
- let dirtyText;
193
- if (props.writable) dirtyText = props.dirty ? t("statusDirty") : t("statusClean");
194
- else dirtyText = t("statusReadOnly");
195
- return (0, react.createElement)("div", { className: "qgc-savebar" }, (0, react.createElement)("button", {
196
- type: "button",
197
- className: "qgc-btn qgc-btn-primary",
198
- "data-field": "save",
199
- disabled: dis || !props.dirty,
200
- onClick: props.onSave
201
- }, props.busy ? t("saving") : t("save")), (0, react.createElement)("button", {
202
- type: "button",
203
- className: "qgc-btn",
204
- "data-field": "discard",
205
- disabled: dis || !props.dirty,
206
- onClick: props.onDiscard
207
- }, t("revert")), props.error === null ? (0, react.createElement)("span", { className: "qgc-dirty" }, dirtyText) : (0, react.createElement)("span", { className: "qgc-saveerr" }, props.error));
499
+ const MAX_INJECTS_BOUNDS = boundsOf(MAX_INJECTS_VALUE);
500
+ const GATE_BUDGET_MS_BOUNDS = boundsOf(GATE_BUDGET_MS_VALUE);
501
+ /** 两个数值 schema 的对外投影:测试用 `.minValue`/`.maxValue` 与 `z.toJSONSchema` 钉边界。 */
502
+ const CARD_NUMBER_SCHEMAS = {
503
+ maxInjectsPerTurn: MAX_INJECTS_VALUE,
504
+ gateBudgetMs: GATE_BUDGET_MS_VALUE
505
+ };
506
+ /** 版本可移植的闭区间整数判:宿主平台冻结的 zod(实测 4.4.3)没有 `.validate`
507
+ * (4.6 才回到 classic 面),判据用 `boundsOf` 折出的边界自持,不押 zod 的方法面——
508
+ * 否则单仓测试(catalog 4.6.7)全绿、真宿主里槽位崩溃、设置表单静默消失。 */
509
+ function inBounds(bounds, value) {
510
+ return Number.isInteger(value) && (bounds.min ?? Number.NEGATIVE_INFINITY) <= value && value <= (bounds.max ?? Number.POSITIVE_INFINITY);
511
+ }
512
+ /** 可选整数:越界/小数/非有限值折成 undefined,只丢那一格而不是整份快照。
513
+ * 不引 `z.compile`:编译器给客户端 bundle 增约 28 KB,且 CSP 环境会拒绝 `new Function`;
514
+ * 卡片每帧只解析一枚快照对象,收益抵不上这两条代价。 */
515
+ const MAX_INJECTS_SCHEMA = zod.z.preprocess((value) => typeof value === "number" && inBounds(MAX_INJECTS_BOUNDS, value) ? value : void 0, MAX_INJECTS_VALUE.optional());
516
+ const GATE_BUDGET_MS_SCHEMA = zod.z.preprocess((value) => typeof value === "number" && inBounds(GATE_BUDGET_MS_BOUNDS, value) ? value : void 0, GATE_BUDGET_MS_VALUE.optional());
517
+ /** 卡片可编辑字段的值面。逐字段先把错误类型折成 undefined:宿主给了坏形状时只丢掉
518
+ * 那一格,其余字段照常渲染;整份快照不是对象时才整体回落到空表。 */
519
+ const optionalBoolean = zod.z.preprocess((value) => typeof value === "boolean" ? value : void 0, zod.z.boolean().optional());
520
+ const optionalString = zod.z.preprocess((value) => typeof value === "string" ? value : void 0, zod.z.string().optional());
521
+ /** 网关地址:必须是 http(s),且与宿主运行期同一份判据——不能带 userinfo,远端只能 https,
522
+ * 明文 http 只允许本机回环。空串与坏文本折成未设置。 */
523
+ const optionalHttpUrl = zod.z.preprocess((value) => {
524
+ let text;
525
+ if (typeof value === "string") {
526
+ const trimmed = value.trim();
527
+ if (trimmed !== "") text = trimmed;
528
+ }
529
+ return text;
530
+ }, zod.z.string().refine(isCredentialSafeUrl, { error: "credential-unsafe-url" }).optional());
531
+ const CARD_VALUES = zod.z.object({
532
+ enabled: optionalBoolean,
533
+ maxInjectsPerTurn: MAX_INJECTS_SCHEMA,
534
+ gateBudgetMs: GATE_BUDGET_MS_SCHEMA,
535
+ memoryFeedback: optionalBoolean,
536
+ memoryGatewayUrl: optionalHttpUrl,
537
+ memoryGatewayKeyRef: optionalString
538
+ });
539
+ /** 草稿面仍按宽 string 读:`TextField` 没有 invalid 态,输入中的半截地址不会被当场折成空串;
540
+ * 提交时由 `writeTouched` 用同一份凭据地址判据拦下,不合规的地址不落盘。 */
541
+ const CARD_FIELD_SCHEMAS = {
542
+ enabled: optionalBoolean,
543
+ maxInjectsPerTurn: MAX_INJECTS_SCHEMA,
544
+ gateBudgetMs: GATE_BUDGET_MS_SCHEMA,
545
+ memoryFeedback: optionalBoolean,
546
+ memoryGatewayUrl: optionalString,
547
+ memoryGatewayKeyRef: optionalString
548
+ };
549
+ /** 快照值投影:逐字段解析;坏形状与越界值只折掉那一格,其余字段照常渲染。
550
+ * 整份一次 `safeParse` 会让一个坏 URL 把 enabled 与两个数字字段一起清空。 */
551
+ function projectCardValues(raw) {
552
+ const out = {};
553
+ for (const [field, schema] of Object.entries(CARD_VALUES.shape)) {
554
+ const parsed = schema.safeParse(raw[field]);
555
+ if (parsed.success) out[field] = parsed.data;
556
+ }
557
+ return out;
208
558
  }
209
559
  /** 值语义相等(与 shared 的 diffTouched 同口径):表单值是标量与数组,引用相等会把等值数组判成改动。 */
210
560
  function sameValue(left, right) {
211
561
  return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
212
562
  }
563
+ /** 凭据引用名形状:与 @deepseek-ai/dsh-credentials 的 REF_PATTERN 同式。客户端不引该包的
564
+ * 值导出(那是宿主侧凭据服务面),所以这里按同一正则复判;两处失配时宿主运行期仍会拒绝。 */
565
+ const CREDENTIAL_REF_PATTERN = /^[A-Za-z_]\w*$/u;
213
566
  /**
214
567
  * 逐键 await 写入并**消费受理位**(0.1.7 的 set/unset 回 Promise<boolean>:
215
568
  * true=宿主受理;false=拒绝或写入被跳过(官方语义:"refuses stale revisions";
@@ -225,6 +578,16 @@ async function writeTouched(channels) {
225
578
  try {
226
579
  for (const key of keys) {
227
580
  const val = touched[key];
581
+ if (key === "memoryGatewayUrl" && typeof val === "string" && val.trim() !== "" && !isCredentialSafeUrl(val.trim())) {
582
+ setBusy(false);
583
+ setSaveError(t("invalidGatewayUrl"));
584
+ return;
585
+ }
586
+ if (key === "memoryGatewayKeyRef" && typeof val === "string" && val.trim() !== "" && !CREDENTIAL_REF_PATTERN.test(val.trim())) {
587
+ setBusy(false);
588
+ setSaveError(t("invalidKeyRef"));
589
+ return;
590
+ }
228
591
  if (!await (val === void 0 ? unset(key) : set(key, val))) {
229
592
  setBusy(false);
230
593
  setSaveError(t("saveRejected"));
@@ -243,162 +606,125 @@ async function writeTouched(channels) {
243
606
  setSaveError(t("saveFailed", { message: String(error instanceof Error ? error.message : error) }));
244
607
  }
245
608
  }
246
- /** 头部:渲染子树扁平化(规避 max-nested-calls)——DOM 各层拆成独立 createElement 元素
247
- * const,逐层用变量组合,避免 createElement 回调逐层内嵌。 */
248
- function buildCardHeader(props) {
249
- const { t, open, onToggle } = props;
250
- const nameEl = (0, react.createElement)("div", { className: "qgc-name" }, t("cardTitle"));
251
- const descEl = (0, react.createElement)("div", { className: "qgc-desc" }, t("cardDescription"));
252
- const headDiv = (0, react.createElement)("div", { className: "qgc-head" }, nameEl, descEl);
253
- const chevronPathData = "M3 5l4 4 4-4";
254
- const chevronPath = (0, react.createElement)("path", {
255
- ["d"]: chevronPathData,
256
- fill: "none",
257
- stroke: "currentColor",
258
- strokeWidth: 1.5,
259
- strokeLinecap: "round",
260
- strokeLinejoin: "round"
261
- });
262
- const chevron = (0, react.createElement)("svg", {
263
- width: 14,
264
- height: 14,
265
- viewBox: "0 0 14 14",
266
- "aria-hidden": true,
267
- className: `qgc-chevron${open ? " qgc-chevron-open" : ""}`
268
- }, chevronPath);
269
- return (0, react.createElement)("button", {
270
- type: "button",
271
- className: "qgc-header",
272
- "aria-expanded": open,
273
- onClick: onToggle
274
- }, headDiv, chevron);
275
- }
276
- /** 展开态的 `ul` 本体:行元素仍按从前的顺序逐个传给同一个 createElement(不折成数组
277
- * 子节点,保持宿主侧 children 形状一致)。 */
278
- function buildCardBody(props) {
279
- const { t, writable, eff, setField, clearField, dirty, busy, saveError, onSave, onDiscard } = props;
280
- const rowEnabled = (0, react.createElement)(ToggleRow, {
609
+ /** 门禁小节:总开关 + 注入配额 + 预算(毫秒按秒显示)。 */
610
+ function buildGateSection(props) {
611
+ const { t, writable, eff, setField } = props;
612
+ const maxInjects = eff("maxInjectsPerTurn");
613
+ const budgetMs = eff("gateBudgetMs");
614
+ const budgetSeconds = budgetMs === void 0 ? void 0 : budgetMs / 1e3;
615
+ const budgetMinSeconds = (GATE_BUDGET_MS_BOUNDS.min ?? 0) / 1e3;
616
+ const budgetMaxSeconds = (GATE_BUDGET_MS_BOUNDS.max ?? 0) / 1e3;
617
+ const enabled = (0, react.createElement)(_, {
281
618
  label: t("enabledLabel"),
282
- hint: t("enabledHint"),
619
+ ...l(t("enabledHint")),
283
620
  field: "enabled",
284
- checked: eff("enabled") !== false,
621
+ checked: eff("enabled") ?? true,
285
622
  disabled: !writable,
286
- onToggle: () => {
287
- setField("enabled", eff("enabled") === false);
623
+ onChange: (next) => {
624
+ setField("enabled", next);
288
625
  }
289
626
  });
290
- const rowInjects = (0, react.createElement)(NumberInputRow, {
291
- t,
627
+ const injects = (0, react.createElement)(v, {
292
628
  label: t("injectsLabel"),
293
- hint: t("injectsHint"),
629
+ ...l(t("injectsHint")),
294
630
  field: "maxInjectsPerTurn",
295
- value: eff("maxInjectsPerTurn"),
631
+ value: maxInjects,
632
+ ...MAX_INJECTS_BOUNDS,
633
+ step: 1,
634
+ unit: t("unitTimes"),
635
+ invalidHint: t("outOfRange"),
296
636
  disabled: !writable,
297
- onChange: (field, val) => {
298
- setField(field, val);
299
- },
300
- onClear: (field) => {
301
- clearField(field);
302
- },
303
- min: 1,
304
- max: 5
637
+ onChange: (next) => {
638
+ setField("maxInjectsPerTurn", next);
639
+ }
305
640
  });
306
- const rowBudget = (0, react.createElement)(NumberInputRow, {
307
- t,
641
+ const budget = (0, react.createElement)(v, {
308
642
  label: t("budgetLabel"),
309
- hint: t("budgetHint"),
643
+ ...l(t("budgetHint")),
310
644
  field: "gateBudgetMs",
311
- value: eff("gateBudgetMs"),
645
+ value: budgetSeconds,
646
+ min: budgetMinSeconds,
647
+ max: budgetMaxSeconds,
648
+ step: 1,
649
+ unit: t("unitSeconds"),
650
+ invalidHint: t("outOfRange"),
312
651
  disabled: !writable,
313
- onChange: (field, val) => {
314
- setField(field, val);
315
- },
316
- onClear: (field) => {
317
- clearField(field);
318
- },
319
- min: 1e4,
320
- max: 6e5
652
+ onChange: (next) => {
653
+ setField("gateBudgetMs", next === void 0 ? void 0 : next * 1e3);
654
+ }
321
655
  });
322
- const rowMemory = (0, react.createElement)(ToggleRow, {
656
+ return (0, react.createElement)(d, { title: t("gateSection") }, enabled, injects, budget);
657
+ }
658
+ /** 记忆反馈小节:开关 + 网关地址 + 密钥引用。 */
659
+ function buildMemorySection(props) {
660
+ const { t, writable, eff, setField } = props;
661
+ const gatewayUrl = eff("memoryGatewayUrl") ?? "";
662
+ const keyRef = eff("memoryGatewayKeyRef") ?? "";
663
+ const memory = (0, react.createElement)(_, {
323
664
  label: t("memoryLabel"),
324
- hint: t("memoryHint"),
665
+ ...l(t("memoryHint")),
325
666
  field: "memoryFeedback",
326
667
  checked: eff("memoryFeedback") === true,
327
668
  disabled: !writable,
328
- onToggle: () => {
329
- setField("memoryFeedback", eff("memoryFeedback") !== true);
669
+ onChange: (next) => {
670
+ setField("memoryFeedback", next);
330
671
  }
331
672
  });
332
- const rowGatewayUrl = (0, react.createElement)(TextInputRow, {
673
+ const url = (0, react.createElement)(y, {
333
674
  label: t("gatewayUrlLabel"),
334
- hint: t("gatewayUrlHint"),
675
+ ...l(t("gatewayUrlHint")),
335
676
  field: "memoryGatewayUrl",
336
- value: eff("memoryGatewayUrl"),
677
+ value: gatewayUrl,
337
678
  placeholder: "http://127.0.0.1:8420",
679
+ monospace: true,
338
680
  disabled: !writable,
339
- onChange: (field, val) => {
340
- setField(field, val);
341
- },
342
- onClear: (field) => {
343
- clearField(field);
681
+ onChange: (next) => {
682
+ setField("memoryGatewayUrl", next.trim() === "" ? void 0 : next.trim());
344
683
  }
345
684
  });
346
- const rowGatewayKeyRef = (0, react.createElement)(TextInputRow, {
685
+ const keyRefField = (0, react.createElement)(y, {
347
686
  label: t("keyRefLabel"),
348
- hint: t("keyRefHint"),
687
+ ...l(t("keyRefHint")),
349
688
  field: "memoryGatewayKeyRef",
350
- value: eff("memoryGatewayKeyRef"),
689
+ value: keyRef,
351
690
  placeholder: "TDAI_GATEWAY_KEY",
691
+ monospace: true,
352
692
  disabled: !writable,
353
- onChange: (field, val) => {
354
- setField(field, val);
355
- },
356
- onClear: (field) => {
357
- clearField(field);
693
+ onChange: (next) => {
694
+ setField("memoryGatewayKeyRef", next.trim() === "" ? void 0 : next.trim());
358
695
  }
359
696
  });
360
- const rowSave = (0, react.createElement)(SaveBar, {
361
- t,
362
- dirty,
363
- writable,
364
- busy,
365
- error: saveError,
366
- onSave,
367
- onDiscard
368
- });
369
- return (0, react.createElement)("ul", { className: "qgc-body" }, rowEnabled, rowInjects, rowBudget, rowMemory, rowGatewayUrl, rowGatewayKeyRef, rowSave);
697
+ return (0, react.createElement)(d, { title: t("memorySection") }, memory, url, keyRefField);
370
698
  }
371
699
  function DgCard(props) {
372
- const { t: t$1 } = props;
373
- const [open, setOpen] = (0, react.useState)(false);
700
+ const { t } = props;
374
701
  const snap = props.useCard((cardSnap) => cardSnap);
375
- const { value } = snap;
702
+ const value = projectCardValues(snap.value);
376
703
  const writable = snap.status === "ready" && snap.writable;
377
704
  const [touched, setTouched] = (0, react.useState)({});
378
705
  const [busy, setBusy] = (0, react.useState)(false);
379
706
  const [saveError, setSaveError] = (0, react.useState)(null);
380
- /** 渲染取值:touched 优先,快照兜底(未触碰字段实时跟随外部快照)。 */
381
- const eff = (field) => field in touched ? touched[field] : value[field];
382
- const dirty = t(touched, value).length > 0;
707
+ /** 渲染取值:touched 优先(经 zod 收窄),快照兜底(未触碰字段实时跟随外部快照)。 */
708
+ const eff = (field) => {
709
+ if (!(field in touched)) return value[field];
710
+ const parsedTouched = CARD_FIELD_SCHEMAS[field].safeParse(touched[field]);
711
+ return parsedTouched.success ? parsedTouched.data : void 0;
712
+ };
713
+ const dirtyKeys = t$1(touched, value);
714
+ const dirty = dirtyKeys.length > 0;
383
715
  const setField = (field, val) => {
384
716
  setTouched((prev) => ({
385
717
  ...prev,
386
718
  [field]: val
387
719
  }));
388
720
  };
389
- const clearField = (field) => {
390
- setTouched((prev) => ({
391
- ...prev,
392
- [field]: void 0
393
- }));
394
- };
395
721
  const save = () => {
396
- const keys = t(touched, value);
722
+ const keys = t$1(touched, value);
397
723
  if (keys.length === 0) return;
398
724
  setBusy(true);
399
725
  setSaveError(null);
400
726
  writeTouched({
401
- t: t$1,
727
+ t,
402
728
  set: props.set,
403
729
  unset: props.unset,
404
730
  keys,
@@ -412,26 +738,29 @@ function DgCard(props) {
412
738
  setTouched({});
413
739
  setSaveError(null);
414
740
  };
415
- const header = buildCardHeader({
416
- t: t$1,
417
- open,
418
- onToggle: () => {
419
- setOpen(!open);
420
- }
421
- });
422
- const body = open ? buildCardBody({
423
- t: t$1,
741
+ const inputs = {
742
+ t,
424
743
  writable,
425
744
  eff,
426
- setField,
427
- clearField,
745
+ setField
746
+ };
747
+ const gate = buildGateSection(inputs);
748
+ const memory = buildMemorySection(inputs);
749
+ const saveBar = (0, react.createElement)(S, {
428
750
  dirty,
429
751
  busy,
430
- saveError,
752
+ writable,
753
+ error: saveError,
754
+ saveLabel: t("save"),
755
+ savingLabel: t("saving"),
756
+ revertLabel: t("revert"),
757
+ dirtyLabel: t("dirtyCount", { count: dirtyKeys.length }),
758
+ cleanLabel: t("statusClean"),
759
+ readOnlyLabel: t("statusReadOnly"),
431
760
  onSave: save,
432
761
  onDiscard: discard
433
- }) : null;
434
- return (0, react.createElement)("li", { className: `qgc-card${open ? " qgc-card-open" : ""}` }, header, body);
762
+ });
763
+ return (0, react.createElement)(u, null, gate, memory, saveBar);
435
764
  }
436
765
  function cardStore(scope) {
437
766
  let cachedSnap = null;
@@ -467,8 +796,8 @@ const inject = [
467
796
  function apply(ctx) {
468
797
  ctx.effect(() => {
469
798
  const tag = document.createElement("style");
470
- tag.id = "quality-gate-card-css";
471
- tag.textContent = CARD_CSS;
799
+ tag.id = "dsh-card-ui-css-quality-gate";
800
+ tag.textContent = c;
472
801
  document.head.append(tag);
473
802
  return () => {
474
803
  tag.remove();
@@ -491,8 +820,11 @@ function apply(ctx) {
491
820
  }, DgCard);
492
821
  });
493
822
  }
823
+ exports.CARD_NUMBER_SCHEMAS = CARD_NUMBER_SCHEMAS;
494
824
  exports.apply = apply;
495
825
  exports.inject = inject;
826
+ exports.projectCardValues = projectCardValues;
827
+ exports.writeTouched = writeTouched;
496
828
  return module.exports;
497
829
  },
498
830
  });