@jayyuen66/dsh-ocr-review 0.1.8 → 0.1.10

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,18 +7,20 @@ 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: "无未保存的修改",
18
21
  save: "保存",
19
22
  saving: "保存中…",
20
23
  revert: "撤销",
21
- saveFailed: "保存失败:",
22
24
  saveRefused: "未写入(宿主拒收或跳过):{fields}",
23
25
  loadProvidersFailed: "加载 providers 失败:{error}(远程面未就绪?重启 dsh 后重试)",
24
26
  remoteUnavailable: "远程服务不可用:宿主没有挂上本插件的设置通道。",
@@ -59,8 +61,6 @@ const UI_MESSAGES = {
59
61
  languageHint: "OCR 评论输出语言(写入 config.language)",
60
62
  autoVerifyLabel: "应用后自动验证",
61
63
  autoVerifyHint: "选择 provider/model 后自动跑 ocr llm test",
62
- onLabel: "开启",
63
- offLabel: "关闭",
64
64
  maxCommentsLabel: "摘要评论条数 (maxComments)",
65
65
  maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认见本行设置项的初值)",
66
66
  timeoutLabel: "运行超时 (timeoutMinutes)",
@@ -73,6 +73,7 @@ const UI_MESSAGES = {
73
73
  mcpSaving: "保存中…",
74
74
  mcpSaved: "MCP servers 已保存",
75
75
  mcpLoadFailed: "读取 MCP 配置失败:",
76
+ mcpNotLoaded: "还没读到当前 MCP 配置,先等读取完成(失败时先修好读取)再保存",
76
77
  chatGptTitle: "ChatGPT(OCR auth)",
77
78
  chatGptHint: "登录态由 ocr auth 管理;本卡片只读查询状态,并在登录后把 provider 切到 chatgpt。",
78
79
  chatGptCheck: "检查登录态",
@@ -90,7 +91,7 @@ const UI_MESSAGES = {
90
91
  binaryUnknown: "状态未取到:刷新本页,或确认 dsh 的 web 服务已起来",
91
92
  binaryBundled: "随本插件分发的兜底入口已就位:{path}({channel})",
92
93
  binaryPath: "用 PATH 上的全局安装:{path}({channel})",
93
- binaryNone: "未找到 ocr:PATH 与随包兜底两条路都没有可执行候选。装法与版本看下一行(由插件按核对版本给出)",
94
+ binaryNone: "未找到 ocr:PATH 与随包兜底两条路都没有可执行候选",
94
95
  channelBrew: "brew formula",
95
96
  channelNpm: "npm 全局包",
96
97
  channelUnknown: "装法未识别",
@@ -111,15 +112,16 @@ 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",
119
122
  save: "Save",
120
123
  saving: "Saving…",
121
124
  revert: "Revert",
122
- saveFailed: "save failed: ",
123
125
  saveRefused: "not written (host refused or skipped): {fields}",
124
126
  loadProvidersFailed: "failed to load providers: {error} (remote face not ready? restart dsh and retry)",
125
127
  remoteUnavailable: "remote service unavailable: the host did not mount this plugin's settings channel",
@@ -160,8 +162,6 @@ const UI_MESSAGES = {
160
162
  languageHint: "Output language of OCR comments (written into config.language)",
161
163
  autoVerifyLabel: "Verify after applying",
162
164
  autoVerifyHint: "Run `ocr llm test` automatically after picking a provider/model",
163
- onLabel: "On",
164
- offLabel: "Off",
165
165
  maxCommentsLabel: "Summary comment cap (maxComments)",
166
166
  maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default: the row's initial value)",
167
167
  timeoutLabel: "Run timeout (timeoutMinutes)",
@@ -174,6 +174,7 @@ const UI_MESSAGES = {
174
174
  mcpSaving: "Saving…",
175
175
  mcpSaved: "MCP servers saved",
176
176
  mcpLoadFailed: "Failed to read the MCP config: ",
177
+ mcpNotLoaded: "The current MCP config has not loaded yet; wait for the read (or fix it) before saving",
177
178
  chatGptTitle: "ChatGPT (OCR auth)",
178
179
  chatGptHint: "Sign-in is managed by ocr auth; this card only reports the status and switches OCR's provider to chatgpt after you sign in.",
179
180
  chatGptCheck: "Check sign-in",
@@ -191,7 +192,7 @@ const UI_MESSAGES = {
191
192
  binaryUnknown: "status not reported: reload this page, or check that the dsh web server is up",
192
193
  binaryBundled: "bundled fallback entry in place: {path} ({channel})",
193
194
  binaryPath: "using the global install on PATH: {path} ({channel})",
194
- binaryNone: "ocr not found: neither PATH nor the bundled fallback has an executable candidate. The install command and version are on the next line (this plugin states the version its contract was verified against)",
195
+ binaryNone: "ocr not found: neither PATH nor the bundled fallback has an executable candidate",
195
196
  channelBrew: "brew formula",
196
197
  channelNpm: "npm global package",
197
198
  channelUnknown: "install method unrecognised",
@@ -200,7 +201,7 @@ const UI_MESSAGES = {
200
201
  versionDiverged: "this machine reports {actual}, which is not the verified {expected}: comment wording and severity may drift between versions",
201
202
  badgeDiverged: "ocr version differs",
202
203
  detectCli: "Detect version",
203
- detecting: "Detecting...",
204
+ detecting: "Detecting…",
204
205
  probeVersion: "ocr version: {version}",
205
206
  probeReason: "no version read: {reason}",
206
207
  badgeBundled: "ocr from bundled fallback",
@@ -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,24 @@ 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({
831
+ json: zod.z.string({ error: MCP_SAVE_MESSAGE }),
832
+ revision: zod.z.string().optional()
833
+ }, { error: MCP_SAVE_MESSAGE });
834
+ const CHAT_GPT_MESSAGE = "chatgpt request requires a string model";
835
+ const CHAT_GPT_SCHEMA = zod.z.object({ model: zod.z.string({ error: CHAT_GPT_MESSAGE }) }, { error: CHAT_GPT_MESSAGE });
285
836
  /** 把宿主那九个方法挂到客户端远程面上的描述符(`id` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
286
837
  * 导出它只为一件事:`test/remote.test.ts` 拿它与宿主侧 `remoteMethods()` 的标记对质
287
838
  * —— 两侧分叉时卡片不会报错,只会"读不到",所以那条比较必须在门禁里。 */
@@ -309,13 +860,7 @@ const OCR_DESCRIPTORS = [
309
860
  mode: "strict",
310
861
  typeSymbol: `${PACKAGE_NAME}#select-request`,
311
862
  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
- };
863
+ return parseOrThrow(SELECT_SCHEMA, value);
319
864
  } })
320
865
  }
321
866
  }],
@@ -371,8 +916,7 @@ const OCR_DESCRIPTORS = [
371
916
  mode: "strict",
372
917
  typeSymbol: `${PACKAGE_NAME}#mcp-save-request`,
373
918
  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"] };
919
+ return parseOrThrow(MCP_SAVE_SCHEMA, value);
376
920
  } })
377
921
  }
378
922
  }],
@@ -401,8 +945,7 @@ const OCR_DESCRIPTORS = [
401
945
  mode: "strict",
402
946
  typeSymbol: `${PACKAGE_NAME}#chatgpt-request`,
403
947
  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"] };
948
+ return parseOrThrow(CHAT_GPT_SCHEMA, value);
406
949
  } })
407
950
  }
408
951
  }],
@@ -410,12 +953,13 @@ const OCR_DESCRIPTORS = [
410
953
  }
411
954
  ];
412
955
  /** 这个值能当远程面用吗(只核我们要调的那一位;服务来自别的实现或版本时保守当作没有)。 */
413
- const isRemoteMounter = (value) => e$2(value) && typeof value["$mount"] === "function";
956
+ const isRemoteMounter = (value) => e$3(value) && typeof value["$mount"] === "function";
414
957
  /** 同上:命名空间服务(挂载之后才有,缺它说明网关没认这枚贡献)。 */
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";
416
- /** 网关回执 → 值;载体/网关失败抛(卡片那层按「请求失败」显示)。 */
958
+ 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";
959
+ /** 网关回执 → 值;载体/网关失败抛(卡片那层按「请求失败」显示)。
960
+ * 机器码放 `cause`:卡片只展示 message,`REMOTE_...` 这类内部标识不是给用户看的。 */
417
961
  function unwrap(result) {
418
- if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
962
+ if (!result.ok) throw new Error(result.error.message, { cause: { code: result.error.code } });
419
963
  return result.value;
420
964
  }
421
965
  /**
@@ -431,7 +975,7 @@ function buildOcrReader(ctx) {
431
975
  let mounted;
432
976
  const remoteOf = () => {
433
977
  const remote = ctx.get?.("remote", false);
434
- if (!isRemoteMounter(remote)) throw new Error("remote 未提供:当前部署没有远程读面(每次读取都会重新解析)");
978
+ if (!isRemoteMounter(remote)) throw new Error("remote 未提供:当前部署没有远程读面");
435
979
  return remote;
436
980
  };
437
981
  const namespaceOf = async () => {
@@ -447,7 +991,10 @@ function buildOcrReader(ctx) {
447
991
  throw error;
448
992
  }
449
993
  const namespace = ctx.get?.(OCR_SERVICE_KEY, false);
450
- if (!isOcrNamespace(namespace)) throw new Error(`${OCR_SERVICE_KEY} 未提供:宿主网关没有挂上这枚命名空间`);
994
+ if (!isOcrNamespace(namespace)) {
995
+ mounted = void 0;
996
+ throw new Error(`${OCR_SERVICE_KEY} 未提供:宿主网关没有挂上这枚命名空间`);
997
+ }
451
998
  return namespace;
452
999
  };
453
1000
  return {
@@ -472,8 +1019,11 @@ function buildOcrReader(ctx) {
472
1019
  async mcp() {
473
1020
  return unwrap(await (await namespaceOf()).mcp());
474
1021
  },
475
- async saveMcp(json) {
476
- return unwrap(await (await namespaceOf()).saveMcp({ json }));
1022
+ async saveMcp(json, revision) {
1023
+ return unwrap(await (await namespaceOf()).saveMcp(revision === void 0 ? { json } : {
1024
+ json,
1025
+ revision
1026
+ }));
477
1027
  },
478
1028
  async auth() {
479
1029
  return unwrap(await (await namespaceOf()).auth());
@@ -524,35 +1074,65 @@ const BUNDLE_PKG = OCR_BUNDLE_PACKAGE;
524
1074
  function strOf(value) {
525
1075
  return typeof value === "string" ? value : "";
526
1076
  }
527
- /** 未知值 → 安全数字(非 number 给 fallback)。 */
528
- function numOf(value, fallback) {
529
- return typeof value === "number" ? value : fallback;
1077
+ /** 设置行的取值域:枚举/计数都交给 zod 声明,界面只消费解析后的类型。 */
1078
+ const EFFORT_SCHEMA = zod.z.enum([
1079
+ "auto",
1080
+ "low",
1081
+ "medium",
1082
+ "high"
1083
+ ]);
1084
+ const LANGUAGE_SCHEMA = zod.z.enum(["中文", "English"]);
1085
+ const COUNT_SCHEMA = zod.z.int().min(0);
1086
+ /** 未知值 → 安全数字(非整数/越界给 fallback)。 */
1087
+ function numOf(value, fallback, schema) {
1088
+ const parsed = schema.safeParse(value);
1089
+ return parsed.success ? parsed.data : fallback;
1090
+ }
1091
+ /** 未知值 → 枚举里的一个取值(域外给 fallback)。 */
1092
+ function textOf(value, schema, fallback) {
1093
+ const parsed = schema.safeParse(value);
1094
+ return parsed.success ? parsed.data : fallback;
1095
+ }
1096
+ /** 未知值 → 布尔(非布尔给 fallback)。 */
1097
+ function booleanOf(value, fallback) {
1098
+ const parsed = zod.z.boolean().safeParse(value);
1099
+ return parsed.success ? parsed.data : fallback;
530
1100
  }
531
1101
  /** 错误摘要(非 Error 抛出也要可读;catch 分支共用)。 */
532
1102
  function errorSummary(error) {
533
1103
  return String(error instanceof Error ? error.message : error);
534
1104
  }
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
- }
1105
+ const STRING_OR_EMPTY = zod.z.preprocess((value) => typeof value === "string" ? value : "", zod.z.string());
1106
+ const NULLABLE_STRING = zod.z.preprocess((value) => typeof value === "string" ? value : null, zod.z.string().nullable());
1107
+ const TRUE_ONLY = zod.z.preprocess((value) => value === true, zod.z.boolean());
1108
+ const MODEL_SCHEMA = zod.z.object({
1109
+ id: STRING_OR_EMPTY,
1110
+ name: STRING_OR_EMPTY
1111
+ });
1112
+ const PROVIDER_SCHEMA = zod.z.object({
1113
+ name: STRING_OR_EMPTY,
1114
+ displayName: STRING_OR_EMPTY,
1115
+ apiKeyEnv: NULLABLE_STRING,
1116
+ baseURL: STRING_OR_EMPTY,
1117
+ models: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(MODEL_SCHEMA)),
1118
+ hasKey: TRUE_ONLY
1119
+ });
1120
+ const CURRENT_SCHEMA = zod.z.object({
1121
+ provider: STRING_OR_EMPTY,
1122
+ model: STRING_OR_EMPTY,
1123
+ keyIsDynamic: TRUE_ONLY,
1124
+ url: STRING_OR_EMPTY
1125
+ });
1126
+ const SOURCE_SCHEMA = zod.z.object({
1127
+ status: STRING_OR_EMPTY,
1128
+ message: STRING_OR_EMPTY
1129
+ });
1130
+ const ENDPOINT_VERIFY_SCHEMA = zod.z.object({
1131
+ ok: TRUE_ONLY,
1132
+ message: STRING_OR_EMPTY
1133
+ });
554
1134
  /** 可得性来源的合法域(与 host 侧 `OcrBinarySource` 同一份取值)。形状不合即「没报」。 */
555
- const BINARY_SOURCES = /* @__PURE__ */ new Set([
1135
+ const BINARY_SOURCE_SCHEMA = zod.z.enum([
556
1136
  "bundled",
557
1137
  "path",
558
1138
  "none"
@@ -565,22 +1145,25 @@ const BINARY_SOURCES = /* @__PURE__ */ new Set([
565
1145
  * 作废——「宿主报了 PATH 却没报版本」仍然是一条用户要看的事实。`measuredVersion` 例外,
566
1146
  * 它恒在(null = 还没测过),因为「没测过」与「旧宿主不带这字段」在卡片上是同一句话。
567
1147
  */
1148
+ const BINARY_SCHEMA = zod.z.object({
1149
+ source: BINARY_SOURCE_SCHEMA,
1150
+ path: STRING_OR_EMPTY,
1151
+ channel: STRING_OR_EMPTY,
1152
+ expectedVersion: STRING_OR_EMPTY,
1153
+ installHint: STRING_OR_EMPTY,
1154
+ measuredVersion: NULLABLE_STRING
1155
+ });
568
1156
  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"];
1157
+ const parsed = BINARY_SCHEMA.safeParse(raw);
1158
+ if (!parsed.success) return null;
1159
+ const { source, path, channel, expectedVersion, installHint, measuredVersion } = parsed.data;
577
1160
  return {
578
1161
  source,
579
1162
  ...path === "" ? {} : { path },
580
1163
  ...channel === "" ? {} : { channel },
581
1164
  ...expectedVersion === "" ? {} : { expectedVersion },
582
1165
  ...installHint === "" ? {} : { installHint },
583
- measuredVersion: typeof measured === "string" ? measured : null
1166
+ measuredVersion
584
1167
  };
585
1168
  }
586
1169
  /**
@@ -591,67 +1174,55 @@ function binaryOf(raw) {
591
1174
  async function loadCliStatus(reader) {
592
1175
  if (reader === void 0) return null;
593
1176
  try {
594
- return binaryOf(i(await reader.providers(), "ocrBinary"));
1177
+ return binaryOf(i$1(await reader.providers(), "ocrBinary"));
595
1178
  } catch {
596
1179
  return null;
597
1180
  }
598
1181
  }
1182
+ const EMPTY_PAYLOAD = {
1183
+ providers: [],
1184
+ current: {
1185
+ provider: "",
1186
+ model: "",
1187
+ keyIsDynamic: false,
1188
+ url: ""
1189
+ },
1190
+ getCredReady: false,
1191
+ source: {
1192
+ status: "",
1193
+ message: ""
1194
+ },
1195
+ endpointVerify: null,
1196
+ ocrBinary: null
1197
+ };
1198
+ const PROVIDERS_PAYLOAD_SCHEMA = zod.z.object({
1199
+ providers: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(PROVIDER_SCHEMA)),
1200
+ current: zod.z.preprocess((value) => e$3(value) ? value : {}, CURRENT_SCHEMA),
1201
+ getCredReady: TRUE_ONLY,
1202
+ source: zod.z.preprocess((value) => e$3(value) ? value : {}, SOURCE_SCHEMA),
1203
+ endpointVerify: zod.z.preprocess((value) => {
1204
+ const parsed = ENDPOINT_VERIFY_SCHEMA.safeParse(value);
1205
+ return parsed.success ? parsed.data : null;
1206
+ }, ENDPOINT_VERIFY_SCHEMA.nullable()),
1207
+ ocrBinary: zod.z.unknown().transform((value) => binaryOf(value))
1208
+ });
599
1209
  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
- };
1210
+ const parsed = PROVIDERS_PAYLOAD_SCHEMA.safeParse(json);
1211
+ return parsed.success ? parsed.data : EMPTY_PAYLOAD;
626
1212
  }
627
- /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
628
- * 抽出来是因为两处 input 与一处 select 必须同宽同边框:写三遍字面量时改一处漏两处,
629
- * 症状是设置卡里某一行的控件比其它行窄。 */
630
- const FORM_CONTROL_CLASS = "ocr-select";
631
- /** 小节标题的类名(卡片头、CLI 状态行、详情页区块共用;sonarjs 的重复字面量门禁)。 */
1213
+ /** 动作按钮行的类名(共享组件的行内按钮组;多处共用,见 sonarjs 的重复字面量门禁)。 */
1214
+ const ACTIONS_CLASS = "ocr-actions";
1215
+ /** 小节标题的类名(详情页区块共用;sonarjs 的重复字面量门禁)。 */
632
1216
  const CARD_NAME_CLASS = "ocr-card-name";
633
1217
  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}",
1218
+ ".ocr-card-name{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.4}",
1219
+ ".ocr-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5}",
1220
+ ".ocr-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
1221
+ ".ocr-status{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin:8px 0 0}",
1222
+ ".ocr-err{color:var(--dsw-alias-state-error-primary)}",
653
1223
  ".ocr-ok{color:var(--dsw-alias-label-primary,inherit)}",
654
- ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}"
1224
+ ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}",
1225
+ ".ocr-adminTextarea{min-height:160px}"
655
1226
  ].join("\n");
656
1227
  function cardStore(scope) {
657
1228
  let cachedSnap = null;
@@ -679,190 +1250,6 @@ function cardStore(scope) {
679
1250
  }
680
1251
  };
681
1252
  }
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
1253
  /** provider 下拉项文本(没有 key 的加后缀并被禁用)。提成模块级函数是为了不把
867
1254
  * 带插值的 t() 嵌进 createElement(unicorn/max-nested-calls 上限 3)。 */
868
1255
  function providerOptionLabel(t, provider) {
@@ -876,14 +1263,13 @@ function currentConfigText(t, current) {
876
1263
  keyPart: t(current.keyIsDynamic ? "dynamicKey" : "plainKeyWarning")
877
1264
  });
878
1265
  }
879
- /** 装法枚举到文案键:查表而不是嵌套三元(本仓 lint 禁嵌套三元),查不到就是「未识别」那一档。 */
880
- const CHANNEL_KEYS = {
881
- brew: "channelBrew",
882
- npm: "channelNpm"
883
- };
884
- /** 那份 CLI 是怎么装上的,翻成人话。判不出来源(含 host 没报这一档)不猜。 */
1266
+ /** 那份 CLI 是怎么装上的,翻成人话。判不出来源(含 host 没报这一档)不猜。
1267
+ * 逐个比对已知值而不是查一张普通对象表:`channel` 来自 wire,只校验了「是字符串」,
1268
+ * 用属性查表时 `"toString"`、`"constructor"` 一类会命中原型上的成员,把函数当文案键传下去。 */
885
1269
  function channelText(t, channel) {
886
- return t(CHANNEL_KEYS[channel ?? ""] ?? "channelUnknown");
1270
+ if (channel === "brew") return t("channelBrew");
1271
+ if (channel === "npm") return t("channelNpm");
1272
+ return t("channelUnknown");
887
1273
  }
888
1274
  /**
889
1275
  * 外部 CLI 可得性那一行的文本。三档来源与「宿主没报」四种说法,判据全在 host 侧
@@ -939,35 +1325,30 @@ function statusClass(ok) {
939
1325
  */
940
1326
  function buildCliStatusRow(deps) {
941
1327
  const { t } = deps;
942
- const label = (0, react.createElement)("div", { className: CARD_NAME_CLASS }, t("binaryTitle"));
943
1328
  const button = (0, react.createElement)("button", {
944
- className: "ocr-btn",
1329
+ className: "dsu-btn",
945
1330
  type: "button",
946
1331
  disabled: deps.busy !== null,
947
1332
  onClick: deps.onProbe,
948
1333
  "data-field": "probe"
949
1334
  }, 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);
1335
+ const controls = (0, react.createElement)("div", {
1336
+ className: ACTIONS_CLASS,
1337
+ "data-field": "cli-status"
1338
+ }, button);
1339
+ const status = (0, react.createElement)("p", { className: "dsu-hint" }, binaryStatusText(t, deps.binary));
959
1340
  const versionText = cliVersionText(t, deps.binary);
960
- const versionLine = versionText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, versionText);
1341
+ const versionLine = versionText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, versionText);
961
1342
  const hintText = cliInstallText(deps.binary);
962
- const installLine = hintText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, hintText);
1343
+ const installLine = hintText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, hintText);
963
1344
  const probe = deps.probeMsg ? (0, react.createElement)("div", {
964
1345
  className: statusClass(deps.probeMsg.ok),
965
1346
  "data-field": "probe-result"
966
1347
  }, 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);
1348
+ return (0, react.createElement)(d, {
1349
+ title: t("binaryTitle"),
1350
+ hint: t("binaryProbeHint")
1351
+ }, status, versionLine, installLine, controls, probe);
971
1352
  }
972
1353
  /**
973
1354
  * 徽标用的短文案(标题旁那只标签放不下整句装法)。与 `binaryStatusText` 同一判据、
@@ -1095,100 +1476,165 @@ function OcrCliActivation(props) {
1095
1476
  if (!mine || status?.source !== "none") return null;
1096
1477
  return buildCliActivation(props.t, props, cliInstallText(status));
1097
1478
  }
1098
- /** 数据齐(source.status === "ready")那一支:provider/model 两行 + 三个动作按钮 +
1099
- * 当前配置与 key 两行提示。拆出来是因为这一支画的是整块表单,与上面三条「一行提示」
1100
- * 的降级支本就不是同一件事。 */
1101
- function buildReadyProviderSection(deps, data) {
1479
+ /** 数据齐时的动作行:三个按钮共用一只在飞旗标,按钮文案只跟**自己**那条走。 */
1480
+ function buildActionField(deps) {
1102
1481
  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
1482
  const applyBtn = (0, react.createElement)("button", {
1139
- className: "ocr-btn ocr-btn-primary",
1483
+ className: "dsu-btn dsu-btnPrimary",
1140
1484
  type: "button",
1141
1485
  disabled: deps.busy !== null,
1142
1486
  onClick: deps.onApply,
1143
1487
  "data-field": "apply"
1144
1488
  }, deps.busy === "apply" ? t("applying") : t("applyToOcr"));
1145
1489
  const testBtn = (0, react.createElement)("button", {
1146
- className: "ocr-btn",
1490
+ className: "dsu-btn",
1147
1491
  type: "button",
1148
1492
  disabled: deps.busy !== null,
1149
1493
  onClick: deps.onTest,
1150
1494
  "data-field": "test"
1151
1495
  }, t("testConnection"));
1152
1496
  const migrateBtn = (0, react.createElement)("button", {
1153
- className: "ocr-btn",
1497
+ className: "dsu-btn",
1154
1498
  type: "button",
1155
1499
  disabled: deps.busy !== null,
1156
1500
  onClick: deps.onMigrate,
1157
1501
  "data-field": "migrate"
1158
1502
  }, 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);
1503
+ return (0, react.createElement)(g, {
1504
+ label: t("actionsLabel"),
1505
+ wide: true
1506
+ }, (0, react.createElement)("div", { className: ACTIONS_CLASS }, applyBtn, testBtn, migrateBtn));
1507
+ }
1508
+ /** 当前配置 / key / 装载期自检三行提示(自检失败只作提示,不挡表单)。 */
1509
+ function buildProviderInfo(deps, data) {
1510
+ const { t } = deps;
1166
1511
  const cur = data.current;
1167
1512
  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);
1513
+ const currentInfo = (0, react.createElement)("p", { className: "dsu-hint" }, currentHint);
1514
+ const keyInfo = (0, react.createElement)("p", { className: "dsu-hint" }, deps.keyHint);
1170
1515
  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);
1516
+ return [
1517
+ verify !== null && !verify.ok ? (0, react.createElement)("p", {
1518
+ className: "dsu-error",
1519
+ "data-field": "endpoint-verify"
1520
+ }, t("endpointVerifyFailed", { reason: verify.message })) : null,
1521
+ currentInfo,
1522
+ keyInfo
1523
+ ];
1524
+ }
1525
+ /** 数据齐(source.status === "ready")那一支:模型四个字段 + 动作行 + 三行提示。 */
1526
+ function buildReadyProviderSection(deps, data) {
1527
+ const { t } = deps;
1528
+ const providerOptions = data.providers.map((provider) => ({
1529
+ value: provider.name,
1530
+ label: providerOptionLabel(t, provider),
1531
+ disabled: !provider.hasKey
1532
+ }));
1533
+ const fallbackModelName = deps.effectiveModel === "" ? t("noModel") : deps.effectiveModel;
1534
+ const modelOptions = (deps.modelOptions.length > 0 ? deps.modelOptions : [{
1535
+ id: deps.effectiveModel,
1536
+ name: fallbackModelName
1537
+ }]).map((model) => ({
1538
+ value: model.id,
1539
+ label: model.name
1540
+ }));
1541
+ const effortOptions = [
1542
+ {
1543
+ value: "auto",
1544
+ label: t("effortAuto")
1545
+ },
1546
+ {
1547
+ value: "low",
1548
+ label: t("effortLow")
1549
+ },
1550
+ {
1551
+ value: "medium",
1552
+ label: t("effortMedium")
1553
+ },
1554
+ {
1555
+ value: "high",
1556
+ label: t("effortHigh")
1557
+ }
1558
+ ];
1559
+ const languageOptions = [{
1560
+ value: "中文",
1561
+ label: "中文"
1562
+ }, {
1563
+ value: "English",
1564
+ label: "English"
1565
+ }];
1566
+ const modelUrl = deps.chosenProvider?.baseURL ?? "";
1567
+ return [
1568
+ (0, react.createElement)(x, {
1569
+ label: "Provider",
1570
+ ...l(t("providerHint")),
1571
+ field: "ocrProvider",
1572
+ value: deps.effectiveProvider,
1573
+ options: providerOptions,
1574
+ disabled: data.providers.length === 0,
1575
+ onChange: deps.onPickProvider
1576
+ }),
1577
+ (0, react.createElement)(x, {
1578
+ label: "Model",
1579
+ ...modelUrl === "" ? {} : { hint: modelUrl },
1580
+ field: "ocrModel",
1581
+ value: deps.effectiveModel,
1582
+ options: modelOptions,
1583
+ disabled: !deps.chosenProvider || deps.modelOptions.length === 0,
1584
+ onChange: deps.onPickModel
1585
+ }),
1586
+ (0, react.createElement)(x, {
1587
+ label: t("effortLabel"),
1588
+ ...l(t("effortHint")),
1589
+ field: "effort",
1590
+ value: deps.effortValue,
1591
+ options: effortOptions,
1592
+ disabled: !deps.writable,
1593
+ onChange: (next) => {
1594
+ deps.onField("effort", next);
1595
+ }
1596
+ }),
1597
+ (0, react.createElement)(x, {
1598
+ label: t("languageLabel"),
1599
+ ...l(t("languageHint")),
1600
+ field: "language",
1601
+ value: deps.languageValue,
1602
+ options: languageOptions,
1603
+ disabled: !deps.writable,
1604
+ onChange: (next) => {
1605
+ deps.onField("language", next);
1606
+ }
1607
+ }),
1608
+ (0, react.createElement)(_, {
1609
+ label: t("autoVerifyLabel"),
1610
+ ...l(t("autoVerifyHint")),
1611
+ field: "autoVerify",
1612
+ checked: deps.autoVerifyValue,
1613
+ disabled: !deps.writable,
1614
+ onChange: (next) => {
1615
+ deps.onField("autoVerify", next);
1616
+ }
1617
+ }),
1618
+ buildActionField(deps),
1619
+ ...buildProviderInfo(deps, data)
1620
+ ];
1176
1621
  }
1177
1622
  /** Provider / Model 选择与应用区:三条降级各出一行提示(数据未就绪/读取失败/
1178
1623
  * 数据源不可用),只有 source.status === "ready" 才交给 buildReadyProviderSection 画表单。 */
1179
1624
  function buildProviderSection(deps) {
1180
1625
  const { t } = deps;
1626
+ let body;
1181
1627
  if (deps.fetchError !== null) {
1182
1628
  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") {
1629
+ body = (0, react.createElement)("p", { className: "dsu-error" }, reason);
1630
+ } else if (deps.data === null) body = (0, react.createElement)("p", { className: "dsu-hint" }, t("loadingProviders"));
1631
+ else if (deps.data.source.status === "ready") body = buildReadyProviderSection(deps, deps.data);
1632
+ else {
1187
1633
  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);
1634
+ const reason = sourceMessage === "" ? t("noReason") : sourceMessage;
1635
+ body = (0, react.createElement)("p", { className: "dsu-error" }, t("readProvidersFailed", { reason }));
1190
1636
  }
1191
- return buildReadyProviderSection(deps, deps.data);
1637
+ return (0, react.createElement)(d, { title: t("modelSection") }, body);
1192
1638
  }
1193
1639
  /** provider 预选:显式选择 > 当前 OCR 配置(仍存在)> 第一个有 key 的 provider。 */
1194
1640
  function resolveBackend(providerList, current, selProvider) {
@@ -1218,157 +1664,95 @@ function resolveSelection(providerList, current, selProvider, selModel) {
1218
1664
  modelOptions: model.modelOptions
1219
1665
  };
1220
1666
  }
1221
- /** 设置行(effort/language/autoVerify + 保存条)。 */
1667
+ /** 审查参数小节:评论条数、运行超时、配置文件路径 + 保存条。 */
1222
1668
  function buildSettingsSection(deps, writable, setField) {
1223
1669
  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, {
1670
+ const maxCommentsRow = (0, react.createElement)(v, {
1287
1671
  label: t("maxCommentsLabel"),
1288
- hint: t("maxCommentsHint"),
1672
+ ...l(t("maxCommentsHint")),
1289
1673
  field: "maxComments",
1290
1674
  value: deps.maxCommentsValue,
1291
- min: 0,
1675
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1676
+ step: 1,
1677
+ invalidHint: t("outOfRange"),
1292
1678
  disabled: !writable,
1293
- onCommit: (num) => {
1294
- setField("maxComments", num);
1679
+ onChange: (next) => {
1680
+ setField("maxComments", next);
1295
1681
  }
1296
1682
  });
1297
- const timeoutRow = (0, react.createElement)(NumberRow, {
1683
+ const timeoutRow = (0, react.createElement)(v, {
1298
1684
  label: t("timeoutLabel"),
1299
- hint: t("timeoutHint"),
1685
+ ...l(t("timeoutHint")),
1300
1686
  field: "timeoutMinutes",
1301
1687
  value: deps.timeoutMinutesValue,
1302
- min: 0,
1688
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1689
+ step: 1,
1690
+ invalidHint: t("outOfRange"),
1303
1691
  disabled: !writable,
1304
- onCommit: (num) => {
1305
- setField("timeoutMinutes", num);
1692
+ onChange: (next) => {
1693
+ setField("timeoutMinutes", next);
1306
1694
  }
1307
1695
  });
1308
- const configPathRow = (0, react.createElement)(TextRow, {
1696
+ const configPathRow = (0, react.createElement)(y, {
1309
1697
  label: t("configPathLabel"),
1310
- hint: t("configPathHint"),
1698
+ ...l(t("configPathHint")),
1311
1699
  field: "ocrConfigPath",
1312
1700
  value: deps.ocrConfigPathValue,
1313
1701
  placeholder: "~/.opencodereview/config.json",
1702
+ monospace: true,
1703
+ wide: true,
1314
1704
  disabled: !writable,
1315
- onCommit: (text) => {
1316
- setField("ocrConfigPath", text);
1705
+ onChange: (next) => {
1706
+ setField("ocrConfigPath", next.trim());
1317
1707
  }
1318
1708
  });
1319
- const saveRow = (0, react.createElement)(SaveBar, {
1320
- t,
1709
+ const saveRow = (0, react.createElement)(S, {
1321
1710
  dirty: deps.dirty,
1322
1711
  writable,
1323
1712
  busy: deps.settingsBusy,
1324
1713
  error: deps.saveError,
1714
+ saveLabel: t("save"),
1715
+ savingLabel: t("saving"),
1716
+ revertLabel: t("revert"),
1717
+ dirtyLabel: t("statusDirty"),
1718
+ cleanLabel: t("statusClean"),
1719
+ readOnlyLabel: t("statusReadOnly"),
1325
1720
  onSave: deps.onSave,
1326
1721
  onDiscard: deps.onDiscard
1327
1722
  });
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);
1723
+ const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("p", { className: "dsu-hint" }, deps.statusHint) : null;
1724
+ return (0, react.createElement)(d, { title: t("reviewSection") }, statusHintNode, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1330
1725
  }
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
1726
  function buildMcpAdmin(props) {
1349
1727
  const { t } = props;
1350
1728
  const textarea = (0, react.createElement)("textarea", {
1351
1729
  "data-field": "mcpJson",
1352
1730
  value: props.mcpJson,
1353
- disabled: !props.writable || props.mcpBusy || !props.available,
1731
+ disabled: !props.writable || props.mcpBusy || !props.available || !props.mcpReady,
1732
+ "aria-label": t("mcpTitle"),
1733
+ className: "dsu-input dsu-textarea dsu-mono ocr-adminTextarea",
1354
1734
  onChange: (event) => {
1355
1735
  props.onMcpChange(event.target.value);
1356
- },
1357
- style: textareaStyle
1736
+ }
1358
1737
  });
1359
1738
  const button = (0, react.createElement)("button", {
1360
1739
  type: "button",
1740
+ className: "dsu-btn",
1361
1741
  "data-field": "mcpSave",
1362
- disabled: !props.writable || props.mcpBusy || !props.available,
1742
+ disabled: !props.writable || props.mcpBusy || !props.available || !props.mcpReady,
1363
1743
  onClick: props.onMcpSave
1364
1744
  }, 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);
1745
+ const message = props.mcpMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.mcpMsg);
1746
+ return (0, react.createElement)(d, {
1747
+ title: t("mcpTitle"),
1748
+ hint: t("mcpHint")
1749
+ }, props.available ? textarea : (0, react.createElement)("p", { className: "dsu-hint" }, t("adminUnavailable")), (0, react.createElement)("div", { className: ACTIONS_CLASS }, button), message);
1367
1750
  }
1368
1751
  function buildAuthAdmin(props) {
1369
1752
  const { t } = props;
1370
1753
  const check = (0, react.createElement)("button", {
1371
1754
  type: "button",
1755
+ className: "dsu-btn",
1372
1756
  "data-field": "authCheck",
1373
1757
  disabled: props.authBusy || !props.available,
1374
1758
  onClick: props.onCheckAuth
@@ -1377,23 +1761,29 @@ function buildAuthAdmin(props) {
1377
1761
  "data-field": "chatGptModel",
1378
1762
  value: props.authModel,
1379
1763
  disabled: !props.writable || props.authBusy || !props.available,
1764
+ "aria-label": t("chatGptModelLabel"),
1765
+ className: "dsu-input",
1380
1766
  onChange: (event) => {
1381
1767
  props.onAuthModelChange(event.target.value);
1382
1768
  }
1383
1769
  });
1384
1770
  const use = (0, react.createElement)("button", {
1385
1771
  type: "button",
1772
+ className: "dsu-btn",
1386
1773
  "data-field": "chatGptUse",
1387
1774
  disabled: !props.writable || props.authBusy || !props.available,
1388
1775
  onClick: props.onUseChatGpt
1389
1776
  }, 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);
1777
+ const output = props.authOutput === null ? null : (0, react.createElement)("pre", { className: "ocr-status dsu-mono" }, props.authOutput);
1778
+ const message = props.authMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.authMsg);
1779
+ return (0, react.createElement)(d, {
1780
+ title: t("chatGptTitle"),
1781
+ hint: t("chatGptHint")
1782
+ }, (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
1783
  }
1394
1784
  /** MCP 配置 + ChatGPT 登录态/切换:都写 OCR 自己的配置,放在设置卡下方。 */
1395
1785
  function buildAdminSection(props) {
1396
- return (0, react.createElement)("div", { style: sectionStyle }, buildMcpAdmin(props), buildAuthAdmin(props));
1786
+ return (0, react.createElement)("div", null, buildMcpAdmin(props), buildAuthAdmin(props));
1397
1787
  }
1398
1788
  /**
1399
1789
  * 读设置页载荷(卡片唯一的数据入口):失败只落 fetchError 文本,不抛穿渲染。
@@ -1405,8 +1795,8 @@ async function refreshProviders(reader, t, sinks) {
1405
1795
  try {
1406
1796
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1407
1797
  const json = await reader.providers();
1408
- if (i(json, "ok") === false) {
1409
- const detail = strOf(i(json, "error"));
1798
+ if (i$1(json, "ok") === false) {
1799
+ const detail = strOf(i$1(json, "error"));
1410
1800
  throw new Error(detail === "" ? t("remoteUnavailable") : detail);
1411
1801
  }
1412
1802
  const payload = providersPayloadOf(json);
@@ -1423,8 +1813,8 @@ function readerPost(reader, t) {
1423
1813
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1424
1814
  switch (action) {
1425
1815
  case "select": {
1426
- const provider = strOf(i(body, "provider"));
1427
- const model = strOf(i(body, "model"));
1816
+ const provider = strOf(i$1(body, "provider"));
1817
+ const model = strOf(i$1(body, "model"));
1428
1818
  const reply = await reader.select(provider, model);
1429
1819
  return { json: () => Promise.resolve(reply) };
1430
1820
  }
@@ -1444,7 +1834,8 @@ function readerPost(reader, t) {
1444
1834
  }
1445
1835
  };
1446
1836
  }
1447
- /** POST select:把选中的 provider/model 写进 OCR 配置,回执里按 autoVerify 决定是否带 llm test。 */
1837
+ /** POST select:把选中的 provider/model 写进 OCR 配置,回执里按 autoVerify 决定是否带 llm test。
1838
+ * 导出与 `runProbe` 同因:卡片上的动作以纯函数形态可测(回执着色与文案都在这里定)。 */
1448
1839
  async function runApplySelection(deps) {
1449
1840
  const { t, effectiveProvider, effectiveModel } = deps;
1450
1841
  if (!effectiveProvider || !effectiveModel) {
@@ -1461,12 +1852,14 @@ async function runApplySelection(deps) {
1461
1852
  provider: effectiveProvider,
1462
1853
  model: effectiveModel
1463
1854
  })).json();
1464
- if (i(selJson, "ok") === true) {
1855
+ if (i$1(selJson, "ok") === true) {
1465
1856
  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}`;
1857
+ let verified = true;
1858
+ const llmTestRaw = i$1(selJson, "llmTest");
1859
+ if (e$3(llmTestRaw)) {
1860
+ const testOutput = strOf(i$1(llmTestRaw, "output")).slice(0, 400);
1861
+ verified = i$1(llmTestRaw, "ok") === true;
1862
+ testText = verified ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1470
1863
  } else testText = t("notVerified");
1471
1864
  const appliedText = t("applied", {
1472
1865
  provider: effectiveProvider,
@@ -1474,12 +1867,12 @@ async function runApplySelection(deps) {
1474
1867
  test: testText
1475
1868
  });
1476
1869
  deps.setActionMsg({
1477
- ok: true,
1870
+ ok: verified,
1478
1871
  text: appliedText
1479
1872
  });
1480
1873
  deps.refresh();
1481
1874
  } else {
1482
- const failure = strOf(i(selJson, "error"));
1875
+ const failure = strOf(i$1(selJson, "error"));
1483
1876
  deps.setActionMsg({
1484
1877
  ok: false,
1485
1878
  text: failure === "" ? t("applyFailed") : failure
@@ -1502,12 +1895,14 @@ async function runMigrateKeys(deps) {
1502
1895
  deps.setActionMsg(null);
1503
1896
  try {
1504
1897
  const json = await (await deps.post("migrate")).json();
1505
- const migratedVal = i(json, "migrated");
1506
- const skippedVal = i(json, "skippedUnknown");
1898
+ const migratedVal = i$1(json, "migrated");
1899
+ const skippedVal = i$1(json, "skippedUnknown");
1507
1900
  const migrated = Array.isArray(migratedVal) ? migratedVal.filter((item) => typeof item === "string") : [];
1508
1901
  const skipped = Array.isArray(skippedVal) ? skippedVal.filter((item) => typeof item === "string") : [];
1509
- const ok = i(json, "ok") === true;
1510
- const head = ok ? (migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey")) + (skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "") : t("migrateFailed");
1902
+ const ok = i$1(json, "ok") === true;
1903
+ const migratedText = migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey");
1904
+ const skippedText = skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "";
1905
+ const head = ok ? migratedText + skippedText : t("migrateFailed");
1511
1906
  deps.setActionMsg({
1512
1907
  ok,
1513
1908
  text: head
@@ -1530,8 +1925,8 @@ async function runTestConnection(deps) {
1530
1925
  deps.setActionMsg(null);
1531
1926
  try {
1532
1927
  const json = await (await deps.post("test")).json();
1533
- const ok = i(json, "ok") === true;
1534
- const testOutput = strOf(i(json, "output"));
1928
+ const ok = i$1(json, "ok") === true;
1929
+ const testOutput = strOf(i$1(json, "output"));
1535
1930
  deps.setActionMsg({
1536
1931
  ok,
1537
1932
  text: (ok ? t("llmTestPassed") : t("llmTestFailed")) + testOutput
@@ -1558,12 +1953,12 @@ async function runProbe(deps) {
1558
1953
  deps.setProbeMsg(null);
1559
1954
  try {
1560
1955
  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"));
1956
+ const ok = i$1(json, "ok") === true;
1957
+ const head = ok ? t("probeVersion", { version: strOf(i$1(json, "version")) }) : t("probeReason", { reason: strOf(i$1(json, "reason")) });
1958
+ const output = strOf(i$1(json, "output"));
1564
1959
  const lines = [
1565
1960
  head,
1566
- strOf(i(json, "changeText")),
1961
+ strOf(i$1(json, "changeText")),
1567
1962
  output
1568
1963
  ].filter((line) => line !== "");
1569
1964
  deps.setProbeMsg({
@@ -1585,8 +1980,8 @@ async function runProbe(deps) {
1585
1980
  * 这时不能清 touched、也不能报成功——否则用户看到「已保存」而配置原封未动。
1586
1981
  * 传输失败仍以 rejection 表达。 */
1587
1982
  async function saveSettingsDraft(deps) {
1588
- const { t: t$3, value, touched } = deps;
1589
- const keys = t(touched, value);
1983
+ const { t, value, touched } = deps;
1984
+ const keys = t$1(touched, value);
1590
1985
  if (keys.length === 0) return;
1591
1986
  deps.setSettingsBusy(true);
1592
1987
  deps.setSaveError(null);
@@ -1612,7 +2007,7 @@ async function saveSettingsDraft(deps) {
1612
2007
  for (const field of refused) kept[field] = touched[field];
1613
2008
  deps.setTouched(kept);
1614
2009
  const reason = failures.length > 0 ? `(${errorSummary(failures[0])})` : "";
1615
- deps.setSaveError(`${t$3("saveRefused", { fields: refused.join(", ") })}${reason}`);
2010
+ deps.setSaveError(`${t("saveRefused", { fields: refused.join(", ") })}${reason}`);
1616
2011
  return;
1617
2012
  }
1618
2013
  deps.setTouched({});
@@ -1620,11 +2015,11 @@ async function saveSettingsDraft(deps) {
1620
2015
  function draftFieldsOf(touched, value) {
1621
2016
  const eff = (field, def) => field in touched ? touched[field] : value[field] ?? def;
1622
2017
  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),
2018
+ effortValue: textOf(eff("effort", "auto"), EFFORT_SCHEMA, "auto"),
2019
+ languageValue: textOf(eff("language", "中文"), LANGUAGE_SCHEMA, "中文"),
2020
+ autoVerifyValue: booleanOf(eff("autoVerify", true), true),
2021
+ maxCommentsValue: numOf(eff("maxComments", 12), 12, COUNT_SCHEMA),
2022
+ timeoutMinutesValue: numOf(eff("timeoutMinutes", 0), 0, COUNT_SCHEMA),
1628
2023
  ocrConfigPathValue: strOf(eff("ocrConfigPath", ""))
1629
2024
  };
1630
2025
  }
@@ -1643,20 +2038,16 @@ function cardHints(t, ready, status, data) {
1643
2038
  /** Provider 区 = provider/model 表单 + 最近一次动作的回执条。 */
1644
2039
  function buildProviderPanel(deps) {
1645
2040
  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);
2041
+ return [buildProviderSection(deps), actionMsg ? (0, react.createElement)("p", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null];
1649
2042
  }
1650
- /** 管理区的状态与动作(MCP 配置 + ChatGPT 登录态/切换);OcrCard 只负责摆位置。 */
1651
- function useAdminPanel(reader, t) {
2043
+ /** MCP 配置编辑器的状态与动作:读、写,以及「没读成功不许写」的判据。 */
2044
+ function useMcpPanel(reader, t) {
1652
2045
  const [mcpJson, setMcpJson] = (0, react.useState)("{}");
1653
2046
  const [mcpBusy, setMcpBusy] = (0, react.useState)(false);
1654
2047
  const [mcpMsg, setMcpMsg] = (0, react.useState)(null);
1655
- const [authOutput, setAuthOutput] = (0, react.useState)(null);
1656
- const [authBusy, setAuthBusy] = (0, react.useState)(false);
1657
- const [authMsg, setAuthMsg] = (0, react.useState)(null);
1658
- const [authModel, setAuthModel] = (0, react.useState)("");
1659
- const [loginCommand, setLoginCommand] = (0, react.useState)("ocr auth login --provider chatgpt");
2048
+ const [mcpReady, setMcpReady] = (0, react.useState)(false);
2049
+ const mcpDirtyRef = (0, react.useRef)(false);
2050
+ const mcpRevisionRef = (0, react.useRef)(void 0);
1660
2051
  const loadMcp = (0, react.useCallback)(async () => {
1661
2052
  if (reader === void 0) {
1662
2053
  setMcpMsg(t("adminUnavailable"));
@@ -1664,10 +2055,17 @@ function useAdminPanel(reader, t) {
1664
2055
  }
1665
2056
  try {
1666
2057
  const reply = await reader.mcp();
1667
- if ("status" in reply) setMcpMsg(reply.error);
1668
- else setMcpJson(reply.json);
2058
+ if ("status" in reply) {
2059
+ setMcpMsg(reply.error);
2060
+ setMcpReady(false);
2061
+ } else {
2062
+ if (!mcpDirtyRef.current) setMcpJson(reply.json);
2063
+ mcpRevisionRef.current = reply.revision;
2064
+ setMcpReady(true);
2065
+ }
1669
2066
  } catch (error) {
1670
2067
  setMcpMsg(`${t("mcpLoadFailed")}${String(error)}`);
2068
+ setMcpReady(false);
1671
2069
  }
1672
2070
  }, [reader, t]);
1673
2071
  (0, react.useEffect)(() => {
@@ -1678,17 +2076,48 @@ function useAdminPanel(reader, t) {
1678
2076
  setMcpMsg(t("adminUnavailable"));
1679
2077
  return;
1680
2078
  }
2079
+ if (!mcpReady) {
2080
+ setMcpMsg(t("mcpNotLoaded"));
2081
+ return;
2082
+ }
1681
2083
  setMcpBusy(true);
1682
2084
  setMcpMsg(null);
1683
2085
  try {
1684
- const reply = await reader.saveMcp(mcpJson);
1685
- setMcpMsg("status" in reply ? reply.error : t("mcpSaved"));
2086
+ const reply = await reader.saveMcp(mcpJson, mcpRevisionRef.current);
2087
+ if ("status" in reply) setMcpMsg(reply.error);
2088
+ else {
2089
+ mcpDirtyRef.current = false;
2090
+ mcpRevisionRef.current = reply.revision;
2091
+ setMcpMsg(t("mcpSaved"));
2092
+ }
1686
2093
  } catch (error) {
1687
2094
  setMcpMsg(String(error));
1688
2095
  } finally {
1689
2096
  setMcpBusy(false);
1690
2097
  }
1691
2098
  }
2099
+ return {
2100
+ mcpJson,
2101
+ mcpReady,
2102
+ mcpBusy,
2103
+ mcpMsg,
2104
+ onMcpChange: (value) => {
2105
+ mcpDirtyRef.current = true;
2106
+ setMcpJson(value);
2107
+ },
2108
+ onMcpSave: () => {
2109
+ saveMcp();
2110
+ }
2111
+ };
2112
+ }
2113
+ /** 管理区的状态与动作(MCP 配置 + ChatGPT 登录态/切换);OcrCard 只负责摆位置。 */
2114
+ function useAdminPanel(reader, t) {
2115
+ const mcp = useMcpPanel(reader, t);
2116
+ const [authOutput, setAuthOutput] = (0, react.useState)(null);
2117
+ const [authBusy, setAuthBusy] = (0, react.useState)(false);
2118
+ const [authMsg, setAuthMsg] = (0, react.useState)(null);
2119
+ const [authModel, setAuthModel] = (0, react.useState)("");
2120
+ const [loginCommand, setLoginCommand] = (0, react.useState)("ocr auth login --provider chatgpt");
1692
2121
  async function checkAuth() {
1693
2122
  if (reader === void 0) {
1694
2123
  setAuthMsg(t("adminUnavailable"));
@@ -1728,18 +2157,12 @@ function useAdminPanel(reader, t) {
1728
2157
  }
1729
2158
  return {
1730
2159
  available: reader !== void 0,
1731
- mcpJson,
1732
- mcpBusy,
1733
- mcpMsg,
2160
+ ...mcp,
1734
2161
  authOutput,
1735
2162
  authBusy,
1736
2163
  authMsg,
1737
2164
  authModel,
1738
2165
  loginCommand,
1739
- onMcpChange: setMcpJson,
1740
- onMcpSave: () => {
1741
- saveMcp();
1742
- },
1743
2166
  onAuthModelChange: setAuthModel,
1744
2167
  onCheckAuth: () => {
1745
2168
  checkAuth();
@@ -1750,25 +2173,34 @@ function useAdminPanel(reader, t) {
1750
2173
  };
1751
2174
  }
1752
2175
  function OcrCard(props) {
1753
- const { t: t$4 } = props;
2176
+ const { t } = props;
1754
2177
  const snap = props.useCard((state) => state);
1755
2178
  const { value } = snap;
1756
2179
  const ready = snap.status === "ready";
1757
2180
  const writable = snap.writable && ready;
1758
- const adminState = useAdminPanel(props.reader, t$4);
2181
+ const adminState = useAdminPanel(props.reader, t);
1759
2182
  const [data, setData] = (0, react.useState)(null);
1760
2183
  const [fetchError, setFetchError] = (0, react.useState)(null);
1761
2184
  const [busy, setBusy] = (0, react.useState)(null);
1762
2185
  const [actionMsg, setActionMsg] = (0, react.useState)(null);
1763
2186
  const [probeMsg, setProbeMsg] = (0, react.useState)(null);
1764
- const refresh = (0, react.useCallback)(async () => refreshProviders(props.reader, t$4, {
1765
- setData,
1766
- setFetchError
1767
- }), [props.reader, t$4]);
2187
+ const refreshSeq = (0, react.useRef)(0);
2188
+ const refresh = (0, react.useCallback)(async () => {
2189
+ refreshSeq.current += 1;
2190
+ const seq = refreshSeq.current;
2191
+ return refreshProviders(props.reader, t, {
2192
+ setData: (payload) => {
2193
+ if (seq === refreshSeq.current) setData(payload);
2194
+ },
2195
+ setFetchError: (message) => {
2196
+ if (seq === refreshSeq.current) setFetchError(message);
2197
+ }
2198
+ });
2199
+ }, [props.reader, t]);
1768
2200
  (0, react.useEffect)(() => {
1769
2201
  refresh();
1770
2202
  }, [refresh]);
1771
- const post = readerPost(props.reader, t$4);
2203
+ const post = readerPost(props.reader, t);
1772
2204
  const [selProvider, setSelProvider] = (0, react.useState)("");
1773
2205
  const [selModel, setSelModel] = (0, react.useState)("");
1774
2206
  const { effectiveProvider, effectiveModel, chosenProvider, modelOptions } = resolveSelection(data?.providers ?? [], data?.current, selProvider, selModel);
@@ -1779,7 +2211,7 @@ function OcrCard(props) {
1779
2211
  setSelModel(first);
1780
2212
  }
1781
2213
  const actions = {
1782
- t: t$4,
2214
+ t,
1783
2215
  post,
1784
2216
  refresh,
1785
2217
  setBusy,
@@ -1790,7 +2222,7 @@ function OcrCard(props) {
1790
2222
  const [touched, setTouched] = (0, react.useState)({});
1791
2223
  const [settingsBusy, setSettingsBusy] = (0, react.useState)(false);
1792
2224
  const [saveError, setSaveError] = (0, react.useState)(null);
1793
- const dirty = t(touched, value).length > 0;
2225
+ const dirty = t$1(touched, value).length > 0;
1794
2226
  const setField = (field, draft) => {
1795
2227
  setTouched((prev) => ({
1796
2228
  ...prev,
@@ -1802,7 +2234,7 @@ function OcrCard(props) {
1802
2234
  setSaveError(null);
1803
2235
  };
1804
2236
  const deps = {
1805
- t: t$4,
2237
+ t,
1806
2238
  data,
1807
2239
  fetchError,
1808
2240
  busy,
@@ -1813,7 +2245,7 @@ function OcrCard(props) {
1813
2245
  effectiveModel,
1814
2246
  chosenProvider,
1815
2247
  modelOptions,
1816
- ...cardHints(t$4, ready, snap.status, data),
2248
+ ...cardHints(t, ready, snap.status, data),
1817
2249
  binary: data?.ocrBinary ?? null,
1818
2250
  probeMsg,
1819
2251
  ...draftFieldsOf(touched, value),
@@ -1833,7 +2265,7 @@ function OcrCard(props) {
1833
2265
  },
1834
2266
  onProbe: () => {
1835
2267
  runProbe({
1836
- t: t$4,
2268
+ t,
1837
2269
  post,
1838
2270
  refresh,
1839
2271
  setBusy,
@@ -1843,7 +2275,7 @@ function OcrCard(props) {
1843
2275
  onField: setField,
1844
2276
  onSave: () => {
1845
2277
  saveSettingsDraft({
1846
- t: t$4,
2278
+ t,
1847
2279
  value,
1848
2280
  touched,
1849
2281
  setTouched,
@@ -1859,15 +2291,10 @@ function OcrCard(props) {
1859
2291
  const settingsSection = buildSettingsSection(deps, writable, setField);
1860
2292
  const adminSection = buildAdminSection({
1861
2293
  ...adminState,
1862
- t: t$4,
2294
+ t,
1863
2295
  writable
1864
2296
  });
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);
2297
+ return (0, react.createElement)(u, null, cliStatus, providerPanel, settingsSection, adminSection);
1871
2298
  }
1872
2299
  const inject = [
1873
2300
  "slots",
@@ -1875,11 +2302,11 @@ const inject = [
1875
2302
  "locale"
1876
2303
  ];
1877
2304
  function apply(ctx) {
1878
- if (!n(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
2305
+ if (!n$1(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
1879
2306
  ctx.effect(() => {
1880
2307
  const tag = document.createElement("style");
1881
2308
  tag.id = "ocr-review-card-css";
1882
- tag.textContent = CARD_CSS;
2309
+ tag.textContent = `${CARD_CSS}\n${c}`;
1883
2310
  document.head.append(tag);
1884
2311
  return () => {
1885
2312
  tag.remove();
@@ -1930,7 +2357,6 @@ exports.OcrCard = OcrCard;
1930
2357
  exports.OcrCliActivation = OcrCliActivation;
1931
2358
  exports.OcrCliBadge = OcrCliBadge;
1932
2359
  exports.OcrCliSection = OcrCliSection;
1933
- exports.SaveBar = SaveBar;
1934
2360
  exports.apply = apply;
1935
2361
  exports.binaryOf = binaryOf;
1936
2362
  exports.binaryStatusText = binaryStatusText;
@@ -1950,6 +2376,7 @@ exports.inject = inject;
1950
2376
  exports.isOwnSubject = isOwnSubject;
1951
2377
  exports.loadCliStatus = loadCliStatus;
1952
2378
  exports.providerOptionLabel = providerOptionLabel;
2379
+ exports.runApplySelection = runApplySelection;
1953
2380
  exports.runProbe = runProbe;
1954
2381
  exports.saveSettingsDraft = saveSettingsDraft;
1955
2382
  return module.exports;