@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/CHANGELOG.md +91 -0
- package/LICENSE +1 -1
- package/README.md +179 -93
- package/README.zh-CN.md +181 -91
- package/THIRD_PARTY_NOTICES.md +4 -1
- package/client.js +532 -266
- package/cordis.patch.yml +3 -1
- package/docs/DETAIL.md +24 -0
- package/docs/DETAIL.zh.md +24 -0
- package/host.js +343 -76
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +54 -38
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:
|
|
3
|
+
id: "@jayyuen66/dsh-quality-gate",
|
|
4
4
|
factory: (require) => {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
383
|
+
memoryHint: "默认关闭。开启后:代码型门禁失败的证据(命令/根/全文报错,不截断)写入记忆网关的 L0 会话层供后续会话召回;总线 lesson-loop 的同步上报不受此开关影响。关闭后只注入不沉淀。真正决定能否投递的是网关地址能否解析出值(见下面两项)",
|
|
38
384
|
gatewayUrlLabel: "记忆网关地址",
|
|
39
|
-
gatewayUrlHint: "
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
*
|
|
156
|
-
*
|
|
424
|
+
* 两个数值字段的闭区间只写在 zod schema 上:host schema(`Schema.natural().min/max`)
|
|
425
|
+
* 与卡片输入框都从这里取。zod 4.6 的 `.minValue`/`.maxValue` 直接读出折叠后的边界,
|
|
426
|
+
* `NumberField` 的 `min/max` 因此不再另抄一份数字。
|
|
157
427
|
*/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
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
|
-
|
|
534
|
+
...l(t("enabledHint")),
|
|
263
535
|
field: "enabled",
|
|
264
|
-
checked: eff("enabled")
|
|
536
|
+
checked: eff("enabled") ?? true,
|
|
265
537
|
disabled: !writable,
|
|
266
|
-
|
|
267
|
-
setField("enabled",
|
|
538
|
+
onChange: (next) => {
|
|
539
|
+
setField("enabled", next);
|
|
268
540
|
}
|
|
269
541
|
});
|
|
270
|
-
const
|
|
271
|
-
t,
|
|
542
|
+
const injects = (0, react.createElement)(v, {
|
|
272
543
|
label: t("injectsLabel"),
|
|
273
|
-
|
|
544
|
+
...l(t("injectsHint")),
|
|
274
545
|
field: "maxInjectsPerTurn",
|
|
275
|
-
value:
|
|
546
|
+
value: maxInjects,
|
|
547
|
+
...MAX_INJECTS_BOUNDS,
|
|
548
|
+
step: 1,
|
|
549
|
+
unit: t("unitTimes"),
|
|
550
|
+
invalidHint: t("outOfRange"),
|
|
276
551
|
disabled: !writable,
|
|
277
|
-
onChange: (
|
|
278
|
-
setField(
|
|
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
|
|
287
|
-
t,
|
|
556
|
+
const budget = (0, react.createElement)(v, {
|
|
288
557
|
label: t("budgetLabel"),
|
|
289
|
-
|
|
558
|
+
...l(t("budgetHint")),
|
|
290
559
|
field: "gateBudgetMs",
|
|
291
|
-
value:
|
|
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: (
|
|
294
|
-
setField(
|
|
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
|
-
|
|
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
|
-
|
|
580
|
+
...l(t("memoryHint")),
|
|
305
581
|
field: "memoryFeedback",
|
|
306
582
|
checked: eff("memoryFeedback") === true,
|
|
307
583
|
disabled: !writable,
|
|
308
|
-
|
|
309
|
-
setField("memoryFeedback",
|
|
584
|
+
onChange: (next) => {
|
|
585
|
+
setField("memoryFeedback", next);
|
|
310
586
|
}
|
|
311
587
|
});
|
|
312
|
-
const
|
|
588
|
+
const url = (0, react.createElement)(y, {
|
|
313
589
|
label: t("gatewayUrlLabel"),
|
|
314
|
-
|
|
590
|
+
...l(t("gatewayUrlHint")),
|
|
315
591
|
field: "memoryGatewayUrl",
|
|
316
|
-
value:
|
|
592
|
+
value: gatewayUrl,
|
|
317
593
|
placeholder: "http://127.0.0.1:8420",
|
|
594
|
+
monospace: true,
|
|
318
595
|
disabled: !writable,
|
|
319
|
-
onChange: (
|
|
320
|
-
setField(
|
|
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
|
|
600
|
+
const keyRefField = (0, react.createElement)(y, {
|
|
327
601
|
label: t("keyRefLabel"),
|
|
328
|
-
|
|
602
|
+
...l(t("keyRefHint")),
|
|
329
603
|
field: "memoryGatewayKeyRef",
|
|
330
|
-
value:
|
|
604
|
+
value: keyRef,
|
|
331
605
|
placeholder: "TDAI_GATEWAY_KEY",
|
|
606
|
+
monospace: true,
|
|
332
607
|
disabled: !writable,
|
|
333
|
-
onChange: (
|
|
334
|
-
setField(
|
|
335
|
-
},
|
|
336
|
-
onClear: (field) => {
|
|
337
|
-
clearField(field);
|
|
608
|
+
onChange: (next) => {
|
|
609
|
+
setField("memoryGatewayKeyRef", next.trim() === "" ? void 0 : next.trim());
|
|
338
610
|
}
|
|
339
611
|
});
|
|
340
|
-
|
|
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
|
|
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) =>
|
|
362
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
414
|
-
return (0, react.createElement)(
|
|
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 = "
|
|
451
|
-
tag.textContent =
|
|
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
|
-
|
|
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
|
});
|