@jayyuen66/dsh-ocr-review 0.1.8 → 0.1.9

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,11 +7,14 @@ 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
  // src/ui-messages.ts
11
12
  const UI_MESSAGES = {
12
13
  zh: {
13
- cardTitle: "open-code-review 代码审查",
14
- cardDescription: "ocr 工具配置:选择 provider/model 应用到 OCR(应用后 key 由 api_key_cmd 动态读取,不再写入明文)+ 审查参数设置",
14
+ modelSection: "模型",
15
+ reviewSection: "审查参数",
16
+ actionsLabel: "动作",
17
+ outOfRange: "超出允许范围,未保存",
15
18
  statusReadOnly: "当前作用域只读",
16
19
  statusDirty: "有未保存的修改,点「保存」生效",
17
20
  statusClean: "无未保存的修改",
@@ -59,8 +62,6 @@ const UI_MESSAGES = {
59
62
  languageHint: "OCR 评论输出语言(写入 config.language)",
60
63
  autoVerifyLabel: "应用后自动验证",
61
64
  autoVerifyHint: "选择 provider/model 后自动跑 ocr llm test",
62
- onLabel: "开启",
63
- offLabel: "关闭",
64
65
  maxCommentsLabel: "摘要评论条数 (maxComments)",
65
66
  maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认见本行设置项的初值)",
66
67
  timeoutLabel: "运行超时 (timeoutMinutes)",
@@ -111,8 +112,10 @@ const UI_MESSAGES = {
111
112
  binaryGuideDismiss: "知道了"
112
113
  },
113
114
  en: {
114
- cardTitle: "open-code-review code review",
115
- cardDescription: "ocr tool setup: pick a provider/model and apply it to OCR (once applied, keys are read dynamically via api_key_cmd and no longer written in plain text) + review parameters",
115
+ modelSection: "Model",
116
+ reviewSection: "Review parameters",
117
+ actionsLabel: "Actions",
118
+ outOfRange: "Out of the allowed range — not saved",
116
119
  statusReadOnly: "This scope is read-only",
117
120
  statusDirty: "Unsaved changes — press Save to apply",
118
121
  statusClean: "No unsaved changes",
@@ -160,8 +163,6 @@ const UI_MESSAGES = {
160
163
  languageHint: "Output language of OCR comments (written into config.language)",
161
164
  autoVerifyLabel: "Verify after applying",
162
165
  autoVerifyHint: "Run `ocr llm test` automatically after picking a provider/model",
163
- onLabel: "On",
164
- offLabel: "Off",
165
166
  maxCommentsLabel: "Summary comment cap (maxComments)",
166
167
  maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default: the row's initial value)",
167
168
  timeoutLabel: "Run timeout (timeoutMinutes)",
@@ -213,59 +214,591 @@ const UI_MESSAGES = {
213
214
  }
214
215
  };
215
216
  // node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
216
- function e$2(e) {
217
+ function e$3(e) {
217
218
  return typeof e == `object` && !!e && !Array.isArray(e);
218
219
  }
219
- const t$2 = { absent: !0 };
220
- function n$1(e, n) {
220
+ const t$3 = { absent: !0 };
221
+ function n$2(e, n) {
221
222
  try {
222
223
  return {
223
224
  absent: !1,
224
225
  value: e[n]
225
226
  };
226
227
  } catch {
227
- return t$2;
228
+ return t$3;
228
229
  }
229
230
  }
230
- function i(r, i) {
231
- let a = e$2(r) ? n$1(r, i) : t$2;
231
+ function i$1(r, i) {
232
+ let a = e$3(r) ? n$2(r, i) : t$3;
232
233
  return a.absent ? void 0 : a.value;
233
234
  }
234
235
  // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-apply.js
235
- const e$1 = (e) => e.length > 0 && !(e in Object.prototype);
236
- function t$1(e, t, n) {
236
+ const e$2 = (e) => e.length > 0 && !(e in Object.prototype);
237
+ function t$2(e, t, n) {
237
238
  n === void 0 ? e[t] = void 0 : Object.defineProperty(e, t, n);
238
239
  }
239
- function n(n, r, i) {
240
- if (!e$1(r)) throw Error(`claimApply 的 flag 不得为空、也不得命中 Object.prototype 的成员(__proto__ 一类会让占位静默失灵或遮蔽全局成员):${r}`);
240
+ function n$1(n, r, i) {
241
+ if (!e$2(r)) throw Error(`claimApply 的 flag 不得为空、也不得命中 Object.prototype 的成员(__proto__ 一类会让占位静默失灵或遮蔽全局成员):${r}`);
241
242
  let a = globalThis, o = Object.hasOwn(a, r) ? a[r] : void 0;
242
243
  if (o === !0 || typeof o == `symbol`) return !1;
243
244
  let s = Object.getOwnPropertyDescriptor(a, r), c = Symbol(r);
244
245
  a[r] = c;
245
246
  try {
246
247
  n.effect(() => () => {
247
- a[r] === c && t$1(a, r, s);
248
+ a[r] === c && t$2(a, r, s);
248
249
  }, i);
249
250
  } catch (e) {
250
- throw a[r] === c && t$1(a, r, s), e;
251
+ throw a[r] === c && t$2(a, r, s), e;
251
252
  }
252
253
  return !0;
253
254
  }
254
255
  // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
255
- function e(t, n, r) {
256
+ function e$1(t, n, r) {
256
257
  if (t == null || n == null) return (t ?? null) === (n ?? null);
257
258
  if (Object.is(t, n)) return !0;
258
259
  if (typeof t != `object` || typeof n != `object`) return !1;
259
260
  if (r.get(t) === n) return !0;
260
- 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));
261
+ 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));
261
262
  let i = Object.keys(t), a = Object.keys(n);
262
- return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e(Reflect.get(t, i), Reflect.get(n, i), r));
263
+ return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e$1(Reflect.get(t, i), Reflect.get(n, i), r));
263
264
  }
264
- function t(t, n) {
265
+ function t$1(t, n) {
265
266
  let r = [];
266
- for (let i of Object.keys(t)) e(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
267
+ for (let i of Object.keys(t)) e$1(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
267
268
  return r;
268
269
  }
270
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-ui.js
271
+ const c = `
272
+ .dsu-form{display:flex;flex-direction:column}
273
+ .dsu-section{margin-top:24px;padding-top:24px;border-top:.5px solid var(--dsw-alias-border-l1)}
274
+ .dsu-form>.dsu-section:first-child{margin-top:0;padding-top:0;border-top:0}
275
+ .dsu-sectionTitle{margin:0;font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
276
+ .dsu-sectionHint{margin:4px 0 0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
277
+ .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)}
278
+ .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)}
279
+ .dsu-fieldMeta{display:flex;flex-direction:column;gap:4px;min-width:0}
280
+ .dsu-labelRow{display:flex;align-items:center;gap:6px;min-width:0}
281
+ .dsu-label{font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
282
+ .dsu-labelMono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
283
+ .dsu-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
284
+ .dsu-control{min-width:0;display:flex;align-items:center;gap:8px}
285
+ .dsu-fieldWide{grid-template-columns:minmax(0,1fr);align-items:start}
286
+ .dsu-fieldWide .dsu-control{width:100%;max-width:720px;margin-top:2px}
287
+ .dsu-help{display:inline-flex;align-items:center;gap:6px;min-width:0}
288
+ .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}
289
+ .dsu-help:hover .dsu-helpBtn,.dsu-helpBtn:focus-visible{color:var(--dsw-alias-label-primary)}
290
+ .dsu-helpBtn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
291
+ .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)}
292
+ .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}
293
+ .dsu-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}
294
+ .dsu-input[aria-invalid="true"]{border-color:var(--dsw-alias-state-error-primary)}
295
+ .dsu-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
296
+ .dsu-input::placeholder{color:var(--dsw-alias-label-tertiary)}
297
+ .dsu-textarea{height:auto;min-height:96px;padding:8px 12px;line-height:1.6;resize:vertical}
298
+ .dsu-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
299
+ .dsu-numberWrap{display:flex;flex-direction:column;gap:4px;width:100%;min-width:0}
300
+ .dsu-number{display:flex;align-items:center;gap:8px;min-width:0}
301
+ .dsu-number .dsu-input{min-width:0}
302
+ .dsu-unit{flex:none;font-size:12px;color:var(--dsw-alias-label-tertiary)}
303
+ .dsu-error{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary)}
304
+ .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}
305
+ .dsu-knob{width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:transform .16s,background-color .16s}
306
+ .dsu-switch[aria-checked="true"]{background:var(--dsw-alias-label-primary)}
307
+ .dsu-switch[aria-checked="true"] .dsu-knob{transform:translateX(16px);background:var(--dsw-alias-label-primary-foreground)}
308
+ .dsu-switch:disabled{opacity:.4;cursor:not-allowed}
309
+ .dsu-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
310
+ .dsu-selectWrap{position:relative;width:100%;min-width:0}
311
+ .dsu-select{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}
312
+ .dsu-select[aria-expanded="true"]{border-color:var(--dsw-alias-state-business-primary)}
313
+ .dsu-selectValue{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
314
+ .dsu-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
315
+ .dsu-select[aria-expanded="true"] .dsu-chevron{transform:rotate(180deg)}
316
+ .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}
317
+ .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}
318
+ .dsu-option:hover,.dsu-option[data-active="true"]{background:var(--dsw-alias-bg-layer-3)}
319
+ .dsu-option:disabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}
320
+ .dsu-option:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
321
+ .dsu-optionGroup{padding:6px 10px 2px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-tertiary)}
322
+ .dsu-check{opacity:0;flex:none}
323
+ .dsu-option[aria-selected="true"] .dsu-check{opacity:1}
324
+ .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)}
325
+ .dsu-savebar[data-dirty="true"]{position:sticky;bottom:0;z-index:2;box-shadow:0 -12px 24px rgb(0 0 0 / 35%)}
326
+ .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}
327
+ .dsu-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l3)}
328
+ .dsu-btn:active:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
329
+ .dsu-btn:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
330
+ .dsu-btn:disabled{opacity:.4;cursor:not-allowed}
331
+ .dsu-btnPrimary{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
332
+ .dsu-btnPrimary:hover:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.9)}
333
+ .dsu-btnPrimary:active:not(:disabled){background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);filter:brightness(.84)}
334
+ .dsu-btnDanger{color:var(--dsw-alias-state-error-primary)}
335
+ .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)}
336
+ .dsu-status{margin-left:auto;font-size:12px;color:var(--dsw-alias-label-tertiary)}
337
+ .dsu-statusError{color:var(--dsw-alias-state-error-primary)}
338
+ .dsu-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(200px,100%),1fr));gap:0 24px}
339
+ .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)}
340
+ .dsu-grid>.dsu-field .dsu-control{width:100%}
341
+ @media (max-width:640px){
342
+ .dsu-field{grid-template-columns:minmax(0,1fr);align-items:start}
343
+ .dsu-control{width:100%;max-width:720px}
344
+ .dsu-section{margin-top:20px;padding-top:20px}
345
+ }
346
+ @media (prefers-reduced-motion:reduce){
347
+ .dsu-switch,.dsu-knob,.dsu-chevron,.dsu-btn{transition:none}
348
+ }
349
+ `;
350
+ function l(e, t = 30) {
351
+ return e.length <= t ? { hint: e } : { detail: e };
352
+ }
353
+ function u(t) {
354
+ return (0, react.createElement)(`div`, { className: `dsu-form` }, t.children);
355
+ }
356
+ function d(t) {
357
+ 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);
358
+ }
359
+ function m(a) {
360
+ 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)(() => {
361
+ v.current !== null && (clearTimeout(v.current), v.current = null);
362
+ }, []), b = (0, react.useCallback)(() => {
363
+ let e = g.current, t = _.current;
364
+ /* v8 ignore next 3 -- place 只在 open 的 layout effect 与挂载期监听器里调用;refs 为空只可能是卸载竞态 */
365
+ if (e === null || t === null) return;
366
+ let n = e.getBoundingClientRect(), r = t.offsetWidth, i = t.offsetHeight, { left: a } = n, o = n.bottom + 8;
367
+ o + i > window.innerHeight - 8 && (o = n.top - i - 8), a + r > window.innerWidth - 8 && (a = window.innerWidth - r - 8);
368
+ let s = Math.max(8, window.innerWidth - r - 8), c = Math.max(8, window.innerHeight - i - 8);
369
+ t.style.left = `${Math.min(Math.max(8, a), s)}px`, t.style.top = `${Math.min(Math.max(8, o), c)}px`;
370
+ }, []), x = (0, react.useCallback)(() => {
371
+ y(), h.current = !1, f(!1), u(!1);
372
+ }, [y]), S = (0, react.useCallback)(() => {
373
+ y(), u(!0);
374
+ }, [y]), C = (0, react.useCallback)(() => {
375
+ y(), !(p.current || m.current || h.current) && (v.current = setTimeout(() => {
376
+ /* v8 ignore next 3 -- 任何重新进入/钉住都会 cancelHide;这次复查只防未来的异步路径 */
377
+ v.current = null, p.current || m.current || h.current || u(!1);
378
+ }, 120));
379
+ }, [y]), w = (0, react.useCallback)((e) => {
380
+ h.current = e, f(e);
381
+ }, []);
382
+ (0, react.useLayoutEffect)(() => {
383
+ if (!l) return () => {};
384
+ b();
385
+ let e = (e) => {
386
+ let { target: t } = e;
387
+ t instanceof Node && _.current?.contains(t) === !0 || (h.current ? b() : x());
388
+ }, t = () => {
389
+ b();
390
+ };
391
+ return window.addEventListener(`scroll`, e, !0), window.addEventListener(`resize`, t), () => {
392
+ window.removeEventListener(`scroll`, e, !0), window.removeEventListener(`resize`, t);
393
+ };
394
+ }, [
395
+ l,
396
+ a.detail,
397
+ b,
398
+ x
399
+ ]), (0, react.useEffect)(() => {
400
+ if (!l) return () => {};
401
+ let e = (e) => {
402
+ e.key === `Escape` && x();
403
+ };
404
+ return document.addEventListener(`keydown`, e), () => {
405
+ document.removeEventListener(`keydown`, e);
406
+ };
407
+ }, [l, x]), (0, react.useEffect)(() => y, [y]);
408
+ let T = (0, react.createElement)(`button`, {
409
+ ref: g,
410
+ type: `button`,
411
+ className: `dsu-helpBtn`,
412
+ "aria-label": `${a.label} 说明`,
413
+ "aria-describedby": c,
414
+ "aria-pressed": d,
415
+ onClick: () => {
416
+ h.current ? x() : (w(!0), S());
417
+ }
418
+ }, (0, react.createElement)(`svg`, {
419
+ width: 12,
420
+ height: 12,
421
+ viewBox: `0 0 14 14`,
422
+ "aria-hidden": !0
423
+ }, (0, react.createElement)(`circle`, {
424
+ cx: 7,
425
+ cy: 7,
426
+ r: 6,
427
+ fill: `none`,
428
+ stroke: `currentColor`,
429
+ strokeWidth: 1.2
430
+ }), (0, react.createElement)(`path`, {
431
+ d: `M7 6.2v4`,
432
+ stroke: `currentColor`,
433
+ strokeWidth: 1.3,
434
+ strokeLinecap: `round`
435
+ }), (0, react.createElement)(`circle`, {
436
+ cx: 7,
437
+ cy: 4.3,
438
+ r: .8,
439
+ fill: `currentColor`
440
+ }))), E = (0, react.createElement)(`div`, {
441
+ ref: _,
442
+ id: c,
443
+ role: `tooltip`,
444
+ className: `dsu-tip`,
445
+ hidden: !l
446
+ }, a.detail), D = a.showLabel === !1 ? null : (0, react.createElement)(`span`, { className: `dsu-label${a.compact === !0 ? ` dsu-labelMono` : ``}` }, a.label);
447
+ return (0, react.createElement)(`span`, {
448
+ className: `dsu-help`,
449
+ onMouseEnter: () => {
450
+ p.current = !0, S();
451
+ },
452
+ onMouseLeave: () => {
453
+ p.current = !1, C();
454
+ },
455
+ onFocus: () => {
456
+ m.current = !0, S();
457
+ },
458
+ onBlur: (e) => {
459
+ let t = e.relatedTarget;
460
+ t instanceof Node && e.currentTarget.contains(t) || (m.current = !1, C());
461
+ }
462
+ }, D, T, E);
463
+ }
464
+ function h(t) {
465
+ 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`, {
466
+ className: n,
467
+ htmlFor: t.controlId
468
+ }, t.label);
469
+ return (0, react.createElement)(`div`, { className: `dsu-labelRow` }, r, t.detail === void 0 ? null : (0, react.createElement)(m, {
470
+ label: t.label,
471
+ detail: t.detail,
472
+ showLabel: !1
473
+ }));
474
+ }
475
+ function g(t) {
476
+ let n = t.controlId === void 0 ? void 0 : `${t.controlId}-hint`;
477
+ return (0, react.createElement)(`div`, { className: `dsu-field${t.wide === !0 ? ` dsu-fieldWide` : ``}` }, (0, react.createElement)(`div`, { className: `dsu-fieldMeta` }, (0, react.createElement)(h, {
478
+ label: t.label,
479
+ detail: t.detail,
480
+ compact: t.compact,
481
+ controlId: t.controlId
482
+ }), t.hint === void 0 ? null : (0, react.createElement)(`p`, {
483
+ className: `dsu-hint`,
484
+ id: n
485
+ }, t.hint)), (0, react.createElement)(`div`, { className: `dsu-control` }, t.children));
486
+ }
487
+ function _(t) {
488
+ let n = (0, react.useId)(), i = t.hint === void 0 ? void 0 : `${n}-hint`;
489
+ return (0, react.createElement)(g, {
490
+ ...t,
491
+ controlId: n
492
+ }, (0, react.createElement)(`button`, {
493
+ type: `button`,
494
+ role: `switch`,
495
+ id: n,
496
+ className: `dsu-switch`,
497
+ "aria-checked": t.checked,
498
+ "aria-describedby": i,
499
+ "data-field": t.field,
500
+ disabled: t.disabled === !0,
501
+ onClick: () => {
502
+ t.onChange(!t.checked);
503
+ }
504
+ }, (0, react.createElement)(`span`, { className: `dsu-knob` })));
505
+ }
506
+ function v(t) {
507
+ 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(` `);
508
+ (0, react.useEffect)(() => {
509
+ t.value !== d.current && (d.current = t.value, c(i), u(!1));
510
+ }, [t.value, i]);
511
+ let _ = (e) => {
512
+ let n = t.min ?? -1 / 0, r = t.max ?? 1 / 0;
513
+ if (!Number.isFinite(e) || e < n || e > r) return !1;
514
+ let { step: i } = t;
515
+ if (i === void 0 || i <= 0) return !0;
516
+ let a = (e - (t.min ?? 0)) / i;
517
+ return Math.abs(a - Math.round(a)) < 1e-9;
518
+ }, v = (e) => {
519
+ if (c(e), e === ``) {
520
+ u(!1), d.current = void 0, t.onChange(void 0);
521
+ return;
522
+ }
523
+ let n = Number(e);
524
+ _(n) ? (u(!1), d.current = n, t.onChange(n)) : u(!0);
525
+ }, y = () => {
526
+ a === `` || l || c(String(Number(a)));
527
+ };
528
+ return (0, react.createElement)(g, {
529
+ ...t,
530
+ controlId: f
531
+ }, (0, react.createElement)(`div`, { className: `dsu-numberWrap` }, (0, react.createElement)(`div`, { className: `dsu-number` }, (0, react.createElement)(`input`, {
532
+ className: `dsu-input`,
533
+ type: `number`,
534
+ inputMode: `numeric`,
535
+ id: f,
536
+ value: a,
537
+ min: t.min,
538
+ max: t.max,
539
+ step: t.step,
540
+ placeholder: t.placeholder,
541
+ "aria-invalid": l || void 0,
542
+ "aria-describedby": h,
543
+ "data-field": t.field,
544
+ disabled: t.disabled === !0,
545
+ onChange: (e) => {
546
+ v(e.target.value);
547
+ },
548
+ onBlur: y
549
+ }), t.unit === void 0 ? null : (0, react.createElement)(`span`, { className: `dsu-unit` }, t.unit)), l ? (0, react.createElement)(`p`, {
550
+ className: `dsu-error`,
551
+ id: p
552
+ }, t.invalidHint) : null));
553
+ }
554
+ function y(t) {
555
+ 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`;
556
+ (0, react.useEffect)(() => {
557
+ o || a(t.value);
558
+ }, [t.value, o]);
559
+ let d = {
560
+ id: l,
561
+ className: `dsu-input${t.multiline === !0 ? ` dsu-textarea` : ``}${t.monospace === !0 ? ` dsu-mono` : ``}`,
562
+ value: i,
563
+ placeholder: t.placeholder,
564
+ "aria-describedby": u,
565
+ "data-field": t.field,
566
+ disabled: t.disabled === !0,
567
+ onFocus: () => {
568
+ c(!0);
569
+ },
570
+ onBlur: () => {
571
+ c(!1);
572
+ },
573
+ onChange: (e) => {
574
+ a(e.target.value), t.onChange(e.target.value);
575
+ }
576
+ };
577
+ return (0, react.createElement)(g, {
578
+ ...t,
579
+ controlId: l
580
+ }, t.multiline === !0 ? (0, react.createElement)(`textarea`, d) : (0, react.createElement)(`input`, d));
581
+ }
582
+ function x(c) {
583
+ let [l, u] = (0, react.useState)(!1), [d, f] = (0, react.useState)(0), p = (0, react.useRef)(null), m = (0, react.useRef)(null), h = (0, react.useId)(), _ = (0, react.useId)(), v = c.hint === void 0 ? void 0 : `${_}-hint`, { disabled: y, value: b, options: x, onChange: S } = c, C = (0, react.useMemo)(() => x.map((e, t) => ({
584
+ option: e,
585
+ index: t
586
+ })).filter((e) => e.option.disabled !== !0), [x]), w = x.find((e) => e.value === b)?.label ?? b, T = x[d], E = T !== void 0 && T.disabled !== !0, D = (0, react.useCallback)(() => {
587
+ let e = p.current, t = m.current;
588
+ /* v8 ignore next 3 -- place 只在 open 的 layout effect 里调用;refs 为空只可能是卸载竞态 */
589
+ if (e === null || t === null) return;
590
+ let n = e.getBoundingClientRect(), r = Math.min(n.width, window.innerWidth - 16), { left: i } = n;
591
+ i + r > window.innerWidth - 8 && (i = window.innerWidth - r - 8);
592
+ let a = t.offsetHeight, o = n.bottom + 6;
593
+ o + a > window.innerHeight - 8 && (o = n.top - a - 6);
594
+ let s = Math.max(8, window.innerWidth - r - 8), c = Math.max(8, window.innerHeight - a - 8);
595
+ t.style.width = `${r}px`, t.style.left = `${Math.min(Math.max(8, i), s)}px`, t.style.top = `${Math.min(Math.max(8, o), c)}px`;
596
+ }, []), O = (0, react.useCallback)(() => {
597
+ u(!1);
598
+ }, []), k = (0, react.useCallback)(() => {
599
+ if (y === !0) return;
600
+ let e = C.find((e) => e.option.value === b) ?? C.at(0);
601
+ e !== void 0 && (f(e.index), u(!0));
602
+ }, [
603
+ y,
604
+ b,
605
+ C
606
+ ]), A = (0, react.useCallback)((e) => {
607
+ /* v8 ignore next 3 -- 浮层只能经由 openMenu 打开;组件禁用时打不开 */
608
+ if (y === !0) return;
609
+ let t = x[e];
610
+ /* v8 ignore next 3 -- disabled 选项在浏览器里不派发 click;越界下标由调用面保证 */
611
+ t !== void 0 && t.disabled !== !0 && (S(t.value), O(), p.current?.focus());
612
+ }, [
613
+ y,
614
+ x,
615
+ S,
616
+ O
617
+ ]), j = (0, react.useCallback)((e) => {
618
+ let t = C.findIndex((e) => e.index === d);
619
+ /* v8 ignore next 5 -- open 时归一 effect 已保证 enabled 非空且 active 落在可用项上 */
620
+ if (t === -1 || C.length === 0) return;
621
+ let n = (t + e + C.length) % C.length, r = C[n];
622
+ /* v8 ignore next 3 -- next 由取模保证在界内 */
623
+ r !== void 0 && f(r.index);
624
+ }, [d, C]);
625
+ (0, react.useLayoutEffect)(() => {
626
+ if (!l) return () => {};
627
+ D();
628
+ let e = (e) => {
629
+ let t = e.target;
630
+ m.current?.contains(t) !== !0 && p.current?.contains(t) !== !0 && O();
631
+ }, t = (e) => {
632
+ let { target: t } = e;
633
+ t instanceof Node && m.current?.contains(t) === !0 || O();
634
+ }, n = () => {
635
+ D();
636
+ };
637
+ return document.addEventListener(`pointerdown`, e, !0), window.addEventListener(`scroll`, t, !0), window.addEventListener(`resize`, n), () => {
638
+ document.removeEventListener(`pointerdown`, e, !0), window.removeEventListener(`scroll`, t, !0), window.removeEventListener(`resize`, n);
639
+ };
640
+ }, [
641
+ l,
642
+ c.options,
643
+ D,
644
+ O
645
+ ]), (0, react.useEffect)(() => {
646
+ if (!l) return;
647
+ if (y === !0 || C.length === 0) {
648
+ O();
649
+ return;
650
+ }
651
+ if (C.some((e) => e.index === d)) return;
652
+ let e = C.find((e) => e.option.value === b) ?? C.at(0);
653
+ /* v8 ignore next 3 -- enabled 非空在上一分支已保证 */
654
+ e !== void 0 && f(e.index);
655
+ }, [
656
+ l,
657
+ y,
658
+ b,
659
+ x,
660
+ d,
661
+ O,
662
+ C
663
+ ]), (0, react.useLayoutEffect)(() => {
664
+ if (!l) return;
665
+ let e = m.current;
666
+ /* v8 ignore next 2 -- 同上,menu 在 open 的 commit 之后必然已挂上 */
667
+ if (e === null) return;
668
+ let t = e.querySelector(`[data-active="true"]`);
669
+ /* v8 ignore next 2 -- active 已由归一 effect 保证落在可用项上 */
670
+ t !== null && (t.offsetTop < e.scrollTop ? e.scrollTop = t.offsetTop : t.offsetTop + t.offsetHeight > e.scrollTop + e.clientHeight && (e.scrollTop = t.offsetTop + t.offsetHeight - e.clientHeight));
671
+ }, [
672
+ l,
673
+ d,
674
+ x
675
+ ]);
676
+ let M = (e) => {
677
+ if (e.key === `ArrowDown` || e.key === `ArrowUp`) {
678
+ if (e.preventDefault(), !l) {
679
+ k();
680
+ return;
681
+ }
682
+ j(e.key === `ArrowDown` ? 1 : -1);
683
+ } else if (e.key === `Home` || e.key === `End`) {
684
+ if (e.preventDefault(), l || k(), C.length === 0) return;
685
+ let t = e.key === `Home` ? C.at(0) : C.at(-1);
686
+ /* v8 ignore next 3 -- enabled 为空已在上一条返回 */
687
+ if (t === void 0) return;
688
+ f(t.index);
689
+ } else if (e.key === `Enter` || e.key === ` `) {
690
+ if (e.preventDefault(), !l) {
691
+ k();
692
+ return;
693
+ }
694
+ A(d);
695
+ } else (e.key === `Tab` || e.key === `Escape`) && O();
696
+ }, N = [], P;
697
+ for (let [t, n] of x.entries()) n.group !== P && (n.group !== void 0 && N.push((0, react.createElement)(`div`, {
698
+ key: `group-${t}-${n.group}`,
699
+ className: `dsu-optionGroup`,
700
+ role: `presentation`
701
+ }, n.group)), P = n.group), N.push((0, react.createElement)(`button`, {
702
+ key: `option-${n.value}`,
703
+ type: `button`,
704
+ role: `option`,
705
+ id: `${h}-${t}`,
706
+ className: `dsu-option`,
707
+ tabIndex: -1,
708
+ "aria-selected": n.value === c.value,
709
+ "data-active": t === d,
710
+ disabled: n.disabled === !0,
711
+ onMouseDown: (e) => {
712
+ e.preventDefault();
713
+ },
714
+ onClick: () => {
715
+ A(t);
716
+ },
717
+ onMouseEnter: () => {
718
+ /* v8 ignore next -- happy-dom 对 disabled 元素不派发 mouse 事件;浏览器实现各有差异,保留禁用检查 */
719
+ n.disabled !== !0 && f(t);
720
+ }
721
+ }, (0, react.createElement)(`span`, null, n.label), (0, react.createElement)(`svg`, {
722
+ className: `dsu-check`,
723
+ width: 14,
724
+ height: 14,
725
+ viewBox: `0 0 14 14`,
726
+ "aria-hidden": !0
727
+ }, (0, react.createElement)(`path`, {
728
+ d: `M2.5 7.5l3 3 6-7`,
729
+ fill: `none`,
730
+ stroke: `currentColor`,
731
+ strokeWidth: 1.5,
732
+ strokeLinecap: `round`,
733
+ strokeLinejoin: `round`
734
+ }))));
735
+ return (0, react.createElement)(g, {
736
+ ...c,
737
+ controlId: _
738
+ }, (0, react.createElement)(`div`, {
739
+ className: `dsu-selectWrap`,
740
+ onBlur: (e) => {
741
+ let t = e.relatedTarget;
742
+ t instanceof Node && e.currentTarget.contains(t) || O();
743
+ }
744
+ }, (0, react.createElement)(`button`, {
745
+ ref: p,
746
+ type: `button`,
747
+ id: _,
748
+ className: `dsu-select dsu-input`,
749
+ role: `combobox`,
750
+ "aria-haspopup": `listbox`,
751
+ "aria-expanded": l,
752
+ "aria-controls": `${h}-list`,
753
+ "aria-activedescendant": l && E ? `${h}-${d}` : void 0,
754
+ "aria-describedby": v,
755
+ "data-field": c.field,
756
+ disabled: y === !0 || x.length === 0,
757
+ onClick: () => {
758
+ l ? O() : k();
759
+ },
760
+ onKeyDown: M
761
+ }, (0, react.createElement)(`span`, { className: `dsu-selectValue` }, w), (0, react.createElement)(`svg`, {
762
+ className: `dsu-chevron`,
763
+ width: 14,
764
+ height: 14,
765
+ viewBox: `0 0 14 14`,
766
+ "aria-hidden": !0
767
+ }, (0, react.createElement)(`path`, {
768
+ d: `M3 5l4 4 4-4`,
769
+ fill: `none`,
770
+ stroke: `currentColor`,
771
+ strokeWidth: 1.5,
772
+ strokeLinecap: `round`,
773
+ strokeLinejoin: `round`
774
+ }))), (0, react.createElement)(`div`, {
775
+ ref: m,
776
+ id: `${h}-list`,
777
+ role: `listbox`,
778
+ className: `dsu-menu`,
779
+ "aria-label": c.label,
780
+ hidden: !l
781
+ }, ...N)));
782
+ }
783
+ function S(t) {
784
+ let n = !t.writable || t.busy || !t.dirty, r;
785
+ return r = t.error === null ? t.writable ? t.dirty ? t.dirtyLabel : t.cleanLabel : t.readOnlyLabel : t.error, (0, react.createElement)(`div`, {
786
+ className: `dsu-savebar`,
787
+ "data-dirty": t.dirty
788
+ }, (0, react.createElement)(`button`, {
789
+ type: `button`,
790
+ className: `dsu-btn dsu-btnPrimary`,
791
+ "data-field": `save`,
792
+ disabled: n,
793
+ onClick: t.onSave
794
+ }, t.busy ? t.savingLabel : t.saveLabel), (0, react.createElement)(`button`, {
795
+ type: `button`,
796
+ className: `dsu-btn`,
797
+ "data-field": `discard`,
798
+ disabled: n,
799
+ onClick: t.onDiscard
800
+ }, t.revertLabel), (0, react.createElement)(`span`, { className: t.error === null ? `dsu-status` : `dsu-status dsu-statusError` }, r));
801
+ }
269
802
  // lib/remote-shape.ts
270
803
  /** 宿主服务键 = 客户端线上命名空间(两侧共用的唯一来源)。 */
271
804
  const OCR_REMOTE_NAMESPACE = "ocrReview";
@@ -282,6 +815,21 @@ const OCR_SERVICE_KEY = `remote.${OCR_REMOTE_NAMESPACE}`;
282
815
  /** 卡片所在 bundle 的包名(`$mount` 的 `package` 位与槽位键同源;唯一定义在
283
816
  * lib/remote-shape.ts,client-entry.ts 的槽位键引的是同一个常量)。 */
284
817
  const PACKAGE_NAME = OCR_BUNDLE_PACKAGE;
818
+ /** strict codec 的统一出口:失败交给 zod 的 `prettifyError`,读到的是一份带字段路径的说明。 */
819
+ function parseOrThrow(schema, value) {
820
+ const parsed = schema.safeParse(value);
821
+ if (!parsed.success) throw new TypeError(zod.z.prettifyError(parsed.error));
822
+ return parsed.data;
823
+ }
824
+ const SELECT_MESSAGE = "select request requires string provider and model";
825
+ const SELECT_SCHEMA = zod.z.object({
826
+ provider: zod.z.string({ error: SELECT_MESSAGE }),
827
+ model: zod.z.string({ error: SELECT_MESSAGE })
828
+ }, { error: "select request must be an object" });
829
+ const MCP_SAVE_MESSAGE = "mcp save request requires a string json";
830
+ const MCP_SAVE_SCHEMA = zod.z.object({ json: zod.z.string({ error: MCP_SAVE_MESSAGE }) }, { error: MCP_SAVE_MESSAGE });
831
+ const CHAT_GPT_MESSAGE = "chatgpt request requires a string model";
832
+ const CHAT_GPT_SCHEMA = zod.z.object({ model: zod.z.string({ error: CHAT_GPT_MESSAGE }) }, { error: CHAT_GPT_MESSAGE });
285
833
  /** 把宿主那九个方法挂到客户端远程面上的描述符(`id` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
286
834
  * 导出它只为一件事:`test/remote.test.ts` 拿它与宿主侧 `remoteMethods()` 的标记对质
287
835
  * —— 两侧分叉时卡片不会报错,只会"读不到",所以那条比较必须在门禁里。 */
@@ -309,13 +857,7 @@ const OCR_DESCRIPTORS = [
309
857
  mode: "strict",
310
858
  typeSymbol: `${PACKAGE_NAME}#select-request`,
311
859
  create: () => ({ parse(value) {
312
- if (!e$2(value)) throw new TypeError("select request must be an object");
313
- const { provider, model } = value;
314
- if (typeof provider !== "string" || typeof model !== "string") throw new TypeError("select request requires string provider and model");
315
- return {
316
- provider,
317
- model
318
- };
860
+ return parseOrThrow(SELECT_SCHEMA, value);
319
861
  } })
320
862
  }
321
863
  }],
@@ -371,8 +913,7 @@ const OCR_DESCRIPTORS = [
371
913
  mode: "strict",
372
914
  typeSymbol: `${PACKAGE_NAME}#mcp-save-request`,
373
915
  create: () => ({ parse(value) {
374
- if (!e$2(value) || typeof value["json"] !== "string") throw new TypeError("mcp save request requires a string json");
375
- return { json: value["json"] };
916
+ return parseOrThrow(MCP_SAVE_SCHEMA, value);
376
917
  } })
377
918
  }
378
919
  }],
@@ -401,8 +942,7 @@ const OCR_DESCRIPTORS = [
401
942
  mode: "strict",
402
943
  typeSymbol: `${PACKAGE_NAME}#chatgpt-request`,
403
944
  create: () => ({ parse(value) {
404
- if (!e$2(value) || typeof value["model"] !== "string") throw new TypeError("chatgpt request requires a string model");
405
- return { model: value["model"] };
945
+ return parseOrThrow(CHAT_GPT_SCHEMA, value);
406
946
  } })
407
947
  }
408
948
  }],
@@ -410,9 +950,9 @@ const OCR_DESCRIPTORS = [
410
950
  }
411
951
  ];
412
952
  /** 这个值能当远程面用吗(只核我们要调的那一位;服务来自别的实现或版本时保守当作没有)。 */
413
- const isRemoteMounter = (value) => e$2(value) && typeof value["$mount"] === "function";
953
+ const isRemoteMounter = (value) => e$3(value) && typeof value["$mount"] === "function";
414
954
  /** 同上:命名空间服务(挂载之后才有,缺它说明网关没认这枚贡献)。 */
415
- const isOcrNamespace = (value) => e$2(value) && typeof value["providers"] === "function" && typeof value["select"] === "function" && typeof value["migrate"] === "function" && typeof value["test"] === "function" && typeof value["probe"] === "function" && typeof value["mcp"] === "function" && typeof value["saveMcp"] === "function" && typeof value["auth"] === "function" && typeof value["chatGpt"] === "function";
955
+ const isOcrNamespace = (value) => e$3(value) && typeof value["providers"] === "function" && typeof value["select"] === "function" && typeof value["migrate"] === "function" && typeof value["test"] === "function" && typeof value["probe"] === "function" && typeof value["mcp"] === "function" && typeof value["saveMcp"] === "function" && typeof value["auth"] === "function" && typeof value["chatGpt"] === "function";
416
956
  /** 网关回执 → 值;载体/网关失败抛(卡片那层按「请求失败」显示)。 */
417
957
  function unwrap(result) {
418
958
  if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
@@ -524,35 +1064,65 @@ const BUNDLE_PKG = OCR_BUNDLE_PACKAGE;
524
1064
  function strOf(value) {
525
1065
  return typeof value === "string" ? value : "";
526
1066
  }
527
- /** 未知值 → 安全数字(非 number 给 fallback)。 */
528
- function numOf(value, fallback) {
529
- return typeof value === "number" ? value : fallback;
1067
+ /** 设置行的取值域:枚举/计数都交给 zod 声明,界面只消费解析后的类型。 */
1068
+ const EFFORT_SCHEMA = zod.z.enum([
1069
+ "auto",
1070
+ "low",
1071
+ "medium",
1072
+ "high"
1073
+ ]);
1074
+ const LANGUAGE_SCHEMA = zod.z.enum(["中文", "English"]);
1075
+ const COUNT_SCHEMA = zod.z.int().min(0);
1076
+ /** 未知值 → 安全数字(非整数/越界给 fallback)。 */
1077
+ function numOf(value, fallback, schema) {
1078
+ const parsed = schema.safeParse(value);
1079
+ return parsed.success ? parsed.data : fallback;
1080
+ }
1081
+ /** 未知值 → 枚举里的一个取值(域外给 fallback)。 */
1082
+ function textOf(value, schema, fallback) {
1083
+ const parsed = schema.safeParse(value);
1084
+ return parsed.success ? parsed.data : fallback;
1085
+ }
1086
+ /** 未知值 → 布尔(非布尔给 fallback)。 */
1087
+ function booleanOf(value, fallback) {
1088
+ const parsed = zod.z.boolean().safeParse(value);
1089
+ return parsed.success ? parsed.data : fallback;
530
1090
  }
531
1091
  /** 错误摘要(非 Error 抛出也要可读;catch 分支共用)。 */
532
1092
  function errorSummary(error) {
533
1093
  return String(error instanceof Error ? error.message : error);
534
1094
  }
535
- function modelOf(raw) {
536
- return {
537
- id: strOf(i(raw, "id")),
538
- name: strOf(i(raw, "name"))
539
- };
540
- }
541
- function providerOf(raw) {
542
- const modelsArr = i(raw, "models");
543
- const models = Array.isArray(modelsArr) ? modelsArr.map((item) => modelOf(item)) : [];
544
- const apiKeyEnvRaw = i(raw, "apiKeyEnv");
545
- return {
546
- name: strOf(i(raw, "name")),
547
- displayName: strOf(i(raw, "displayName")),
548
- apiKeyEnv: typeof apiKeyEnvRaw === "string" ? apiKeyEnvRaw : null,
549
- baseURL: strOf(i(raw, "baseURL")),
550
- models,
551
- hasKey: i(raw, "hasKey") === true
552
- };
553
- }
1095
+ const STRING_OR_EMPTY = zod.z.preprocess((value) => typeof value === "string" ? value : "", zod.z.string());
1096
+ const NULLABLE_STRING = zod.z.preprocess((value) => typeof value === "string" ? value : null, zod.z.string().nullable());
1097
+ const TRUE_ONLY = zod.z.preprocess((value) => value === true, zod.z.boolean());
1098
+ const MODEL_SCHEMA = zod.z.object({
1099
+ id: STRING_OR_EMPTY,
1100
+ name: STRING_OR_EMPTY
1101
+ });
1102
+ const PROVIDER_SCHEMA = zod.z.object({
1103
+ name: STRING_OR_EMPTY,
1104
+ displayName: STRING_OR_EMPTY,
1105
+ apiKeyEnv: NULLABLE_STRING,
1106
+ baseURL: STRING_OR_EMPTY,
1107
+ models: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(MODEL_SCHEMA)),
1108
+ hasKey: TRUE_ONLY
1109
+ });
1110
+ const CURRENT_SCHEMA = zod.z.object({
1111
+ provider: STRING_OR_EMPTY,
1112
+ model: STRING_OR_EMPTY,
1113
+ keyIsDynamic: TRUE_ONLY,
1114
+ url: STRING_OR_EMPTY
1115
+ });
1116
+ const SOURCE_SCHEMA = zod.z.object({
1117
+ status: STRING_OR_EMPTY,
1118
+ message: STRING_OR_EMPTY
1119
+ });
1120
+ const ENDPOINT_VERIFY_SCHEMA = zod.z.object({
1121
+ ok: TRUE_ONLY,
1122
+ message: STRING_OR_EMPTY
1123
+ });
554
1124
  /** 可得性来源的合法域(与 host 侧 `OcrBinarySource` 同一份取值)。形状不合即「没报」。 */
555
- const BINARY_SOURCES = /* @__PURE__ */ new Set([
1125
+ const BINARY_SOURCE_SCHEMA = zod.z.enum([
556
1126
  "bundled",
557
1127
  "path",
558
1128
  "none"
@@ -565,22 +1135,25 @@ const BINARY_SOURCES = /* @__PURE__ */ new Set([
565
1135
  * 作废——「宿主报了 PATH 却没报版本」仍然是一条用户要看的事实。`measuredVersion` 例外,
566
1136
  * 它恒在(null = 还没测过),因为「没测过」与「旧宿主不带这字段」在卡片上是同一句话。
567
1137
  */
1138
+ const BINARY_SCHEMA = zod.z.object({
1139
+ source: BINARY_SOURCE_SCHEMA,
1140
+ path: STRING_OR_EMPTY,
1141
+ channel: STRING_OR_EMPTY,
1142
+ expectedVersion: STRING_OR_EMPTY,
1143
+ installHint: STRING_OR_EMPTY,
1144
+ measuredVersion: NULLABLE_STRING
1145
+ });
568
1146
  function binaryOf(raw) {
569
- if (!e$2(raw)) return null;
570
- const source = strOf(raw["source"]);
571
- if (!BINARY_SOURCES.has(source)) return null;
572
- const path = strOf(raw["path"]);
573
- const channel = strOf(raw["channel"]);
574
- const expectedVersion = strOf(raw["expectedVersion"]);
575
- const installHint = strOf(raw["installHint"]);
576
- const measured = raw["measuredVersion"];
1147
+ const parsed = BINARY_SCHEMA.safeParse(raw);
1148
+ if (!parsed.success) return null;
1149
+ const { source, path, channel, expectedVersion, installHint, measuredVersion } = parsed.data;
577
1150
  return {
578
1151
  source,
579
1152
  ...path === "" ? {} : { path },
580
1153
  ...channel === "" ? {} : { channel },
581
1154
  ...expectedVersion === "" ? {} : { expectedVersion },
582
1155
  ...installHint === "" ? {} : { installHint },
583
- measuredVersion: typeof measured === "string" ? measured : null
1156
+ measuredVersion
584
1157
  };
585
1158
  }
586
1159
  /**
@@ -591,67 +1164,55 @@ function binaryOf(raw) {
591
1164
  async function loadCliStatus(reader) {
592
1165
  if (reader === void 0) return null;
593
1166
  try {
594
- return binaryOf(i(await reader.providers(), "ocrBinary"));
1167
+ return binaryOf(i$1(await reader.providers(), "ocrBinary"));
595
1168
  } catch {
596
1169
  return null;
597
1170
  }
598
1171
  }
1172
+ const EMPTY_PAYLOAD = {
1173
+ providers: [],
1174
+ current: {
1175
+ provider: "",
1176
+ model: "",
1177
+ keyIsDynamic: false,
1178
+ url: ""
1179
+ },
1180
+ getCredReady: false,
1181
+ source: {
1182
+ status: "",
1183
+ message: ""
1184
+ },
1185
+ endpointVerify: null,
1186
+ ocrBinary: null
1187
+ };
1188
+ const PROVIDERS_PAYLOAD_SCHEMA = zod.z.object({
1189
+ providers: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(PROVIDER_SCHEMA)),
1190
+ current: zod.z.preprocess((value) => e$3(value) ? value : {}, CURRENT_SCHEMA),
1191
+ getCredReady: TRUE_ONLY,
1192
+ source: zod.z.preprocess((value) => e$3(value) ? value : {}, SOURCE_SCHEMA),
1193
+ endpointVerify: zod.z.preprocess((value) => {
1194
+ const parsed = ENDPOINT_VERIFY_SCHEMA.safeParse(value);
1195
+ return parsed.success ? parsed.data : null;
1196
+ }, ENDPOINT_VERIFY_SCHEMA.nullable()),
1197
+ ocrBinary: zod.z.unknown().transform((value) => binaryOf(value))
1198
+ });
599
1199
  function providersPayloadOf(json) {
600
- const providersArr = i(json, "providers");
601
- const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
602
- const currentRaw = i(json, "current");
603
- const current = {
604
- provider: strOf(i(currentRaw, "provider")),
605
- model: strOf(i(currentRaw, "model")),
606
- keyIsDynamic: i(currentRaw, "keyIsDynamic") === true,
607
- url: strOf(i(currentRaw, "url"))
608
- };
609
- const sourceRaw = i(json, "source");
610
- const verifyRaw = i(json, "endpointVerify");
611
- const endpointVerify = verifyRaw === null || verifyRaw === void 0 ? null : {
612
- ok: i(verifyRaw, "ok") === true,
613
- message: strOf(i(verifyRaw, "message"))
614
- };
615
- return {
616
- providers,
617
- current,
618
- getCredReady: i(json, "getCredReady") === true,
619
- source: {
620
- status: strOf(i(sourceRaw, "status")),
621
- message: strOf(i(sourceRaw, "message"))
622
- },
623
- endpointVerify,
624
- ocrBinary: binaryOf(i(json, "ocrBinary"))
625
- };
1200
+ const parsed = PROVIDERS_PAYLOAD_SCHEMA.safeParse(json);
1201
+ return parsed.success ? parsed.data : EMPTY_PAYLOAD;
626
1202
  }
627
- /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
628
- * 抽出来是因为两处 input 与一处 select 必须同宽同边框:写三遍字面量时改一处漏两处,
629
- * 症状是设置卡里某一行的控件比其它行窄。 */
630
- const FORM_CONTROL_CLASS = "ocr-select";
631
- /** 小节标题的类名(卡片头、CLI 状态行、详情页区块共用;sonarjs 的重复字面量门禁)。 */
1203
+ /** 动作按钮行的类名(共享组件的行内按钮组;多处共用,见 sonarjs 的重复字面量门禁)。 */
1204
+ const ACTIONS_CLASS = "ocr-actions";
1205
+ /** 小节标题的类名(详情页区块共用;sonarjs 的重复字面量门禁)。 */
632
1206
  const CARD_NAME_CLASS = "ocr-card-name";
633
1207
  const CARD_CSS = [
634
- ".ocr-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}",
635
- ".ocr-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
636
- ".ocr-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
637
- ".ocr-card-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}",
638
- ".ocr-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
639
- ".ocr-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
640
- ".ocr-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
641
- ".ocr-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
642
- ".ocr-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
643
- ".ocr-card-chevron-open{transform:rotate(180deg)}",
644
- ".ocr-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:8px 0 12px;font-size:13px;line-height:1.5}",
645
- ".ocr-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}",
646
- ".ocr-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);margin-top:2px}",
647
- ".ocr-select{flex:none;width:min(46%,240px);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,#d8dbe2);border-radius:8px;padding:5px 8px}",
648
- ".ocr-btn{appearance:auto;font:inherit;font-size:12px;cursor:pointer;border-radius:6px;padding:4px 12px;border:1px solid var(--dsw-alias-border-l2,#d8dbe2);background:var(--dsw-alias-bg-layer-1,transparent);color:var(--dsw-alias-label-primary,inherit)}",
649
- ".ocr-btn:disabled{opacity:.5;cursor:not-allowed}",
650
- ".ocr-btn-primary{background:var(--dsw-alias-brand-primary,#2f6fed);border-color:var(--dsw-alias-brand-primary,#2f6fed);color:var(--dsw-alias-label-primary-foreground,#fff)}",
651
- ".ocr-status{font-size:12px;margin-top:6px;white-space:pre-wrap;word-break:break-word}",
652
- ".ocr-err{color:#c4483f}",
1208
+ ".ocr-card-name{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.4}",
1209
+ ".ocr-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5}",
1210
+ ".ocr-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
1211
+ ".ocr-status{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin:8px 0 0}",
1212
+ ".ocr-err{color:var(--dsw-alias-state-error-primary)}",
653
1213
  ".ocr-ok{color:var(--dsw-alias-label-primary,inherit)}",
654
- ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}"
1214
+ ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}",
1215
+ ".ocr-adminTextarea{min-height:160px}"
655
1216
  ].join("\n");
656
1217
  function cardStore(scope) {
657
1218
  let cachedSnap = null;
@@ -679,190 +1240,6 @@ function cardStore(scope) {
679
1240
  }
680
1241
  };
681
1242
  }
682
- function SaveBar(props) {
683
- const { t } = props;
684
- const disabled = !props.writable || props.busy;
685
- const btnStyle = {
686
- appearance: "auto",
687
- font: "inherit",
688
- fontSize: "12px",
689
- cursor: disabled || !props.dirty ? "not-allowed" : "pointer",
690
- borderRadius: "6px",
691
- padding: "4px 12px",
692
- border: "1px solid var(--dsw-alias-border-l2,#d8dbe2)",
693
- background: "var(--dsw-alias-bg-layer-1,transparent)",
694
- color: "var(--dsw-alias-label-primary,inherit)",
695
- opacity: disabled ? "0.5" : "1"
696
- };
697
- const saveBtn = (0, react.createElement)("button", {
698
- type: "button",
699
- "data-field": "save",
700
- disabled: disabled || !props.dirty,
701
- onClick: props.onSave,
702
- style: {
703
- ...btnStyle,
704
- background: "var(--dsw-alias-brand-primary,#2f6fed)",
705
- borderColor: "var(--dsw-alias-brand-primary,#2f6fed)",
706
- color: "var(--dsw-alias-label-primary-foreground,#fff)"
707
- }
708
- }, props.busy ? t("saving") : t("save"));
709
- const discardBtn = (0, react.createElement)("button", {
710
- type: "button",
711
- "data-field": "discard",
712
- disabled: disabled || !props.dirty,
713
- onClick: props.onDiscard,
714
- style: btnStyle
715
- }, t("revert"));
716
- const dirtyText = props.dirty ? t("statusDirty") : t("statusClean");
717
- let statusNode;
718
- if (props.error !== null) statusNode = (0, react.createElement)("span", { style: {
719
- fontSize: 12,
720
- color: "#c4483f"
721
- } }, props.error);
722
- else if (props.writable) statusNode = (0, react.createElement)("span", { style: {
723
- fontSize: 12,
724
- color: "var(--dsw-alias-label-tertiary,#8a8f99)"
725
- } }, dirtyText);
726
- else statusNode = (0, react.createElement)("span", { style: {
727
- fontSize: 12,
728
- color: "var(--dsw-alias-label-tertiary,#8a8f99)"
729
- } }, t("statusReadOnly"));
730
- return (0, react.createElement)("div", { style: {
731
- display: "flex",
732
- gap: 8,
733
- alignItems: "center",
734
- padding: "10px 0 2px",
735
- borderTop: "1px dashed var(--dsw-alias-border-l2,#d8dbe2)",
736
- marginTop: 6,
737
- flexWrap: "wrap"
738
- } }, saveBtn, discardBtn, statusNode);
739
- }
740
- /** 设置卡外壳:复刻同款自研卡片(边框圆角卡片 + 可折叠 header + chevron)。 */
741
- function OcrPluginCard(props) {
742
- const { title, description, children } = props;
743
- const [open, setOpen] = (0, react.useState)(false);
744
- const pathKey = "d";
745
- const headRow = (0, react.createElement)("span", { className: "ocr-card-head" }, (0, react.createElement)("span", { className: CARD_NAME_CLASS }, title), (0, react.createElement)("span", { className: "ocr-card-desc" }, description));
746
- const chevron = (0, react.createElement)("svg", {
747
- width: 14,
748
- height: 14,
749
- viewBox: "0 0 14 14",
750
- "aria-hidden": true,
751
- className: `ocr-card-chevron${open ? " ocr-card-chevron-open" : ""}`
752
- }, (0, react.createElement)("path", {
753
- [pathKey]: "M3 5l4 4 4-4",
754
- fill: "none",
755
- stroke: "currentColor",
756
- strokeWidth: 1.5,
757
- strokeLinecap: "round",
758
- strokeLinejoin: "round"
759
- }));
760
- const header = (0, react.createElement)("button", {
761
- type: "button",
762
- className: "ocr-card-header",
763
- "aria-expanded": open,
764
- onClick: () => {
765
- setOpen(!open);
766
- }
767
- }, headRow, chevron);
768
- const body = open ? (0, react.createElement)("div", { className: "ocr-card-body" }, children) : null;
769
- return (0, react.createElement)("div", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
770
- }
771
- /** 整数数字输入行(保存条模式:合法输入上报草稿,不直写)。 */
772
- function NumberRow(props) {
773
- const { label, hint, value, min, max, disabled, onCommit } = props;
774
- const [draft, setDraft] = (0, react.useState)(String(value));
775
- (0, react.useEffect)(() => {
776
- setDraft(String(value));
777
- }, [value]);
778
- /** 用**本次输入的值**上报,不读 `draft`:state 更新是异步的,`onChange` 里
779
- * `setDraft(x)` 之后立刻读 `draft` 拿到的还是上一个字符,草稿会永远滞后一格。 */
780
- const stage = (text) => {
781
- const parsed = Number(text);
782
- if (Number.isInteger(parsed) && parsed >= min && (max === void 0 || parsed <= max)) onCommit(parsed);
783
- };
784
- return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
785
- minWidth: 0,
786
- marginRight: 12
787
- } }, (0, react.createElement)("div", {}, label), (0, react.createElement)("div", { className: "ocr-hint" }, hint)), (0, react.createElement)("input", {
788
- type: "number",
789
- "data-field": props.field,
790
- value: draft,
791
- min,
792
- max,
793
- disabled,
794
- "aria-label": label,
795
- onChange: (ev) => {
796
- setDraft(ev.target.value);
797
- stage(ev.target.value);
798
- },
799
- onKeyDown: (ev) => {
800
- if (ev.key === "Enter") {
801
- ev.preventDefault();
802
- stage(draft);
803
- }
804
- },
805
- className: FORM_CONTROL_CLASS
806
- }));
807
- }
808
- /** 文本输入行(保存条模式:改动只上报草稿)。空串是合法取值(= 用宿主默认位置),
809
- * 所以不像 NumberRow 那样要求「解析成功」才上报。 */
810
- function TextRow(props) {
811
- const { label, hint, value, placeholder, disabled, onCommit } = props;
812
- const [draft, setDraft] = (0, react.useState)(value);
813
- (0, react.useEffect)(() => {
814
- setDraft(value);
815
- }, [value]);
816
- /** 同 NumberRow:读参数而不是 `draft`,否则草稿永远滞后一格。 */
817
- const stage = (text) => {
818
- onCommit(text.trim());
819
- };
820
- return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
821
- minWidth: 0,
822
- marginRight: 12
823
- } }, (0, react.createElement)("div", {}, label), (0, react.createElement)("div", { className: "ocr-hint" }, hint)), (0, react.createElement)("input", {
824
- type: "text",
825
- "data-field": props.field,
826
- value: draft,
827
- placeholder,
828
- disabled,
829
- "aria-label": label,
830
- className: FORM_CONTROL_CLASS,
831
- style: { width: "min(60%,320px)" },
832
- onChange: (ev) => {
833
- setDraft(ev.target.value);
834
- stage(ev.target.value);
835
- },
836
- onKeyDown: (ev) => {
837
- if (ev.key === "Enter") {
838
- ev.preventDefault();
839
- stage(draft);
840
- }
841
- }
842
- }));
843
- }
844
- function SelectRow(props) {
845
- const current = typeof props.current === "string" ? props.current : "";
846
- const options = props.options.map((option) => (0, react.createElement)("option", {
847
- key: option.value,
848
- value: option.value,
849
- disabled: option.disabled === true
850
- }, option.label));
851
- const labelBlock = (0, react.createElement)("div", { style: {
852
- minWidth: 0,
853
- marginRight: 12
854
- } }, (0, react.createElement)("div", {}, props.label), (0, react.createElement)("div", { className: "ocr-hint" }, props.hint));
855
- const select = (0, react.createElement)("select", {
856
- className: FORM_CONTROL_CLASS,
857
- "aria-label": props.label,
858
- value: current,
859
- disabled: props.disabled,
860
- onChange: (ev) => {
861
- props.onChange(props.field, ev.target.value);
862
- }
863
- }, options);
864
- return (0, react.createElement)("div", { className: "ocr-row" }, labelBlock, select);
865
- }
866
1243
  /** provider 下拉项文本(没有 key 的加后缀并被禁用)。提成模块级函数是为了不把
867
1244
  * 带插值的 t() 嵌进 createElement(unicorn/max-nested-calls 上限 3)。 */
868
1245
  function providerOptionLabel(t, provider) {
@@ -939,35 +1316,30 @@ function statusClass(ok) {
939
1316
  */
940
1317
  function buildCliStatusRow(deps) {
941
1318
  const { t } = deps;
942
- const label = (0, react.createElement)("div", { className: CARD_NAME_CLASS }, t("binaryTitle"));
943
1319
  const button = (0, react.createElement)("button", {
944
- className: "ocr-btn",
1320
+ className: "dsu-btn",
945
1321
  type: "button",
946
1322
  disabled: deps.busy !== null,
947
1323
  onClick: deps.onProbe,
948
1324
  "data-field": "probe"
949
1325
  }, deps.busy === "probe" ? t("detecting") : t("detectCli"));
950
- const row = (0, react.createElement)("div", {
951
- className: "ocr-row",
952
- style: {
953
- justifyContent: "flex-start",
954
- gap: 8
955
- }
956
- }, label, button);
957
- const statusText = binaryStatusText(t, deps.binary);
958
- const status = (0, react.createElement)("div", { className: "ocr-hint" }, statusText);
1326
+ const controls = (0, react.createElement)("div", {
1327
+ className: ACTIONS_CLASS,
1328
+ "data-field": "cli-status"
1329
+ }, button);
1330
+ const status = (0, react.createElement)("p", { className: "dsu-hint" }, binaryStatusText(t, deps.binary));
959
1331
  const versionText = cliVersionText(t, deps.binary);
960
- const versionLine = versionText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, versionText);
1332
+ const versionLine = versionText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, versionText);
961
1333
  const hintText = cliInstallText(deps.binary);
962
- const installLine = hintText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, hintText);
1334
+ const installLine = hintText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, hintText);
963
1335
  const probe = deps.probeMsg ? (0, react.createElement)("div", {
964
1336
  className: statusClass(deps.probeMsg.ok),
965
1337
  "data-field": "probe-result"
966
1338
  }, deps.probeMsg.text) : null;
967
- return (0, react.createElement)("div", {
968
- style: { padding: "2px 0" },
969
- "data-field": "cli-status"
970
- }, row, status, versionLine, installLine, probe);
1339
+ return (0, react.createElement)(d, {
1340
+ title: t("binaryTitle"),
1341
+ hint: t("binaryProbeHint")
1342
+ }, status, versionLine, installLine, controls, probe);
971
1343
  }
972
1344
  /**
973
1345
  * 徽标用的短文案(标题旁那只标签放不下整句装法)。与 `binaryStatusText` 同一判据、
@@ -1095,100 +1467,165 @@ function OcrCliActivation(props) {
1095
1467
  if (!mine || status?.source !== "none") return null;
1096
1468
  return buildCliActivation(props.t, props, cliInstallText(status));
1097
1469
  }
1098
- /** 数据齐(source.status === "ready")那一支:provider/model 两行 + 三个动作按钮 +
1099
- * 当前配置与 key 两行提示。拆出来是因为这一支画的是整块表单,与上面三条「一行提示」
1100
- * 的降级支本就不是同一件事。 */
1101
- function buildReadyProviderSection(deps, data) {
1470
+ /** 数据齐时的动作行:三个按钮共用一只在飞旗标,按钮文案只跟**自己**那条走。 */
1471
+ function buildActionField(deps) {
1102
1472
  const { t } = deps;
1103
- const providerOptions = data.providers.map((provider) => ({
1104
- value: provider.name,
1105
- label: providerOptionLabel(t, provider),
1106
- disabled: !provider.hasKey
1107
- }));
1108
- const fallbackModelName = deps.effectiveModel === "" ? t("noModel") : deps.effectiveModel;
1109
- const modelOptions = (deps.modelOptions.length > 0 ? deps.modelOptions : [{
1110
- id: deps.effectiveModel,
1111
- name: fallbackModelName
1112
- }]).map((model) => ({
1113
- value: model.id,
1114
- label: model.name
1115
- }));
1116
- const providerRow = (0, react.createElement)(SelectRow, {
1117
- label: "Provider",
1118
- hint: t("providerHint"),
1119
- field: "ocrProvider",
1120
- current: deps.effectiveProvider,
1121
- disabled: data.providers.length === 0,
1122
- options: providerOptions,
1123
- onChange: (field, value) => {
1124
- deps.onPickProvider(value);
1125
- }
1126
- });
1127
- const modelRow = (0, react.createElement)(SelectRow, {
1128
- label: "Model",
1129
- hint: deps.chosenProvider ? deps.chosenProvider.baseURL : "",
1130
- field: "ocrModel",
1131
- current: deps.effectiveModel,
1132
- disabled: !deps.chosenProvider || deps.modelOptions.length === 0,
1133
- options: modelOptions,
1134
- onChange: (field, value) => {
1135
- deps.onPickModel(value);
1136
- }
1137
- });
1138
1473
  const applyBtn = (0, react.createElement)("button", {
1139
- className: "ocr-btn ocr-btn-primary",
1474
+ className: "dsu-btn dsu-btnPrimary",
1140
1475
  type: "button",
1141
1476
  disabled: deps.busy !== null,
1142
1477
  onClick: deps.onApply,
1143
1478
  "data-field": "apply"
1144
1479
  }, deps.busy === "apply" ? t("applying") : t("applyToOcr"));
1145
1480
  const testBtn = (0, react.createElement)("button", {
1146
- className: "ocr-btn",
1481
+ className: "dsu-btn",
1147
1482
  type: "button",
1148
1483
  disabled: deps.busy !== null,
1149
1484
  onClick: deps.onTest,
1150
1485
  "data-field": "test"
1151
1486
  }, t("testConnection"));
1152
1487
  const migrateBtn = (0, react.createElement)("button", {
1153
- className: "ocr-btn",
1488
+ className: "dsu-btn",
1154
1489
  type: "button",
1155
1490
  disabled: deps.busy !== null,
1156
1491
  onClick: deps.onMigrate,
1157
1492
  "data-field": "migrate"
1158
1493
  }, t("migrateKeys"));
1159
- const actionRow = (0, react.createElement)("div", {
1160
- className: "ocr-row",
1161
- style: {
1162
- justifyContent: "flex-start",
1163
- gap: 8
1164
- }
1165
- }, applyBtn, testBtn, migrateBtn);
1494
+ return (0, react.createElement)(g, {
1495
+ label: t("actionsLabel"),
1496
+ wide: true
1497
+ }, (0, react.createElement)("div", { className: ACTIONS_CLASS }, applyBtn, testBtn, migrateBtn));
1498
+ }
1499
+ /** 当前配置 / key / 装载期自检三行提示(自检失败只作提示,不挡表单)。 */
1500
+ function buildProviderInfo(deps, data) {
1501
+ const { t } = deps;
1166
1502
  const cur = data.current;
1167
1503
  const currentHint = cur.provider === "" ? t("noProvider") : currentConfigText(t, cur);
1168
- const currentInfo = (0, react.createElement)("div", { className: "ocr-hint" }, currentHint);
1169
- const keyInfo = (0, react.createElement)("div", { className: "ocr-hint" }, deps.keyHint);
1504
+ const currentInfo = (0, react.createElement)("p", { className: "dsu-hint" }, currentHint);
1505
+ const keyInfo = (0, react.createElement)("p", { className: "dsu-hint" }, deps.keyHint);
1170
1506
  const verify = data.endpointVerify ?? null;
1171
- const verifyWarning = verify !== null && !verify.ok ? (0, react.createElement)("div", {
1172
- className: STATUS_ERROR_CLASS,
1173
- "data-field": "endpoint-verify"
1174
- }, t("endpointVerifyFailed", { reason: verify.message })) : null;
1175
- return (0, react.createElement)("div", {}, verifyWarning, providerRow, modelRow, actionRow, currentInfo, keyInfo);
1507
+ return [
1508
+ verify !== null && !verify.ok ? (0, react.createElement)("p", {
1509
+ className: "dsu-error",
1510
+ "data-field": "endpoint-verify"
1511
+ }, t("endpointVerifyFailed", { reason: verify.message })) : null,
1512
+ currentInfo,
1513
+ keyInfo
1514
+ ];
1515
+ }
1516
+ /** 数据齐(source.status === "ready")那一支:模型四个字段 + 动作行 + 三行提示。 */
1517
+ function buildReadyProviderSection(deps, data) {
1518
+ const { t } = deps;
1519
+ const providerOptions = data.providers.map((provider) => ({
1520
+ value: provider.name,
1521
+ label: providerOptionLabel(t, provider),
1522
+ disabled: !provider.hasKey
1523
+ }));
1524
+ const fallbackModelName = deps.effectiveModel === "" ? t("noModel") : deps.effectiveModel;
1525
+ const modelOptions = (deps.modelOptions.length > 0 ? deps.modelOptions : [{
1526
+ id: deps.effectiveModel,
1527
+ name: fallbackModelName
1528
+ }]).map((model) => ({
1529
+ value: model.id,
1530
+ label: model.name
1531
+ }));
1532
+ const effortOptions = [
1533
+ {
1534
+ value: "auto",
1535
+ label: t("effortAuto")
1536
+ },
1537
+ {
1538
+ value: "low",
1539
+ label: t("effortLow")
1540
+ },
1541
+ {
1542
+ value: "medium",
1543
+ label: t("effortMedium")
1544
+ },
1545
+ {
1546
+ value: "high",
1547
+ label: t("effortHigh")
1548
+ }
1549
+ ];
1550
+ const languageOptions = [{
1551
+ value: "中文",
1552
+ label: "中文"
1553
+ }, {
1554
+ value: "English",
1555
+ label: "English"
1556
+ }];
1557
+ const modelUrl = deps.chosenProvider?.baseURL ?? "";
1558
+ return [
1559
+ (0, react.createElement)(x, {
1560
+ label: "Provider",
1561
+ ...l(t("providerHint")),
1562
+ field: "ocrProvider",
1563
+ value: deps.effectiveProvider,
1564
+ options: providerOptions,
1565
+ disabled: data.providers.length === 0,
1566
+ onChange: deps.onPickProvider
1567
+ }),
1568
+ (0, react.createElement)(x, {
1569
+ label: "Model",
1570
+ ...modelUrl === "" ? {} : { hint: modelUrl },
1571
+ field: "ocrModel",
1572
+ value: deps.effectiveModel,
1573
+ options: modelOptions,
1574
+ disabled: !deps.chosenProvider || deps.modelOptions.length === 0,
1575
+ onChange: deps.onPickModel
1576
+ }),
1577
+ (0, react.createElement)(x, {
1578
+ label: t("effortLabel"),
1579
+ ...l(t("effortHint")),
1580
+ field: "effort",
1581
+ value: deps.effortValue,
1582
+ options: effortOptions,
1583
+ disabled: !deps.writable,
1584
+ onChange: (next) => {
1585
+ deps.onField("effort", next);
1586
+ }
1587
+ }),
1588
+ (0, react.createElement)(x, {
1589
+ label: t("languageLabel"),
1590
+ ...l(t("languageHint")),
1591
+ field: "language",
1592
+ value: deps.languageValue,
1593
+ options: languageOptions,
1594
+ disabled: !deps.writable,
1595
+ onChange: (next) => {
1596
+ deps.onField("language", next);
1597
+ }
1598
+ }),
1599
+ (0, react.createElement)(_, {
1600
+ label: t("autoVerifyLabel"),
1601
+ ...l(t("autoVerifyHint")),
1602
+ field: "autoVerify",
1603
+ checked: deps.autoVerifyValue,
1604
+ disabled: !deps.writable,
1605
+ onChange: (next) => {
1606
+ deps.onField("autoVerify", next);
1607
+ }
1608
+ }),
1609
+ buildActionField(deps),
1610
+ ...buildProviderInfo(deps, data)
1611
+ ];
1176
1612
  }
1177
1613
  /** Provider / Model 选择与应用区:三条降级各出一行提示(数据未就绪/读取失败/
1178
1614
  * 数据源不可用),只有 source.status === "ready" 才交给 buildReadyProviderSection 画表单。 */
1179
1615
  function buildProviderSection(deps) {
1180
1616
  const { t } = deps;
1617
+ let body;
1181
1618
  if (deps.fetchError !== null) {
1182
1619
  const reason = t("loadProvidersFailed", { error: deps.fetchError });
1183
- return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, reason);
1184
- }
1185
- if (deps.data === null) return (0, react.createElement)("div", { className: "ocr-status ocr-muted" }, t("loadingProviders"));
1186
- if (deps.data.source.status !== "ready") {
1620
+ body = (0, react.createElement)("p", { className: "dsu-error" }, reason);
1621
+ } else if (deps.data === null) body = (0, react.createElement)("p", { className: "dsu-hint" }, t("loadingProviders"));
1622
+ else if (deps.data.source.status === "ready") body = buildReadyProviderSection(deps, deps.data);
1623
+ else {
1187
1624
  const sourceMessage = deps.data.source.message;
1188
- const line = t("readProvidersFailed", { reason: sourceMessage === "" ? t("noReason") : sourceMessage });
1189
- return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, line);
1625
+ const reason = sourceMessage === "" ? t("noReason") : sourceMessage;
1626
+ body = (0, react.createElement)("p", { className: "dsu-error" }, t("readProvidersFailed", { reason }));
1190
1627
  }
1191
- return buildReadyProviderSection(deps, deps.data);
1628
+ return (0, react.createElement)(d, { title: t("modelSection") }, body);
1192
1629
  }
1193
1630
  /** provider 预选:显式选择 > 当前 OCR 配置(仍存在)> 第一个有 key 的 provider。 */
1194
1631
  function resolveBackend(providerList, current, selProvider) {
@@ -1218,157 +1655,95 @@ function resolveSelection(providerList, current, selProvider, selModel) {
1218
1655
  modelOptions: model.modelOptions
1219
1656
  };
1220
1657
  }
1221
- /** 设置行(effort/language/autoVerify + 保存条)。 */
1658
+ /** 审查参数小节:评论条数、运行超时、配置文件路径 + 保存条。 */
1222
1659
  function buildSettingsSection(deps, writable, setField) {
1223
1660
  const { t } = deps;
1224
- const effortRow = (0, react.createElement)(SelectRow, {
1225
- label: t("effortLabel"),
1226
- hint: t("effortHint"),
1227
- field: "effort",
1228
- current: deps.effortValue,
1229
- disabled: !writable,
1230
- options: [
1231
- {
1232
- value: "auto",
1233
- label: t("effortAuto")
1234
- },
1235
- {
1236
- value: "low",
1237
- label: t("effortLow")
1238
- },
1239
- {
1240
- value: "medium",
1241
- label: t("effortMedium")
1242
- },
1243
- {
1244
- value: "high",
1245
- label: t("effortHigh")
1246
- }
1247
- ],
1248
- onChange: (field, draft) => {
1249
- setField(field, draft);
1250
- }
1251
- });
1252
- const languageRow = (0, react.createElement)(SelectRow, {
1253
- label: t("languageLabel"),
1254
- hint: t("languageHint"),
1255
- field: "language",
1256
- current: deps.languageValue,
1257
- disabled: !writable,
1258
- options: [{
1259
- value: "中文",
1260
- label: "中文"
1261
- }, {
1262
- value: "English",
1263
- label: "English"
1264
- }],
1265
- onChange: (field, draft) => {
1266
- setField(field, draft);
1267
- }
1268
- });
1269
- const verifyRow = (0, react.createElement)(SelectRow, {
1270
- label: t("autoVerifyLabel"),
1271
- hint: t("autoVerifyHint"),
1272
- field: "autoVerify",
1273
- current: deps.autoVerifyValue === true ? "on" : "off",
1274
- disabled: !writable,
1275
- options: [{
1276
- value: "on",
1277
- label: t("onLabel")
1278
- }, {
1279
- value: "off",
1280
- label: t("offLabel")
1281
- }],
1282
- onChange: (field, draft) => {
1283
- setField(field, draft === "on");
1284
- }
1285
- });
1286
- const maxCommentsRow = (0, react.createElement)(NumberRow, {
1661
+ const maxCommentsRow = (0, react.createElement)(v, {
1287
1662
  label: t("maxCommentsLabel"),
1288
- hint: t("maxCommentsHint"),
1663
+ ...l(t("maxCommentsHint")),
1289
1664
  field: "maxComments",
1290
1665
  value: deps.maxCommentsValue,
1291
- min: 0,
1666
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1667
+ step: 1,
1668
+ invalidHint: t("outOfRange"),
1292
1669
  disabled: !writable,
1293
- onCommit: (num) => {
1294
- setField("maxComments", num);
1670
+ onChange: (next) => {
1671
+ setField("maxComments", next);
1295
1672
  }
1296
1673
  });
1297
- const timeoutRow = (0, react.createElement)(NumberRow, {
1674
+ const timeoutRow = (0, react.createElement)(v, {
1298
1675
  label: t("timeoutLabel"),
1299
- hint: t("timeoutHint"),
1676
+ ...l(t("timeoutHint")),
1300
1677
  field: "timeoutMinutes",
1301
1678
  value: deps.timeoutMinutesValue,
1302
- min: 0,
1679
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1680
+ step: 1,
1681
+ invalidHint: t("outOfRange"),
1303
1682
  disabled: !writable,
1304
- onCommit: (num) => {
1305
- setField("timeoutMinutes", num);
1683
+ onChange: (next) => {
1684
+ setField("timeoutMinutes", next);
1306
1685
  }
1307
1686
  });
1308
- const configPathRow = (0, react.createElement)(TextRow, {
1687
+ const configPathRow = (0, react.createElement)(y, {
1309
1688
  label: t("configPathLabel"),
1310
- hint: t("configPathHint"),
1689
+ ...l(t("configPathHint")),
1311
1690
  field: "ocrConfigPath",
1312
1691
  value: deps.ocrConfigPathValue,
1313
1692
  placeholder: "~/.opencodereview/config.json",
1693
+ monospace: true,
1694
+ wide: true,
1314
1695
  disabled: !writable,
1315
- onCommit: (text) => {
1316
- setField("ocrConfigPath", text);
1696
+ onChange: (next) => {
1697
+ setField("ocrConfigPath", next.trim());
1317
1698
  }
1318
1699
  });
1319
- const saveRow = (0, react.createElement)(SaveBar, {
1320
- t,
1700
+ const saveRow = (0, react.createElement)(S, {
1321
1701
  dirty: deps.dirty,
1322
1702
  writable,
1323
1703
  busy: deps.settingsBusy,
1324
1704
  error: deps.saveError,
1705
+ saveLabel: t("save"),
1706
+ savingLabel: t("saving"),
1707
+ revertLabel: t("revert"),
1708
+ dirtyLabel: t("statusDirty"),
1709
+ cleanLabel: t("statusClean"),
1710
+ readOnlyLabel: t("statusReadOnly"),
1325
1711
  onSave: deps.onSave,
1326
1712
  onDiscard: deps.onDiscard
1327
1713
  });
1328
- const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("div", { className: "ocr-hint" }, deps.statusHint) : null;
1329
- return (0, react.createElement)("div", { style: { padding: "2px 0" } }, statusHintNode, effortRow, languageRow, verifyRow, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1714
+ const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("p", { className: "dsu-hint" }, deps.statusHint) : null;
1715
+ return (0, react.createElement)(d, { title: t("reviewSection") }, statusHintNode, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1330
1716
  }
1331
- const sectionStyle = { padding: "6px 0" };
1332
- const titleStyle = {
1333
- fontWeight: "600",
1334
- fontSize: "13px"
1335
- };
1336
- const hintStyle = {
1337
- fontSize: "12px",
1338
- opacity: "0.8",
1339
- margin: "2px 0 6px"
1340
- };
1341
- const textareaStyle = {
1342
- width: "100%",
1343
- minHeight: "120px",
1344
- fontFamily: "monospace",
1345
- fontSize: "12px",
1346
- boxSizing: "border-box"
1347
- };
1348
1717
  function buildMcpAdmin(props) {
1349
1718
  const { t } = props;
1350
1719
  const textarea = (0, react.createElement)("textarea", {
1351
1720
  "data-field": "mcpJson",
1352
1721
  value: props.mcpJson,
1353
1722
  disabled: !props.writable || props.mcpBusy || !props.available,
1723
+ "aria-label": t("mcpTitle"),
1724
+ className: "dsu-input dsu-textarea dsu-mono ocr-adminTextarea",
1354
1725
  onChange: (event) => {
1355
1726
  props.onMcpChange(event.target.value);
1356
- },
1357
- style: textareaStyle
1727
+ }
1358
1728
  });
1359
1729
  const button = (0, react.createElement)("button", {
1360
1730
  type: "button",
1731
+ className: "dsu-btn",
1361
1732
  "data-field": "mcpSave",
1362
1733
  disabled: !props.writable || props.mcpBusy || !props.available,
1363
1734
  onClick: props.onMcpSave
1364
1735
  }, props.mcpBusy ? t("mcpSaving") : t("mcpSave"));
1365
- const message = props.mcpMsg === null ? null : (0, react.createElement)("div", { className: "ocr-hint" }, props.mcpMsg);
1366
- return (0, react.createElement)("div", { style: sectionStyle }, (0, react.createElement)("div", { style: titleStyle }, t("mcpTitle")), (0, react.createElement)("div", { style: hintStyle }, t("mcpHint")), props.available ? textarea : (0, react.createElement)("div", { className: "ocr-hint" }, t("adminUnavailable")), button, message);
1736
+ const message = props.mcpMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.mcpMsg);
1737
+ return (0, react.createElement)(d, {
1738
+ title: t("mcpTitle"),
1739
+ hint: t("mcpHint")
1740
+ }, props.available ? textarea : (0, react.createElement)("p", { className: "dsu-hint" }, t("adminUnavailable")), (0, react.createElement)("div", { className: ACTIONS_CLASS }, button), message);
1367
1741
  }
1368
1742
  function buildAuthAdmin(props) {
1369
1743
  const { t } = props;
1370
1744
  const check = (0, react.createElement)("button", {
1371
1745
  type: "button",
1746
+ className: "dsu-btn",
1372
1747
  "data-field": "authCheck",
1373
1748
  disabled: props.authBusy || !props.available,
1374
1749
  onClick: props.onCheckAuth
@@ -1377,23 +1752,29 @@ function buildAuthAdmin(props) {
1377
1752
  "data-field": "chatGptModel",
1378
1753
  value: props.authModel,
1379
1754
  disabled: !props.writable || props.authBusy || !props.available,
1755
+ "aria-label": t("chatGptModelLabel"),
1756
+ className: "dsu-input",
1380
1757
  onChange: (event) => {
1381
1758
  props.onAuthModelChange(event.target.value);
1382
1759
  }
1383
1760
  });
1384
1761
  const use = (0, react.createElement)("button", {
1385
1762
  type: "button",
1763
+ className: "dsu-btn",
1386
1764
  "data-field": "chatGptUse",
1387
1765
  disabled: !props.writable || props.authBusy || !props.available,
1388
1766
  onClick: props.onUseChatGpt
1389
1767
  }, props.authBusy ? t("chatGptUsing") : t("chatGptUse"));
1390
- const output = props.authOutput === null ? null : (0, react.createElement)("pre", { style: textareaStyle }, props.authOutput);
1391
- const message = props.authMsg === null ? null : (0, react.createElement)("div", { className: "ocr-hint" }, props.authMsg);
1392
- return (0, react.createElement)("div", { style: sectionStyle }, (0, react.createElement)("div", { style: titleStyle }, t("chatGptTitle")), (0, react.createElement)("div", { style: hintStyle }, t("chatGptHint")), check, (0, react.createElement)("div", { style: hintStyle }, `${t("chatGptLoginHint")} ${props.loginCommand}`), output, (0, react.createElement)("div", { style: hintStyle }, t("chatGptModelLabel")), model, use, message);
1768
+ const output = props.authOutput === null ? null : (0, react.createElement)("pre", { className: "ocr-status dsu-mono" }, props.authOutput);
1769
+ const message = props.authMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.authMsg);
1770
+ return (0, react.createElement)(d, {
1771
+ title: t("chatGptTitle"),
1772
+ hint: t("chatGptHint")
1773
+ }, (0, react.createElement)("div", { className: ACTIONS_CLASS }, check), (0, react.createElement)("p", { className: "dsu-hint" }, `${t("chatGptLoginHint")} ${props.loginCommand}`), output, (0, react.createElement)("p", { className: "dsu-hint" }, t("chatGptModelLabel")), model, (0, react.createElement)("div", { className: ACTIONS_CLASS }, use), message);
1393
1774
  }
1394
1775
  /** MCP 配置 + ChatGPT 登录态/切换:都写 OCR 自己的配置,放在设置卡下方。 */
1395
1776
  function buildAdminSection(props) {
1396
- return (0, react.createElement)("div", { style: sectionStyle }, buildMcpAdmin(props), buildAuthAdmin(props));
1777
+ return (0, react.createElement)("div", null, buildMcpAdmin(props), buildAuthAdmin(props));
1397
1778
  }
1398
1779
  /**
1399
1780
  * 读设置页载荷(卡片唯一的数据入口):失败只落 fetchError 文本,不抛穿渲染。
@@ -1405,8 +1786,8 @@ async function refreshProviders(reader, t, sinks) {
1405
1786
  try {
1406
1787
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1407
1788
  const json = await reader.providers();
1408
- if (i(json, "ok") === false) {
1409
- const detail = strOf(i(json, "error"));
1789
+ if (i$1(json, "ok") === false) {
1790
+ const detail = strOf(i$1(json, "error"));
1410
1791
  throw new Error(detail === "" ? t("remoteUnavailable") : detail);
1411
1792
  }
1412
1793
  const payload = providersPayloadOf(json);
@@ -1423,8 +1804,8 @@ function readerPost(reader, t) {
1423
1804
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1424
1805
  switch (action) {
1425
1806
  case "select": {
1426
- const provider = strOf(i(body, "provider"));
1427
- const model = strOf(i(body, "model"));
1807
+ const provider = strOf(i$1(body, "provider"));
1808
+ const model = strOf(i$1(body, "model"));
1428
1809
  const reply = await reader.select(provider, model);
1429
1810
  return { json: () => Promise.resolve(reply) };
1430
1811
  }
@@ -1461,12 +1842,12 @@ async function runApplySelection(deps) {
1461
1842
  provider: effectiveProvider,
1462
1843
  model: effectiveModel
1463
1844
  })).json();
1464
- if (i(selJson, "ok") === true) {
1845
+ if (i$1(selJson, "ok") === true) {
1465
1846
  let testText;
1466
- const llmTestRaw = i(selJson, "llmTest");
1467
- if (e$2(llmTestRaw)) {
1468
- const testOutput = strOf(i(llmTestRaw, "output")).slice(0, 400);
1469
- testText = i(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1847
+ const llmTestRaw = i$1(selJson, "llmTest");
1848
+ if (e$3(llmTestRaw)) {
1849
+ const testOutput = strOf(i$1(llmTestRaw, "output")).slice(0, 400);
1850
+ testText = i$1(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1470
1851
  } else testText = t("notVerified");
1471
1852
  const appliedText = t("applied", {
1472
1853
  provider: effectiveProvider,
@@ -1479,7 +1860,7 @@ async function runApplySelection(deps) {
1479
1860
  });
1480
1861
  deps.refresh();
1481
1862
  } else {
1482
- const failure = strOf(i(selJson, "error"));
1863
+ const failure = strOf(i$1(selJson, "error"));
1483
1864
  deps.setActionMsg({
1484
1865
  ok: false,
1485
1866
  text: failure === "" ? t("applyFailed") : failure
@@ -1502,11 +1883,11 @@ async function runMigrateKeys(deps) {
1502
1883
  deps.setActionMsg(null);
1503
1884
  try {
1504
1885
  const json = await (await deps.post("migrate")).json();
1505
- const migratedVal = i(json, "migrated");
1506
- const skippedVal = i(json, "skippedUnknown");
1886
+ const migratedVal = i$1(json, "migrated");
1887
+ const skippedVal = i$1(json, "skippedUnknown");
1507
1888
  const migrated = Array.isArray(migratedVal) ? migratedVal.filter((item) => typeof item === "string") : [];
1508
1889
  const skipped = Array.isArray(skippedVal) ? skippedVal.filter((item) => typeof item === "string") : [];
1509
- const ok = i(json, "ok") === true;
1890
+ const ok = i$1(json, "ok") === true;
1510
1891
  const head = ok ? (migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey")) + (skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "") : t("migrateFailed");
1511
1892
  deps.setActionMsg({
1512
1893
  ok,
@@ -1530,8 +1911,8 @@ async function runTestConnection(deps) {
1530
1911
  deps.setActionMsg(null);
1531
1912
  try {
1532
1913
  const json = await (await deps.post("test")).json();
1533
- const ok = i(json, "ok") === true;
1534
- const testOutput = strOf(i(json, "output"));
1914
+ const ok = i$1(json, "ok") === true;
1915
+ const testOutput = strOf(i$1(json, "output"));
1535
1916
  deps.setActionMsg({
1536
1917
  ok,
1537
1918
  text: (ok ? t("llmTestPassed") : t("llmTestFailed")) + testOutput
@@ -1558,12 +1939,12 @@ async function runProbe(deps) {
1558
1939
  deps.setProbeMsg(null);
1559
1940
  try {
1560
1941
  const json = await (await deps.post("probe")).json();
1561
- const ok = i(json, "ok") === true;
1562
- const head = ok ? t("probeVersion", { version: strOf(i(json, "version")) }) : t("probeReason", { reason: strOf(i(json, "reason")) });
1563
- const output = strOf(i(json, "output"));
1942
+ const ok = i$1(json, "ok") === true;
1943
+ const head = ok ? t("probeVersion", { version: strOf(i$1(json, "version")) }) : t("probeReason", { reason: strOf(i$1(json, "reason")) });
1944
+ const output = strOf(i$1(json, "output"));
1564
1945
  const lines = [
1565
1946
  head,
1566
- strOf(i(json, "changeText")),
1947
+ strOf(i$1(json, "changeText")),
1567
1948
  output
1568
1949
  ].filter((line) => line !== "");
1569
1950
  deps.setProbeMsg({
@@ -1585,8 +1966,8 @@ async function runProbe(deps) {
1585
1966
  * 这时不能清 touched、也不能报成功——否则用户看到「已保存」而配置原封未动。
1586
1967
  * 传输失败仍以 rejection 表达。 */
1587
1968
  async function saveSettingsDraft(deps) {
1588
- const { t: t$3, value, touched } = deps;
1589
- const keys = t(touched, value);
1969
+ const { t, value, touched } = deps;
1970
+ const keys = t$1(touched, value);
1590
1971
  if (keys.length === 0) return;
1591
1972
  deps.setSettingsBusy(true);
1592
1973
  deps.setSaveError(null);
@@ -1612,7 +1993,7 @@ async function saveSettingsDraft(deps) {
1612
1993
  for (const field of refused) kept[field] = touched[field];
1613
1994
  deps.setTouched(kept);
1614
1995
  const reason = failures.length > 0 ? `(${errorSummary(failures[0])})` : "";
1615
- deps.setSaveError(`${t$3("saveRefused", { fields: refused.join(", ") })}${reason}`);
1996
+ deps.setSaveError(`${t("saveRefused", { fields: refused.join(", ") })}${reason}`);
1616
1997
  return;
1617
1998
  }
1618
1999
  deps.setTouched({});
@@ -1620,11 +2001,11 @@ async function saveSettingsDraft(deps) {
1620
2001
  function draftFieldsOf(touched, value) {
1621
2002
  const eff = (field, def) => field in touched ? touched[field] : value[field] ?? def;
1622
2003
  return {
1623
- effortValue: eff("effort", "auto"),
1624
- languageValue: eff("language", "中文"),
1625
- autoVerifyValue: eff("autoVerify", true),
1626
- maxCommentsValue: numOf(eff("maxComments", 12), 12),
1627
- timeoutMinutesValue: numOf(eff("timeoutMinutes", 0), 0),
2004
+ effortValue: textOf(eff("effort", "auto"), EFFORT_SCHEMA, "auto"),
2005
+ languageValue: textOf(eff("language", "中文"), LANGUAGE_SCHEMA, "中文"),
2006
+ autoVerifyValue: booleanOf(eff("autoVerify", true), true),
2007
+ maxCommentsValue: numOf(eff("maxComments", 12), 12, COUNT_SCHEMA),
2008
+ timeoutMinutesValue: numOf(eff("timeoutMinutes", 0), 0, COUNT_SCHEMA),
1628
2009
  ocrConfigPathValue: strOf(eff("ocrConfigPath", ""))
1629
2010
  };
1630
2011
  }
@@ -1643,9 +2024,7 @@ function cardHints(t, ready, status, data) {
1643
2024
  /** Provider 区 = provider/model 表单 + 最近一次动作的回执条。 */
1644
2025
  function buildProviderPanel(deps) {
1645
2026
  const { actionMsg } = deps;
1646
- const providerSection = buildProviderSection(deps);
1647
- const actionMsgNode = actionMsg ? (0, react.createElement)("div", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null;
1648
- return (0, react.createElement)("div", { style: { padding: "2px 0" } }, providerSection, actionMsgNode);
2027
+ return [buildProviderSection(deps), actionMsg ? (0, react.createElement)("p", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null];
1649
2028
  }
1650
2029
  /** 管理区的状态与动作(MCP 配置 + ChatGPT 登录态/切换);OcrCard 只负责摆位置。 */
1651
2030
  function useAdminPanel(reader, t) {
@@ -1750,25 +2129,25 @@ function useAdminPanel(reader, t) {
1750
2129
  };
1751
2130
  }
1752
2131
  function OcrCard(props) {
1753
- const { t: t$4 } = props;
2132
+ const { t } = props;
1754
2133
  const snap = props.useCard((state) => state);
1755
2134
  const { value } = snap;
1756
2135
  const ready = snap.status === "ready";
1757
2136
  const writable = snap.writable && ready;
1758
- const adminState = useAdminPanel(props.reader, t$4);
2137
+ const adminState = useAdminPanel(props.reader, t);
1759
2138
  const [data, setData] = (0, react.useState)(null);
1760
2139
  const [fetchError, setFetchError] = (0, react.useState)(null);
1761
2140
  const [busy, setBusy] = (0, react.useState)(null);
1762
2141
  const [actionMsg, setActionMsg] = (0, react.useState)(null);
1763
2142
  const [probeMsg, setProbeMsg] = (0, react.useState)(null);
1764
- const refresh = (0, react.useCallback)(async () => refreshProviders(props.reader, t$4, {
2143
+ const refresh = (0, react.useCallback)(async () => refreshProviders(props.reader, t, {
1765
2144
  setData,
1766
2145
  setFetchError
1767
- }), [props.reader, t$4]);
2146
+ }), [props.reader, t]);
1768
2147
  (0, react.useEffect)(() => {
1769
2148
  refresh();
1770
2149
  }, [refresh]);
1771
- const post = readerPost(props.reader, t$4);
2150
+ const post = readerPost(props.reader, t);
1772
2151
  const [selProvider, setSelProvider] = (0, react.useState)("");
1773
2152
  const [selModel, setSelModel] = (0, react.useState)("");
1774
2153
  const { effectiveProvider, effectiveModel, chosenProvider, modelOptions } = resolveSelection(data?.providers ?? [], data?.current, selProvider, selModel);
@@ -1779,7 +2158,7 @@ function OcrCard(props) {
1779
2158
  setSelModel(first);
1780
2159
  }
1781
2160
  const actions = {
1782
- t: t$4,
2161
+ t,
1783
2162
  post,
1784
2163
  refresh,
1785
2164
  setBusy,
@@ -1790,7 +2169,7 @@ function OcrCard(props) {
1790
2169
  const [touched, setTouched] = (0, react.useState)({});
1791
2170
  const [settingsBusy, setSettingsBusy] = (0, react.useState)(false);
1792
2171
  const [saveError, setSaveError] = (0, react.useState)(null);
1793
- const dirty = t(touched, value).length > 0;
2172
+ const dirty = t$1(touched, value).length > 0;
1794
2173
  const setField = (field, draft) => {
1795
2174
  setTouched((prev) => ({
1796
2175
  ...prev,
@@ -1802,7 +2181,7 @@ function OcrCard(props) {
1802
2181
  setSaveError(null);
1803
2182
  };
1804
2183
  const deps = {
1805
- t: t$4,
2184
+ t,
1806
2185
  data,
1807
2186
  fetchError,
1808
2187
  busy,
@@ -1813,7 +2192,7 @@ function OcrCard(props) {
1813
2192
  effectiveModel,
1814
2193
  chosenProvider,
1815
2194
  modelOptions,
1816
- ...cardHints(t$4, ready, snap.status, data),
2195
+ ...cardHints(t, ready, snap.status, data),
1817
2196
  binary: data?.ocrBinary ?? null,
1818
2197
  probeMsg,
1819
2198
  ...draftFieldsOf(touched, value),
@@ -1833,7 +2212,7 @@ function OcrCard(props) {
1833
2212
  },
1834
2213
  onProbe: () => {
1835
2214
  runProbe({
1836
- t: t$4,
2215
+ t,
1837
2216
  post,
1838
2217
  refresh,
1839
2218
  setBusy,
@@ -1843,7 +2222,7 @@ function OcrCard(props) {
1843
2222
  onField: setField,
1844
2223
  onSave: () => {
1845
2224
  saveSettingsDraft({
1846
- t: t$4,
2225
+ t,
1847
2226
  value,
1848
2227
  touched,
1849
2228
  setTouched,
@@ -1859,15 +2238,10 @@ function OcrCard(props) {
1859
2238
  const settingsSection = buildSettingsSection(deps, writable, setField);
1860
2239
  const adminSection = buildAdminSection({
1861
2240
  ...adminState,
1862
- t: t$4,
2241
+ t,
1863
2242
  writable
1864
2243
  });
1865
- const headTitle = t$4("cardTitle");
1866
- const headDesc = t$4("cardDescription");
1867
- return (0, react.createElement)(OcrPluginCard, {
1868
- title: headTitle,
1869
- description: headDesc
1870
- }, cliStatus, providerPanel, settingsSection, adminSection);
2244
+ return (0, react.createElement)(u, null, cliStatus, providerPanel, settingsSection, adminSection);
1871
2245
  }
1872
2246
  const inject = [
1873
2247
  "slots",
@@ -1875,11 +2249,11 @@ const inject = [
1875
2249
  "locale"
1876
2250
  ];
1877
2251
  function apply(ctx) {
1878
- if (!n(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
2252
+ if (!n$1(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
1879
2253
  ctx.effect(() => {
1880
2254
  const tag = document.createElement("style");
1881
2255
  tag.id = "ocr-review-card-css";
1882
- tag.textContent = CARD_CSS;
2256
+ tag.textContent = `${CARD_CSS}\n${c}`;
1883
2257
  document.head.append(tag);
1884
2258
  return () => {
1885
2259
  tag.remove();
@@ -1930,7 +2304,6 @@ exports.OcrCard = OcrCard;
1930
2304
  exports.OcrCliActivation = OcrCliActivation;
1931
2305
  exports.OcrCliBadge = OcrCliBadge;
1932
2306
  exports.OcrCliSection = OcrCliSection;
1933
- exports.SaveBar = SaveBar;
1934
2307
  exports.apply = apply;
1935
2308
  exports.binaryOf = binaryOf;
1936
2309
  exports.binaryStatusText = binaryStatusText;