@jayyuen66/dsh-ocr-review 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -7,11 +7,14 @@ window.__ModuleLoader__.load({
7
7
  let exports = module.exports;
8
8
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
9
9
  let react = require("react");
10
- //#region src/ui-messages.ts
10
+ let zod = require("zod");
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: "无未保存的修改",
@@ -50,22 +53,37 @@ const UI_MESSAGES = {
50
53
  skippedPrefix: "\n跳过(settings 中无对应 apiKeyEnv):",
51
54
  migrateFailed: "迁移失败",
52
55
  effortLabel: "审查深度 (effort)",
53
- effortHint: "low=1 轮快速反馈 / medium=2 轮默认 / high=3 轮深查(越深越贵)",
56
+ effortHint: "auto=跟随 OCR 自身配置 / low=1 轮快速反馈 / medium=2 轮 / high=3 轮深查(越深越贵)",
57
+ effortAuto: "auto — 跟随 OCR 配置",
54
58
  effortLow: "low — 快速",
55
- effortMedium: "medium — 均衡(默认)",
59
+ effortMedium: "medium — 均衡",
56
60
  effortHigh: "high — 深入",
57
61
  languageLabel: "评审语言",
58
62
  languageHint: "OCR 评论输出语言(写入 config.language)",
59
63
  autoVerifyLabel: "应用后自动验证",
60
64
  autoVerifyHint: "选择 provider/model 后自动跑 ocr llm test",
61
- onLabel: "开启",
62
- offLabel: "关闭",
63
65
  maxCommentsLabel: "摘要评论条数 (maxComments)",
64
- maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认 12)",
66
+ maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认见本行设置项的初值)",
65
67
  timeoutLabel: "运行超时 (timeoutMinutes)",
66
- timeoutHint: "前台墙钟超时(分钟);0 = 宿主上限(shell.maxTimeoutMs,当前 60 分钟);更长的评审用工具 wait=false 后台模式(不设宿主 deadline,兜底窗口看设置项 ocrBackgroundMaxMs,默认 0 = 不设窗口)(默认 0)",
68
+ timeoutHint: "前台墙钟超时(分钟);0 = 请求宿主上限(实际生效值由 shell 执行器自己的 maxTimeoutMs 设置项决定,可在 bash-local / pwsh-local 的设置行里改);更长的评审用工具 wait=false 后台模式(不设宿主 deadline,兜底窗口看设置项 ocrBackgroundMaxMs,默认 0 = 不设窗口)(默认 0)",
67
69
  configPathLabel: "OCR 配置文件 (ocrConfigPath)",
68
70
  configPathHint: "外部 open-code-review CLI 自己的配置位置;留空 = ~/.opencodereview/config.json。自定义时必须是 <X>/.opencodereview/config.json 布局(OCR 只认该形状,插件经 HOME 重定向让它生效)",
71
+ mcpTitle: "MCP servers",
72
+ mcpHint: "编辑外部 OCR 的 mcp_servers(JSON)。stdio server 会在评审时执行命令,只在这里由人配置;保存只改这一支,其他配置原样保留。",
73
+ mcpSave: "保存 MCP",
74
+ mcpSaving: "保存中…",
75
+ mcpSaved: "MCP servers 已保存",
76
+ mcpLoadFailed: "读取 MCP 配置失败:",
77
+ chatGptTitle: "ChatGPT(OCR auth)",
78
+ chatGptHint: "登录态由 ocr auth 管理;本卡片只读查询状态,并在登录后把 provider 切到 chatgpt。",
79
+ chatGptCheck: "检查登录态",
80
+ chatGptChecking: "检查中…",
81
+ chatGptModelLabel: "ChatGPT 模型 slug",
82
+ chatGptUse: "使用 ChatGPT",
83
+ chatGptUsing: "应用中…",
84
+ chatGptSaved: "已切换到 ChatGPT provider",
85
+ chatGptLoginHint: "登录请在终端运行:",
86
+ adminUnavailable: "当前部署没有远程面,MCP/ChatGPT 管理区不可用",
69
87
  settingsLoading: "设置加载中,稍候可改",
70
88
  keyDynamicHint: "key 走 get-cred.mjs 动态读取,OCR 配置不留明文",
71
89
  keyScriptMissing: "⚠ get-cred 脚本缺失,api_key_cmd 无法工作",
@@ -94,8 +112,10 @@ const UI_MESSAGES = {
94
112
  binaryGuideDismiss: "知道了"
95
113
  },
96
114
  en: {
97
- cardTitle: "open-code-review code review",
98
- 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",
99
119
  statusReadOnly: "This scope is read-only",
100
120
  statusDirty: "Unsaved changes — press Save to apply",
101
121
  statusClean: "No unsaved changes",
@@ -134,22 +154,37 @@ const UI_MESSAGES = {
134
154
  skippedPrefix: "\nSkipped (no apiKeyEnv in settings): ",
135
155
  migrateFailed: "migration failed",
136
156
  effortLabel: "Review depth (effort)",
137
- effortHint: "low = 1 pass quick feedback / medium = 2 passes default / high = 3 passes deep review (deeper costs more)",
157
+ effortHint: "auto = follow the OCR config / low = 1 pass quick feedback / medium = 2 passes / high = 3 passes deep review (deeper costs more)",
158
+ effortAuto: "auto — follow the OCR config",
138
159
  effortLow: "low — quick",
139
- effortMedium: "medium — balanced (default)",
160
+ effortMedium: "medium — balanced",
140
161
  effortHigh: "high — thorough",
141
162
  languageLabel: "Review language",
142
163
  languageHint: "Output language of OCR comments (written into config.language)",
143
164
  autoVerifyLabel: "Verify after applying",
144
165
  autoVerifyHint: "Run `ocr llm test` automatically after picking a provider/model",
145
- onLabel: "On",
146
- offLabel: "Off",
147
166
  maxCommentsLabel: "Summary comment cap (maxComments)",
148
- maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default 12)",
167
+ maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default: the row's initial value)",
149
168
  timeoutLabel: "Run timeout (timeoutMinutes)",
150
- timeoutHint: "Foreground wall-clock timeout in minutes; 0 = host cap (shell.maxTimeoutMs, currently 60 min); for longer reviews use the tool's wait=false background mode (no host deadline; the backstop window is the ocrBackgroundMaxMs setting, default 0 = none) (default 0)",
169
+ timeoutHint: "Foreground wall-clock timeout in minutes; 0 = ask for the host cap (the effective value comes from the maxTimeoutMs setting of the shell executor itself — edit it on the bash-local / pwsh-local settings row); for longer reviews use the tool's wait=false background mode (no host deadline; the backstop window is the ocrBackgroundMaxMs setting, default 0 = none) (default 0)",
151
170
  configPathLabel: "OCR config file (ocrConfigPath)",
152
171
  configPathHint: "Where the external open-code-review CLI keeps its own config; empty = ~/.opencodereview/config.json. A custom value must follow the <X>/.opencodereview/config.json layout (OCR only reads that shape; the plugin makes it effective via a HOME redirect)",
172
+ mcpTitle: "MCP servers",
173
+ mcpHint: "Edit the external OCR mcp_servers JSON. A stdio server runs commands during reviews, so this is human-managed here; saving touches only this key and preserves everything else.",
174
+ mcpSave: "Save MCP",
175
+ mcpSaving: "Saving…",
176
+ mcpSaved: "MCP servers saved",
177
+ mcpLoadFailed: "Failed to read the MCP config: ",
178
+ chatGptTitle: "ChatGPT (OCR auth)",
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.",
180
+ chatGptCheck: "Check sign-in",
181
+ chatGptChecking: "Checking…",
182
+ chatGptModelLabel: "ChatGPT model slug",
183
+ chatGptUse: "Use ChatGPT",
184
+ chatGptUsing: "Applying…",
185
+ chatGptSaved: "Switched to the ChatGPT provider",
186
+ chatGptLoginHint: "Run the sign-in in a terminal:",
187
+ adminUnavailable: "No remote surface in this deployment; the MCP/ChatGPT admin section is unavailable",
153
188
  settingsLoading: "settings still loading — editable in a moment",
154
189
  keyDynamicHint: "Keys are read dynamically via get-cred.mjs; OCR config keeps no plain text",
155
190
  keyScriptMissing: "⚠ get-cred script missing, api_key_cmd cannot work",
@@ -178,48 +213,624 @@ const UI_MESSAGES = {
178
213
  binaryGuideDismiss: "Got it"
179
214
  }
180
215
  };
181
- //#endregion
182
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.6_@deepseek-ai+cordis@4.0.5_@deepseek-ai+dsh-client-lo_8c875f5679b30ed9bd8ed9c09e6b1478/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
183
- function e$2(e) {
216
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
217
+ function e$3(e) {
184
218
  return typeof e == `object` && !!e && !Array.isArray(e);
185
219
  }
186
- function n(t, n) {
187
- return e$2(t) ? t[n] : void 0;
220
+ const t$3 = { absent: !0 };
221
+ function n$2(e, n) {
222
+ try {
223
+ return {
224
+ absent: !1,
225
+ value: e[n]
226
+ };
227
+ } catch {
228
+ return t$3;
229
+ }
230
+ }
231
+ function i$1(r, i) {
232
+ let a = e$3(r) ? n$2(r, i) : t$3;
233
+ return a.absent ? void 0 : a.value;
188
234
  }
189
- //#endregion
190
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.6_@deepseek-ai+cordis@4.0.5_@deepseek-ai+dsh-client-lo_8c875f5679b30ed9bd8ed9c09e6b1478/node_modules/@jayyuen66/dsh-plugin-shared/dist/card-apply.js
191
- function e$1(e, t, n) {
192
- let r = globalThis;
193
- if (Object.hasOwn(r, t) && r[t] === !0) return !1;
194
- r[t] = !0;
235
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-apply.js
236
+ const e$2 = (e) => e.length > 0 && !(e in Object.prototype);
237
+ function t$2(e, t, n) {
238
+ n === void 0 ? e[t] = void 0 : Object.defineProperty(e, t, n);
239
+ }
240
+ function n$1(n, r, i) {
241
+ if (!e$2(r)) throw Error(`claimApply 的 flag 不得为空、也不得命中 Object.prototype 的成员(__proto__ 一类会让占位静默失灵或遮蔽全局成员):${r}`);
242
+ let a = globalThis, o = Object.hasOwn(a, r) ? a[r] : void 0;
243
+ if (o === !0 || typeof o == `symbol`) return !1;
244
+ let s = Object.getOwnPropertyDescriptor(a, r), c = Symbol(r);
245
+ a[r] = c;
195
246
  try {
196
- e.effect(() => () => {
197
- r[t] = void 0;
198
- }, n);
247
+ n.effect(() => () => {
248
+ a[r] === c && t$2(a, r, s);
249
+ }, i);
199
250
  } catch (e) {
200
- throw r[t] = void 0, e;
251
+ throw a[r] === c && t$2(a, r, s), e;
201
252
  }
202
253
  return !0;
203
254
  }
204
- //#endregion
205
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.6_@deepseek-ai+cordis@4.0.5_@deepseek-ai+dsh-client-lo_8c875f5679b30ed9bd8ed9c09e6b1478/node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
206
- function e(e, t) {
207
- let n = [];
208
- for (let r of Object.keys(e)) JSON.stringify(e[r] ?? null) !== JSON.stringify(t[r] ?? null) && n.push(r);
209
- return n;
255
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
256
+ function e$1(t, n, r) {
257
+ if (t == null || n == null) return (t ?? null) === (n ?? null);
258
+ if (Object.is(t, n)) return !0;
259
+ if (typeof t != `object` || typeof n != `object`) return !1;
260
+ if (r.get(t) === n) return !0;
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));
262
+ let i = Object.keys(t), a = Object.keys(n);
263
+ return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e$1(Reflect.get(t, i), Reflect.get(n, i), r));
264
+ }
265
+ function t$1(t, n) {
266
+ let r = [];
267
+ for (let i of Object.keys(t)) e$1(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
268
+ return r;
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)));
210
782
  }
211
- //#endregion
212
- //#region lib/remote-shape.ts
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
+ }
802
+ // lib/remote-shape.ts
213
803
  /** 宿主服务键 = 客户端线上命名空间(两侧共用的唯一来源)。 */
214
804
  const OCR_REMOTE_NAMESPACE = "ocrReview";
215
- //#endregion
216
- //#region src/remote.ts
805
+ /**
806
+ * 本包 bundle 的 npm 包名。descriptor `id`、`$mount` 的 `package` 位、客户端槽位键与
807
+ * package.json 的 `name` 必须逐字相等(宿主按 bundle 包名派发设置卡槽位,改名漏一处
808
+ * 的症状是卡片静默不渲染而不是报错),因此只有这一个定义处,其余各方引它。
809
+ */
810
+ const OCR_BUNDLE_PACKAGE = "@jayyuen66/dsh-ocr-review";
811
+ // src/remote.ts
217
812
  /** 网关给命名空间注册的**服务键**:`remote.<namespace>`。它在 `$mount` 完成之后才存在,
218
813
  * 所以只能在挂载后按这个键去读,而不是提前 inject。 */
219
814
  const OCR_SERVICE_KEY = `remote.${OCR_REMOTE_NAMESPACE}`;
220
- /** 卡片所在 bundle 的包名(`$mount` 的 `package` 位与槽位键同源,见 client-entry.ts 的 BUNDLE_PKG)。 */
221
- const PACKAGE_NAME = "@jayyuen66/dsh-ocr-review";
222
- /** 把宿主那五个方法挂到客户端远程面上的描述符(`id` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
815
+ /** 卡片所在 bundle 的包名(`$mount` 的 `package` 位与槽位键同源;唯一定义在
816
+ * lib/remote-shape.ts,client-entry.ts 的槽位键引的是同一个常量)。 */
817
+ const PACKAGE_NAME = OCR_BUNDLE_PACKAGE;
818
+ /** strict codec 的统一出口:失败交给 zod 的 `prettifyError`,读到的是一份带字段路径的说明。 */
819
+ function parseOrThrow(schema, value) {
820
+ const parsed = schema.safeParse(value);
821
+ if (!parsed.success) throw new TypeError(zod.z.prettifyError(parsed.error));
822
+ return parsed.data;
823
+ }
824
+ const SELECT_MESSAGE = "select request requires string provider and model";
825
+ const SELECT_SCHEMA = zod.z.object({
826
+ provider: zod.z.string({ error: SELECT_MESSAGE }),
827
+ model: zod.z.string({ error: SELECT_MESSAGE })
828
+ }, { error: "select request must be an object" });
829
+ const MCP_SAVE_MESSAGE = "mcp save request requires a string json";
830
+ const MCP_SAVE_SCHEMA = zod.z.object({ json: zod.z.string({ error: MCP_SAVE_MESSAGE }) }, { error: MCP_SAVE_MESSAGE });
831
+ const CHAT_GPT_MESSAGE = "chatgpt request requires a string model";
832
+ const CHAT_GPT_SCHEMA = zod.z.object({ model: zod.z.string({ error: CHAT_GPT_MESSAGE }) }, { error: CHAT_GPT_MESSAGE });
833
+ /** 把宿主那九个方法挂到客户端远程面上的描述符(`id` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
223
834
  * 导出它只为一件事:`test/remote.test.ts` 拿它与宿主侧 `remoteMethods()` 的标记对质
224
835
  * —— 两侧分叉时卡片不会报错,只会"读不到",所以那条比较必须在门禁里。 */
225
836
  const OCR_DESCRIPTORS = [
@@ -246,13 +857,7 @@ const OCR_DESCRIPTORS = [
246
857
  mode: "strict",
247
858
  typeSymbol: `${PACKAGE_NAME}#select-request`,
248
859
  create: () => ({ parse(value) {
249
- if (!e$2(value)) throw new TypeError("select request must be an object");
250
- const { provider, model } = value;
251
- if (typeof provider !== "string" || typeof model !== "string") throw new TypeError("select request requires string provider and model");
252
- return {
253
- provider,
254
- model
255
- };
860
+ return parseOrThrow(SELECT_SCHEMA, value);
256
861
  } })
257
862
  }
258
863
  }],
@@ -284,31 +889,103 @@ const OCR_DESCRIPTORS = [
284
889
  invocation: { kind: "direct" },
285
890
  parameters: [],
286
891
  result: { mode: "src-json" }
892
+ },
893
+ {
894
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/mcp`,
895
+ service: OCR_REMOTE_NAMESPACE,
896
+ namespace: OCR_REMOTE_NAMESPACE,
897
+ method: "mcp",
898
+ invocation: { kind: "direct" },
899
+ parameters: [],
900
+ result: { mode: "src-json" }
901
+ },
902
+ {
903
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/saveMcp`,
904
+ service: OCR_REMOTE_NAMESPACE,
905
+ namespace: OCR_REMOTE_NAMESPACE,
906
+ method: "saveMcp",
907
+ invocation: { kind: "direct" },
908
+ parameters: [{
909
+ name: "request",
910
+ wire: "request",
911
+ source: "json",
912
+ codec: {
913
+ mode: "strict",
914
+ typeSymbol: `${PACKAGE_NAME}#mcp-save-request`,
915
+ create: () => ({ parse(value) {
916
+ return parseOrThrow(MCP_SAVE_SCHEMA, value);
917
+ } })
918
+ }
919
+ }],
920
+ result: { mode: "src-json" }
921
+ },
922
+ {
923
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/auth`,
924
+ service: OCR_REMOTE_NAMESPACE,
925
+ namespace: OCR_REMOTE_NAMESPACE,
926
+ method: "auth",
927
+ invocation: { kind: "direct" },
928
+ parameters: [],
929
+ result: { mode: "src-json" }
930
+ },
931
+ {
932
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/chatGpt`,
933
+ service: OCR_REMOTE_NAMESPACE,
934
+ namespace: OCR_REMOTE_NAMESPACE,
935
+ method: "chatGpt",
936
+ invocation: { kind: "direct" },
937
+ parameters: [{
938
+ name: "request",
939
+ wire: "request",
940
+ source: "json",
941
+ codec: {
942
+ mode: "strict",
943
+ typeSymbol: `${PACKAGE_NAME}#chatgpt-request`,
944
+ create: () => ({ parse(value) {
945
+ return parseOrThrow(CHAT_GPT_SCHEMA, value);
946
+ } })
947
+ }
948
+ }],
949
+ result: { mode: "src-json" }
287
950
  }
288
951
  ];
289
952
  /** 这个值能当远程面用吗(只核我们要调的那一位;服务来自别的实现或版本时保守当作没有)。 */
290
- const isRemoteMounter = (value) => e$2(value) && typeof value["$mount"] === "function";
953
+ const isRemoteMounter = (value) => e$3(value) && typeof value["$mount"] === "function";
291
954
  /** 同上:命名空间服务(挂载之后才有,缺它说明网关没认这枚贡献)。 */
292
- 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";
955
+ const isOcrNamespace = (value) => e$3(value) && typeof value["providers"] === "function" && typeof value["select"] === "function" && typeof value["migrate"] === "function" && typeof value["test"] === "function" && typeof value["probe"] === "function" && typeof value["mcp"] === "function" && typeof value["saveMcp"] === "function" && typeof value["auth"] === "function" && typeof value["chatGpt"] === "function";
293
956
  /** 网关回执 → 值;载体/网关失败抛(卡片那层按「请求失败」显示)。 */
294
957
  function unwrap(result) {
295
958
  if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
296
959
  return result.value;
297
960
  }
298
961
  /**
299
- * 造出真正的读取器(挂载一次 + 每次读命名空间服务)。
962
+ * 造出真正的读取器。`remote` 不再是构造期快照:本插件与官方网关之间没有加载顺序
963
+ * 保证(本文件刻意不把 `remote` 写进 `inject`),网关可能在卡片注册之后才
964
+ * `provide("remote")`,所以**每次读取都现取**——缺席报可读错误,晚到也能被下一次
965
+ * 读取接上。挂载 promise 仍缓存(`$mount` 幂等),但拒绝态不缓存:失败后清空,
966
+ * 下一次读取重新挂载,没有「错一次就永远错」的路径。
300
967
  * @param ctx - 客户端上下文
301
- * @param remote - 已核过形状的远程面
302
968
  * @returns 读取器
303
969
  */
304
- function buildOcrReader(ctx, remote) {
970
+ function buildOcrReader(ctx) {
305
971
  let mounted;
972
+ const remoteOf = () => {
973
+ const remote = ctx.get?.("remote", false);
974
+ if (!isRemoteMounter(remote)) throw new Error("remote 未提供:当前部署没有远程读面(每次读取都会重新解析)");
975
+ return remote;
976
+ };
306
977
  const namespaceOf = async () => {
978
+ const remote = remoteOf();
307
979
  mounted ??= remote.$mount({
308
980
  package: PACKAGE_NAME,
309
981
  descriptors: OCR_DESCRIPTORS
310
982
  });
311
- await mounted;
983
+ try {
984
+ await mounted;
985
+ } catch (error) {
986
+ mounted = void 0;
987
+ throw error;
988
+ }
312
989
  const namespace = ctx.get?.(OCR_SERVICE_KEY, false);
313
990
  if (!isOcrNamespace(namespace)) throw new Error(`${OCR_SERVICE_KEY} 未提供:宿主网关没有挂上这枚命名空间`);
314
991
  return namespace;
@@ -331,25 +1008,37 @@ function buildOcrReader(ctx, remote) {
331
1008
  },
332
1009
  async probe() {
333
1010
  return unwrap(await (await namespaceOf()).probe());
1011
+ },
1012
+ async mcp() {
1013
+ return unwrap(await (await namespaceOf()).mcp());
1014
+ },
1015
+ async saveMcp(json) {
1016
+ return unwrap(await (await namespaceOf()).saveMcp({ json }));
1017
+ },
1018
+ async auth() {
1019
+ return unwrap(await (await namespaceOf()).auth());
1020
+ },
1021
+ async chatGpt(model) {
1022
+ return unwrap(await (await namespaceOf()).chatGpt({ model }));
334
1023
  }
335
1024
  };
336
1025
  }
337
1026
  /**
338
- * 造一个「读/写宿主侧 OCR 配置」的读取器;**当前部署没有远程面时返回 `undefined`**
339
- * (卡片照常渲染,只是数据面显示读不到 —— 与 host 侧可选读面同一条口径)。
1027
+ * 造一个「读/写宿主侧 OCR 配置」的读取器。**当前部署没有远程面时读取会抛**可读错误
1028
+ * (卡片那层按「请求失败」显示原因)——remote 不在构造期做快照,网关晚于卡片注册
1029
+ * `provide("remote")` 的部署在首次成功读取时就能接上,不存在「apply 期没有就永远
1030
+ * 读不到」的路径。
340
1031
  *
341
- * `$mount` 是**幂等**的(挂进本 fiber,随卡片卸载自动撤),所以这里缓存那枚 promise:
342
- * 同一次会话里反复拉取只挂一次。
1032
+ * `$mount` 是**幂等**的(挂进本 fiber,随卡片卸载自动撤),所以缓存那枚 promise:
1033
+ * 同一次会话里反复拉取只挂一次;拒绝态例外(见 buildOcrReader)。
343
1034
  *
344
1035
  * @param ctx - 客户端上下文(只用 `get` 一位)
345
- * @returns 读取器;没有远程面时 `undefined`
1036
+ * @returns 读取器
346
1037
  */
347
1038
  function createOcrReader(ctx) {
348
- const remote = ctx.get?.("remote");
349
- return isRemoteMounter(remote) ? buildOcrReader(ctx, remote) : void 0;
1039
+ return buildOcrReader(ctx);
350
1040
  }
351
- //#endregion
352
- //#region src/client-entry.ts
1041
+ // src/client-entry.ts
353
1042
  const NS = "ocr-review";
354
1043
  /**
355
1044
  * `plugins.bundle.config` 是 keyed 槽,宿主派发的 entryKey 就是 bundle 的 **npm 包名**:
@@ -365,43 +1054,75 @@ const NS = "ocr-review";
365
1054
  * (不是回落、不是报错)。值 = 本包 package.json 的 name,且必须出现在 profile 的
366
1055
  * `dsh.profile.bundles` 里;test/build-client.test.ts 从
367
1056
  * `~/.dsh/profiles/web/package.json` 解析校验,不在此处硬抄一份到测试。
368
- * 设置命名空间**不跟着改**:下面 `ctx.configForms.get(NS)` 仍吃裸条目 id(installed
369
- * dsh-client-ui-settings/lib/client.js:1309-1315 把入参原样当 settings 命名空间用)。
1057
+ * 唯一定义在 lib/remote-shape.ts 的 OCR_BUNDLE_PACKAGE(descriptor id / $mount 位
1058
+ * 与这里是同一份值),设置命名空间**不跟着改**:下面 `ctx.configForms.get(NS)` 仍吃
1059
+ * 裸条目 id(installed dsh-client-ui-settings/lib/client.js:1309-1315 把入参原样当
1060
+ * settings 命名空间用)。
370
1061
  */
371
- const BUNDLE_PKG = "@jayyuen66/dsh-ocr-review";
1062
+ const BUNDLE_PKG = OCR_BUNDLE_PACKAGE;
372
1063
  /** 未知值 → 安全字符串(非字符串给 '')。 */
373
1064
  function strOf(value) {
374
1065
  return typeof value === "string" ? value : "";
375
1066
  }
376
- /** 未知值 → 安全数字(非 number 给 fallback)。 */
377
- function numOf(value, fallback) {
378
- return typeof value === "number" ? value : fallback;
1067
+ /** 设置行的取值域:枚举/计数都交给 zod 声明,界面只消费解析后的类型。 */
1068
+ const EFFORT_SCHEMA = zod.z.enum([
1069
+ "auto",
1070
+ "low",
1071
+ "medium",
1072
+ "high"
1073
+ ]);
1074
+ const LANGUAGE_SCHEMA = zod.z.enum(["中文", "English"]);
1075
+ const COUNT_SCHEMA = zod.z.int().min(0);
1076
+ /** 未知值 → 安全数字(非整数/越界给 fallback)。 */
1077
+ function numOf(value, fallback, schema) {
1078
+ const parsed = schema.safeParse(value);
1079
+ return parsed.success ? parsed.data : fallback;
1080
+ }
1081
+ /** 未知值 → 枚举里的一个取值(域外给 fallback)。 */
1082
+ function textOf(value, schema, fallback) {
1083
+ const parsed = schema.safeParse(value);
1084
+ return parsed.success ? parsed.data : fallback;
1085
+ }
1086
+ /** 未知值 → 布尔(非布尔给 fallback)。 */
1087
+ function booleanOf(value, fallback) {
1088
+ const parsed = zod.z.boolean().safeParse(value);
1089
+ return parsed.success ? parsed.data : fallback;
379
1090
  }
380
1091
  /** 错误摘要(非 Error 抛出也要可读;catch 分支共用)。 */
381
1092
  function errorSummary(error) {
382
1093
  return String(error instanceof Error ? error.message : error);
383
1094
  }
384
- function modelOf(raw) {
385
- return {
386
- id: strOf(n(raw, "id")),
387
- name: strOf(n(raw, "name"))
388
- };
389
- }
390
- function providerOf(raw) {
391
- const modelsArr = n(raw, "models");
392
- const models = Array.isArray(modelsArr) ? modelsArr.map((item) => modelOf(item)) : [];
393
- const apiKeyEnvRaw = n(raw, "apiKeyEnv");
394
- return {
395
- name: strOf(n(raw, "name")),
396
- displayName: strOf(n(raw, "displayName")),
397
- apiKeyEnv: typeof apiKeyEnvRaw === "string" ? apiKeyEnvRaw : null,
398
- baseURL: strOf(n(raw, "baseURL")),
399
- models,
400
- hasKey: n(raw, "hasKey") === true
401
- };
402
- }
1095
+ const STRING_OR_EMPTY = zod.z.preprocess((value) => typeof value === "string" ? value : "", zod.z.string());
1096
+ const NULLABLE_STRING = zod.z.preprocess((value) => typeof value === "string" ? value : null, zod.z.string().nullable());
1097
+ const TRUE_ONLY = zod.z.preprocess((value) => value === true, zod.z.boolean());
1098
+ const MODEL_SCHEMA = zod.z.object({
1099
+ id: STRING_OR_EMPTY,
1100
+ name: STRING_OR_EMPTY
1101
+ });
1102
+ const PROVIDER_SCHEMA = zod.z.object({
1103
+ name: STRING_OR_EMPTY,
1104
+ displayName: STRING_OR_EMPTY,
1105
+ apiKeyEnv: NULLABLE_STRING,
1106
+ baseURL: STRING_OR_EMPTY,
1107
+ models: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(MODEL_SCHEMA)),
1108
+ hasKey: TRUE_ONLY
1109
+ });
1110
+ const CURRENT_SCHEMA = zod.z.object({
1111
+ provider: STRING_OR_EMPTY,
1112
+ model: STRING_OR_EMPTY,
1113
+ keyIsDynamic: TRUE_ONLY,
1114
+ url: STRING_OR_EMPTY
1115
+ });
1116
+ const SOURCE_SCHEMA = zod.z.object({
1117
+ status: STRING_OR_EMPTY,
1118
+ message: STRING_OR_EMPTY
1119
+ });
1120
+ const ENDPOINT_VERIFY_SCHEMA = zod.z.object({
1121
+ ok: TRUE_ONLY,
1122
+ message: STRING_OR_EMPTY
1123
+ });
403
1124
  /** 可得性来源的合法域(与 host 侧 `OcrBinarySource` 同一份取值)。形状不合即「没报」。 */
404
- const BINARY_SOURCES = /* @__PURE__ */ new Set([
1125
+ const BINARY_SOURCE_SCHEMA = zod.z.enum([
405
1126
  "bundled",
406
1127
  "path",
407
1128
  "none"
@@ -414,22 +1135,25 @@ const BINARY_SOURCES = /* @__PURE__ */ new Set([
414
1135
  * 作废——「宿主报了 PATH 却没报版本」仍然是一条用户要看的事实。`measuredVersion` 例外,
415
1136
  * 它恒在(null = 还没测过),因为「没测过」与「旧宿主不带这字段」在卡片上是同一句话。
416
1137
  */
1138
+ const BINARY_SCHEMA = zod.z.object({
1139
+ source: BINARY_SOURCE_SCHEMA,
1140
+ path: STRING_OR_EMPTY,
1141
+ channel: STRING_OR_EMPTY,
1142
+ expectedVersion: STRING_OR_EMPTY,
1143
+ installHint: STRING_OR_EMPTY,
1144
+ measuredVersion: NULLABLE_STRING
1145
+ });
417
1146
  function binaryOf(raw) {
418
- if (!e$2(raw)) return null;
419
- const source = strOf(raw["source"]);
420
- if (!BINARY_SOURCES.has(source)) return null;
421
- const path = strOf(raw["path"]);
422
- const channel = strOf(raw["channel"]);
423
- const expectedVersion = strOf(raw["expectedVersion"]);
424
- const installHint = strOf(raw["installHint"]);
425
- const measured = raw["measuredVersion"];
1147
+ const parsed = BINARY_SCHEMA.safeParse(raw);
1148
+ if (!parsed.success) return null;
1149
+ const { source, path, channel, expectedVersion, installHint, measuredVersion } = parsed.data;
426
1150
  return {
427
1151
  source,
428
1152
  ...path === "" ? {} : { path },
429
1153
  ...channel === "" ? {} : { channel },
430
1154
  ...expectedVersion === "" ? {} : { expectedVersion },
431
1155
  ...installHint === "" ? {} : { installHint },
432
- measuredVersion: typeof measured === "string" ? measured : null
1156
+ measuredVersion
433
1157
  };
434
1158
  }
435
1159
  /**
@@ -440,67 +1164,55 @@ function binaryOf(raw) {
440
1164
  async function loadCliStatus(reader) {
441
1165
  if (reader === void 0) return null;
442
1166
  try {
443
- return binaryOf(n(await reader.providers(), "ocrBinary"));
1167
+ return binaryOf(i$1(await reader.providers(), "ocrBinary"));
444
1168
  } catch {
445
1169
  return null;
446
1170
  }
447
1171
  }
1172
+ const EMPTY_PAYLOAD = {
1173
+ providers: [],
1174
+ current: {
1175
+ provider: "",
1176
+ model: "",
1177
+ keyIsDynamic: false,
1178
+ url: ""
1179
+ },
1180
+ getCredReady: false,
1181
+ source: {
1182
+ status: "",
1183
+ message: ""
1184
+ },
1185
+ endpointVerify: null,
1186
+ ocrBinary: null
1187
+ };
1188
+ const PROVIDERS_PAYLOAD_SCHEMA = zod.z.object({
1189
+ providers: zod.z.preprocess((value) => Array.isArray(value) ? value.map((item) => e$3(item) ? item : {}) : [], zod.z.array(PROVIDER_SCHEMA)),
1190
+ current: zod.z.preprocess((value) => e$3(value) ? value : {}, CURRENT_SCHEMA),
1191
+ getCredReady: TRUE_ONLY,
1192
+ source: zod.z.preprocess((value) => e$3(value) ? value : {}, SOURCE_SCHEMA),
1193
+ endpointVerify: zod.z.preprocess((value) => {
1194
+ const parsed = ENDPOINT_VERIFY_SCHEMA.safeParse(value);
1195
+ return parsed.success ? parsed.data : null;
1196
+ }, ENDPOINT_VERIFY_SCHEMA.nullable()),
1197
+ ocrBinary: zod.z.unknown().transform((value) => binaryOf(value))
1198
+ });
448
1199
  function providersPayloadOf(json) {
449
- const providersArr = n(json, "providers");
450
- const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
451
- const currentRaw = n(json, "current");
452
- const current = {
453
- provider: strOf(n(currentRaw, "provider")),
454
- model: strOf(n(currentRaw, "model")),
455
- keyIsDynamic: n(currentRaw, "keyIsDynamic") === true,
456
- url: strOf(n(currentRaw, "url"))
457
- };
458
- const sourceRaw = n(json, "source");
459
- const verifyRaw = n(json, "endpointVerify");
460
- const endpointVerify = verifyRaw === null || verifyRaw === void 0 ? null : {
461
- ok: n(verifyRaw, "ok") === true,
462
- message: strOf(n(verifyRaw, "message"))
463
- };
464
- return {
465
- providers,
466
- current,
467
- getCredReady: n(json, "getCredReady") === true,
468
- source: {
469
- status: strOf(n(sourceRaw, "status")),
470
- message: strOf(n(sourceRaw, "message"))
471
- },
472
- endpointVerify,
473
- ocrBinary: binaryOf(n(json, "ocrBinary"))
474
- };
1200
+ const parsed = PROVIDERS_PAYLOAD_SCHEMA.safeParse(json);
1201
+ return parsed.success ? parsed.data : EMPTY_PAYLOAD;
475
1202
  }
476
- /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
477
- * 抽出来是因为两处 input 与一处 select 必须同宽同边框:写三遍字面量时改一处漏两处,
478
- * 症状是设置卡里某一行的控件比其它行窄。 */
479
- const FORM_CONTROL_CLASS = "ocr-select";
480
- /** 小节标题的类名(卡片头、CLI 状态行、详情页区块共用;sonarjs 的重复字面量门禁)。 */
1203
+ /** 动作按钮行的类名(共享组件的行内按钮组;多处共用,见 sonarjs 的重复字面量门禁)。 */
1204
+ const ACTIONS_CLASS = "ocr-actions";
1205
+ /** 小节标题的类名(详情页区块共用;sonarjs 的重复字面量门禁)。 */
481
1206
  const CARD_NAME_CLASS = "ocr-card-name";
482
1207
  const CARD_CSS = [
483
- ".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}",
484
- ".ocr-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
485
- ".ocr-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
486
- ".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}",
487
- ".ocr-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
488
- ".ocr-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
489
- ".ocr-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
490
- ".ocr-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
491
- ".ocr-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}",
492
- ".ocr-card-chevron-open{transform:rotate(180deg)}",
493
- ".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}",
494
- ".ocr-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 0}",
495
- ".ocr-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);margin-top:2px}",
496
- ".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}",
497
- ".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)}",
498
- ".ocr-btn:disabled{opacity:.5;cursor:not-allowed}",
499
- ".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)}",
500
- ".ocr-status{font-size:12px;margin-top:6px;white-space:pre-wrap;word-break:break-word}",
501
- ".ocr-err{color:#c4483f}",
1208
+ ".ocr-card-name{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.4}",
1209
+ ".ocr-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8f99);line-height:1.5}",
1210
+ ".ocr-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
1211
+ ".ocr-status{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;margin:8px 0 0}",
1212
+ ".ocr-err{color:var(--dsw-alias-state-error-primary)}",
502
1213
  ".ocr-ok{color:var(--dsw-alias-label-primary,inherit)}",
503
- ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}"
1214
+ ".ocr-muted{color:var(--dsw-alias-label-tertiary,#8a8f99)}",
1215
+ ".ocr-adminTextarea{min-height:160px}"
504
1216
  ].join("\n");
505
1217
  function cardStore(scope) {
506
1218
  let cachedSnap = null;
@@ -528,187 +1240,6 @@ function cardStore(scope) {
528
1240
  }
529
1241
  };
530
1242
  }
531
- function SaveBar(props) {
532
- const { t } = props;
533
- const disabled = !props.writable || props.busy;
534
- const btnStyle = {
535
- appearance: "auto",
536
- font: "inherit",
537
- fontSize: "12px",
538
- cursor: disabled || !props.dirty ? "not-allowed" : "pointer",
539
- borderRadius: "6px",
540
- padding: "4px 12px",
541
- border: "1px solid var(--dsw-alias-border-l2,#d8dbe2)",
542
- background: "var(--dsw-alias-bg-layer-1,transparent)",
543
- color: "var(--dsw-alias-label-primary,inherit)",
544
- opacity: disabled ? "0.5" : "1"
545
- };
546
- const saveBtn = (0, react.createElement)("button", {
547
- type: "button",
548
- "data-field": "save",
549
- disabled: disabled || !props.dirty,
550
- onClick: props.onSave,
551
- style: {
552
- ...btnStyle,
553
- background: "var(--dsw-alias-brand-primary,#2f6fed)",
554
- borderColor: "var(--dsw-alias-brand-primary,#2f6fed)",
555
- color: "var(--dsw-alias-label-primary-foreground,#fff)"
556
- }
557
- }, props.busy ? t("saving") : t("save"));
558
- const discardBtn = (0, react.createElement)("button", {
559
- type: "button",
560
- "data-field": "discard",
561
- disabled: disabled || !props.dirty,
562
- onClick: props.onDiscard,
563
- style: btnStyle
564
- }, t("revert"));
565
- const dirtyText = props.dirty ? t("statusDirty") : t("statusClean");
566
- let statusNode;
567
- if (props.error !== null) statusNode = (0, react.createElement)("span", { style: {
568
- fontSize: 12,
569
- color: "#c4483f"
570
- } }, props.error);
571
- else if (props.writable) statusNode = (0, react.createElement)("span", { style: {
572
- fontSize: 12,
573
- color: "var(--dsw-alias-label-tertiary,#8a8f99)"
574
- } }, dirtyText);
575
- else statusNode = (0, react.createElement)("span", { style: {
576
- fontSize: 12,
577
- color: "var(--dsw-alias-label-tertiary,#8a8f99)"
578
- } }, t("statusReadOnly"));
579
- return (0, react.createElement)("div", { style: {
580
- display: "flex",
581
- gap: 8,
582
- alignItems: "center",
583
- padding: "10px 0 2px",
584
- borderTop: "1px dashed var(--dsw-alias-border-l2,#d8dbe2)",
585
- marginTop: 6,
586
- flexWrap: "wrap"
587
- } }, saveBtn, discardBtn, statusNode);
588
- }
589
- /** 设置卡外壳:复刻同款自研卡片(边框圆角卡片 + 可折叠 header + chevron)。 */
590
- function OcrPluginCard(props) {
591
- const { title, description, children } = props;
592
- const [open, setOpen] = (0, react.useState)(false);
593
- const pathKey = "d";
594
- 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));
595
- const chevron = (0, react.createElement)("svg", {
596
- width: 14,
597
- height: 14,
598
- viewBox: "0 0 14 14",
599
- "aria-hidden": true,
600
- className: `ocr-card-chevron${open ? " ocr-card-chevron-open" : ""}`
601
- }, (0, react.createElement)("path", {
602
- [pathKey]: "M3 5l4 4 4-4",
603
- fill: "none",
604
- stroke: "currentColor",
605
- strokeWidth: 1.5,
606
- strokeLinecap: "round",
607
- strokeLinejoin: "round"
608
- }));
609
- const header = (0, react.createElement)("button", {
610
- type: "button",
611
- className: "ocr-card-header",
612
- "aria-expanded": open,
613
- onClick: () => {
614
- setOpen(!open);
615
- }
616
- }, headRow, chevron);
617
- const body = open ? (0, react.createElement)("div", { className: "ocr-card-body" }, children) : null;
618
- return (0, react.createElement)("div", { className: `ocr-card${open ? " ocr-card-open" : ""}` }, header, body);
619
- }
620
- /** 整数数字输入行(保存条模式:合法输入上报草稿,不直写)。 */
621
- function NumberRow(props) {
622
- const { label, hint, value, min, max, disabled, onCommit } = props;
623
- const [draft, setDraft] = (0, react.useState)(String(value));
624
- (0, react.useEffect)(() => {
625
- setDraft(String(value));
626
- }, [value]);
627
- const stage = () => {
628
- const parsed = Number(draft);
629
- if (Number.isInteger(parsed) && parsed >= min && (max === void 0 || parsed <= max)) onCommit(parsed);
630
- };
631
- return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
632
- minWidth: 0,
633
- marginRight: 12
634
- } }, (0, react.createElement)("div", {}, label), (0, react.createElement)("div", { className: "ocr-hint" }, hint)), (0, react.createElement)("input", {
635
- type: "number",
636
- "data-field": props.field,
637
- value: draft,
638
- min,
639
- max,
640
- disabled,
641
- "aria-label": label,
642
- onChange: (ev) => {
643
- setDraft(ev.target.value);
644
- stage();
645
- },
646
- onKeyDown: (ev) => {
647
- if (ev.key === "Enter") {
648
- ev.preventDefault();
649
- stage();
650
- }
651
- },
652
- className: FORM_CONTROL_CLASS
653
- }));
654
- }
655
- /** 文本输入行(保存条模式:改动只上报草稿)。空串是合法取值(= 用宿主默认位置),
656
- * 所以不像 NumberRow 那样要求「解析成功」才上报。 */
657
- function TextRow(props) {
658
- const { label, hint, value, placeholder, disabled, onCommit } = props;
659
- const [draft, setDraft] = (0, react.useState)(value);
660
- (0, react.useEffect)(() => {
661
- setDraft(value);
662
- }, [value]);
663
- const stage = () => {
664
- onCommit(draft.trim());
665
- };
666
- return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
667
- minWidth: 0,
668
- marginRight: 12
669
- } }, (0, react.createElement)("div", {}, label), (0, react.createElement)("div", { className: "ocr-hint" }, hint)), (0, react.createElement)("input", {
670
- type: "text",
671
- "data-field": props.field,
672
- value: draft,
673
- placeholder,
674
- disabled,
675
- "aria-label": label,
676
- className: FORM_CONTROL_CLASS,
677
- style: { width: "min(60%,320px)" },
678
- onChange: (ev) => {
679
- setDraft(ev.target.value);
680
- stage();
681
- },
682
- onKeyDown: (ev) => {
683
- if (ev.key === "Enter") {
684
- ev.preventDefault();
685
- stage();
686
- }
687
- }
688
- }));
689
- }
690
- function SelectRow(props) {
691
- const current = typeof props.current === "string" ? props.current : "";
692
- const options = props.options.map((option) => (0, react.createElement)("option", {
693
- key: option.value,
694
- value: option.value,
695
- disabled: option.disabled === true
696
- }, option.label));
697
- const labelBlock = (0, react.createElement)("div", { style: {
698
- minWidth: 0,
699
- marginRight: 12
700
- } }, (0, react.createElement)("div", {}, props.label), (0, react.createElement)("div", { className: "ocr-hint" }, props.hint));
701
- const select = (0, react.createElement)("select", {
702
- className: FORM_CONTROL_CLASS,
703
- "aria-label": props.label,
704
- value: current,
705
- disabled: props.disabled,
706
- onChange: (ev) => {
707
- props.onChange(props.field, ev.target.value);
708
- }
709
- }, options);
710
- return (0, react.createElement)("div", { className: "ocr-row" }, labelBlock, select);
711
- }
712
1243
  /** provider 下拉项文本(没有 key 的加后缀并被禁用)。提成模块级函数是为了不把
713
1244
  * 带插值的 t() 嵌进 createElement(unicorn/max-nested-calls 上限 3)。 */
714
1245
  function providerOptionLabel(t, provider) {
@@ -785,35 +1316,30 @@ function statusClass(ok) {
785
1316
  */
786
1317
  function buildCliStatusRow(deps) {
787
1318
  const { t } = deps;
788
- const label = (0, react.createElement)("div", { className: CARD_NAME_CLASS }, t("binaryTitle"));
789
1319
  const button = (0, react.createElement)("button", {
790
- className: "ocr-btn",
1320
+ className: "dsu-btn",
791
1321
  type: "button",
792
1322
  disabled: deps.busy !== null,
793
1323
  onClick: deps.onProbe,
794
1324
  "data-field": "probe"
795
1325
  }, deps.busy === "probe" ? t("detecting") : t("detectCli"));
796
- const row = (0, react.createElement)("div", {
797
- className: "ocr-row",
798
- style: {
799
- justifyContent: "flex-start",
800
- gap: 8
801
- }
802
- }, label, button);
803
- const statusText = binaryStatusText(t, deps.binary);
804
- const status = (0, react.createElement)("div", { className: "ocr-hint" }, statusText);
1326
+ const controls = (0, react.createElement)("div", {
1327
+ className: ACTIONS_CLASS,
1328
+ "data-field": "cli-status"
1329
+ }, button);
1330
+ const status = (0, react.createElement)("p", { className: "dsu-hint" }, binaryStatusText(t, deps.binary));
805
1331
  const versionText = cliVersionText(t, deps.binary);
806
- const versionLine = versionText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, versionText);
1332
+ const versionLine = versionText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, versionText);
807
1333
  const hintText = cliInstallText(deps.binary);
808
- const installLine = hintText === "" ? null : (0, react.createElement)("div", { className: "ocr-hint" }, hintText);
1334
+ const installLine = hintText === "" ? null : (0, react.createElement)("p", { className: "dsu-hint" }, hintText);
809
1335
  const probe = deps.probeMsg ? (0, react.createElement)("div", {
810
1336
  className: statusClass(deps.probeMsg.ok),
811
1337
  "data-field": "probe-result"
812
1338
  }, deps.probeMsg.text) : null;
813
- return (0, react.createElement)("div", {
814
- style: { padding: "2px 0" },
815
- "data-field": "cli-status"
816
- }, row, status, versionLine, installLine, probe);
1339
+ return (0, react.createElement)(d, {
1340
+ title: t("binaryTitle"),
1341
+ hint: t("binaryProbeHint")
1342
+ }, status, versionLine, installLine, controls, probe);
817
1343
  }
818
1344
  /**
819
1345
  * 徽标用的短文案(标题旁那只标签放不下整句装法)。与 `binaryStatusText` 同一判据、
@@ -941,100 +1467,165 @@ function OcrCliActivation(props) {
941
1467
  if (!mine || status?.source !== "none") return null;
942
1468
  return buildCliActivation(props.t, props, cliInstallText(status));
943
1469
  }
944
- /** 数据齐(source.status === "ready")那一支:provider/model 两行 + 三个动作按钮 +
945
- * 当前配置与 key 两行提示。拆出来是因为这一支画的是整块表单,与上面三条「一行提示」
946
- * 的降级支本就不是同一件事。 */
947
- function buildReadyProviderSection(deps, data) {
1470
+ /** 数据齐时的动作行:三个按钮共用一只在飞旗标,按钮文案只跟**自己**那条走。 */
1471
+ function buildActionField(deps) {
948
1472
  const { t } = deps;
949
- const providerOptions = data.providers.map((provider) => ({
950
- value: provider.name,
951
- label: providerOptionLabel(t, provider),
952
- disabled: !provider.hasKey
953
- }));
954
- const fallbackModelName = deps.effectiveModel === "" ? t("noModel") : deps.effectiveModel;
955
- const modelOptions = (deps.modelOptions.length > 0 ? deps.modelOptions : [{
956
- id: deps.effectiveModel,
957
- name: fallbackModelName
958
- }]).map((model) => ({
959
- value: model.id,
960
- label: model.name
961
- }));
962
- const providerRow = (0, react.createElement)(SelectRow, {
963
- label: "Provider",
964
- hint: t("providerHint"),
965
- field: "ocrProvider",
966
- current: deps.effectiveProvider,
967
- disabled: data.providers.length === 0,
968
- options: providerOptions,
969
- onChange: (field, value) => {
970
- deps.onPickProvider(value);
971
- }
972
- });
973
- const modelRow = (0, react.createElement)(SelectRow, {
974
- label: "Model",
975
- hint: deps.chosenProvider ? deps.chosenProvider.baseURL : "",
976
- field: "ocrModel",
977
- current: deps.effectiveModel,
978
- disabled: !deps.chosenProvider || deps.modelOptions.length === 0,
979
- options: modelOptions,
980
- onChange: (field, value) => {
981
- deps.onPickModel(value);
982
- }
983
- });
984
1473
  const applyBtn = (0, react.createElement)("button", {
985
- className: "ocr-btn ocr-btn-primary",
1474
+ className: "dsu-btn dsu-btnPrimary",
986
1475
  type: "button",
987
1476
  disabled: deps.busy !== null,
988
1477
  onClick: deps.onApply,
989
1478
  "data-field": "apply"
990
1479
  }, deps.busy === "apply" ? t("applying") : t("applyToOcr"));
991
1480
  const testBtn = (0, react.createElement)("button", {
992
- className: "ocr-btn",
1481
+ className: "dsu-btn",
993
1482
  type: "button",
994
1483
  disabled: deps.busy !== null,
995
1484
  onClick: deps.onTest,
996
1485
  "data-field": "test"
997
1486
  }, t("testConnection"));
998
1487
  const migrateBtn = (0, react.createElement)("button", {
999
- className: "ocr-btn",
1488
+ className: "dsu-btn",
1000
1489
  type: "button",
1001
1490
  disabled: deps.busy !== null,
1002
1491
  onClick: deps.onMigrate,
1003
1492
  "data-field": "migrate"
1004
1493
  }, t("migrateKeys"));
1005
- const actionRow = (0, react.createElement)("div", {
1006
- className: "ocr-row",
1007
- style: {
1008
- justifyContent: "flex-start",
1009
- gap: 8
1010
- }
1011
- }, applyBtn, testBtn, migrateBtn);
1494
+ return (0, react.createElement)(g, {
1495
+ label: t("actionsLabel"),
1496
+ wide: true
1497
+ }, (0, react.createElement)("div", { className: ACTIONS_CLASS }, applyBtn, testBtn, migrateBtn));
1498
+ }
1499
+ /** 当前配置 / key / 装载期自检三行提示(自检失败只作提示,不挡表单)。 */
1500
+ function buildProviderInfo(deps, data) {
1501
+ const { t } = deps;
1012
1502
  const cur = data.current;
1013
1503
  const currentHint = cur.provider === "" ? t("noProvider") : currentConfigText(t, cur);
1014
- const currentInfo = (0, react.createElement)("div", { className: "ocr-hint" }, currentHint);
1015
- const keyInfo = (0, react.createElement)("div", { className: "ocr-hint" }, deps.keyHint);
1504
+ const currentInfo = (0, react.createElement)("p", { className: "dsu-hint" }, currentHint);
1505
+ const keyInfo = (0, react.createElement)("p", { className: "dsu-hint" }, deps.keyHint);
1016
1506
  const verify = data.endpointVerify ?? null;
1017
- const verifyWarning = verify !== null && !verify.ok ? (0, react.createElement)("div", {
1018
- className: STATUS_ERROR_CLASS,
1019
- "data-field": "endpoint-verify"
1020
- }, t("endpointVerifyFailed", { reason: verify.message })) : null;
1021
- return (0, react.createElement)("div", {}, verifyWarning, providerRow, modelRow, actionRow, currentInfo, keyInfo);
1507
+ return [
1508
+ verify !== null && !verify.ok ? (0, react.createElement)("p", {
1509
+ className: "dsu-error",
1510
+ "data-field": "endpoint-verify"
1511
+ }, t("endpointVerifyFailed", { reason: verify.message })) : null,
1512
+ currentInfo,
1513
+ keyInfo
1514
+ ];
1515
+ }
1516
+ /** 数据齐(source.status === "ready")那一支:模型四个字段 + 动作行 + 三行提示。 */
1517
+ function buildReadyProviderSection(deps, data) {
1518
+ const { t } = deps;
1519
+ const providerOptions = data.providers.map((provider) => ({
1520
+ value: provider.name,
1521
+ label: providerOptionLabel(t, provider),
1522
+ disabled: !provider.hasKey
1523
+ }));
1524
+ const fallbackModelName = deps.effectiveModel === "" ? t("noModel") : deps.effectiveModel;
1525
+ const modelOptions = (deps.modelOptions.length > 0 ? deps.modelOptions : [{
1526
+ id: deps.effectiveModel,
1527
+ name: fallbackModelName
1528
+ }]).map((model) => ({
1529
+ value: model.id,
1530
+ label: model.name
1531
+ }));
1532
+ const effortOptions = [
1533
+ {
1534
+ value: "auto",
1535
+ label: t("effortAuto")
1536
+ },
1537
+ {
1538
+ value: "low",
1539
+ label: t("effortLow")
1540
+ },
1541
+ {
1542
+ value: "medium",
1543
+ label: t("effortMedium")
1544
+ },
1545
+ {
1546
+ value: "high",
1547
+ label: t("effortHigh")
1548
+ }
1549
+ ];
1550
+ const languageOptions = [{
1551
+ value: "中文",
1552
+ label: "中文"
1553
+ }, {
1554
+ value: "English",
1555
+ label: "English"
1556
+ }];
1557
+ const modelUrl = deps.chosenProvider?.baseURL ?? "";
1558
+ return [
1559
+ (0, react.createElement)(x, {
1560
+ label: "Provider",
1561
+ ...l(t("providerHint")),
1562
+ field: "ocrProvider",
1563
+ value: deps.effectiveProvider,
1564
+ options: providerOptions,
1565
+ disabled: data.providers.length === 0,
1566
+ onChange: deps.onPickProvider
1567
+ }),
1568
+ (0, react.createElement)(x, {
1569
+ label: "Model",
1570
+ ...modelUrl === "" ? {} : { hint: modelUrl },
1571
+ field: "ocrModel",
1572
+ value: deps.effectiveModel,
1573
+ options: modelOptions,
1574
+ disabled: !deps.chosenProvider || deps.modelOptions.length === 0,
1575
+ onChange: deps.onPickModel
1576
+ }),
1577
+ (0, react.createElement)(x, {
1578
+ label: t("effortLabel"),
1579
+ ...l(t("effortHint")),
1580
+ field: "effort",
1581
+ value: deps.effortValue,
1582
+ options: effortOptions,
1583
+ disabled: !deps.writable,
1584
+ onChange: (next) => {
1585
+ deps.onField("effort", next);
1586
+ }
1587
+ }),
1588
+ (0, react.createElement)(x, {
1589
+ label: t("languageLabel"),
1590
+ ...l(t("languageHint")),
1591
+ field: "language",
1592
+ value: deps.languageValue,
1593
+ options: languageOptions,
1594
+ disabled: !deps.writable,
1595
+ onChange: (next) => {
1596
+ deps.onField("language", next);
1597
+ }
1598
+ }),
1599
+ (0, react.createElement)(_, {
1600
+ label: t("autoVerifyLabel"),
1601
+ ...l(t("autoVerifyHint")),
1602
+ field: "autoVerify",
1603
+ checked: deps.autoVerifyValue,
1604
+ disabled: !deps.writable,
1605
+ onChange: (next) => {
1606
+ deps.onField("autoVerify", next);
1607
+ }
1608
+ }),
1609
+ buildActionField(deps),
1610
+ ...buildProviderInfo(deps, data)
1611
+ ];
1022
1612
  }
1023
1613
  /** Provider / Model 选择与应用区:三条降级各出一行提示(数据未就绪/读取失败/
1024
1614
  * 数据源不可用),只有 source.status === "ready" 才交给 buildReadyProviderSection 画表单。 */
1025
1615
  function buildProviderSection(deps) {
1026
1616
  const { t } = deps;
1617
+ let body;
1027
1618
  if (deps.fetchError !== null) {
1028
1619
  const reason = t("loadProvidersFailed", { error: deps.fetchError });
1029
- return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, reason);
1030
- }
1031
- if (deps.data === null) return (0, react.createElement)("div", { className: "ocr-status ocr-muted" }, t("loadingProviders"));
1032
- if (deps.data.source.status !== "ready") {
1620
+ body = (0, react.createElement)("p", { className: "dsu-error" }, reason);
1621
+ } else if (deps.data === null) body = (0, react.createElement)("p", { className: "dsu-hint" }, t("loadingProviders"));
1622
+ else if (deps.data.source.status === "ready") body = buildReadyProviderSection(deps, deps.data);
1623
+ else {
1033
1624
  const sourceMessage = deps.data.source.message;
1034
- const line = t("readProvidersFailed", { reason: sourceMessage === "" ? t("noReason") : sourceMessage });
1035
- return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, line);
1625
+ const reason = sourceMessage === "" ? t("noReason") : sourceMessage;
1626
+ body = (0, react.createElement)("p", { className: "dsu-error" }, t("readProvidersFailed", { reason }));
1036
1627
  }
1037
- return buildReadyProviderSection(deps, deps.data);
1628
+ return (0, react.createElement)(d, { title: t("modelSection") }, body);
1038
1629
  }
1039
1630
  /** provider 预选:显式选择 > 当前 OCR 配置(仍存在)> 第一个有 key 的 provider。 */
1040
1631
  function resolveBackend(providerList, current, selProvider) {
@@ -1064,111 +1655,126 @@ function resolveSelection(providerList, current, selProvider, selModel) {
1064
1655
  modelOptions: model.modelOptions
1065
1656
  };
1066
1657
  }
1067
- /** 设置行(effort/language/autoVerify + 保存条)。 */
1658
+ /** 审查参数小节:评论条数、运行超时、配置文件路径 + 保存条。 */
1068
1659
  function buildSettingsSection(deps, writable, setField) {
1069
1660
  const { t } = deps;
1070
- const effortRow = (0, react.createElement)(SelectRow, {
1071
- label: t("effortLabel"),
1072
- hint: t("effortHint"),
1073
- field: "effort",
1074
- current: deps.effortValue,
1075
- disabled: !writable,
1076
- options: [
1077
- {
1078
- value: "low",
1079
- label: t("effortLow")
1080
- },
1081
- {
1082
- value: "medium",
1083
- label: t("effortMedium")
1084
- },
1085
- {
1086
- value: "high",
1087
- label: t("effortHigh")
1088
- }
1089
- ],
1090
- onChange: (field, draft) => {
1091
- setField(field, draft);
1092
- }
1093
- });
1094
- const languageRow = (0, react.createElement)(SelectRow, {
1095
- label: t("languageLabel"),
1096
- hint: t("languageHint"),
1097
- field: "language",
1098
- current: deps.languageValue,
1099
- disabled: !writable,
1100
- options: [{
1101
- value: "中文",
1102
- label: "中文"
1103
- }, {
1104
- value: "English",
1105
- label: "English"
1106
- }],
1107
- onChange: (field, draft) => {
1108
- setField(field, draft);
1109
- }
1110
- });
1111
- const verifyRow = (0, react.createElement)(SelectRow, {
1112
- label: t("autoVerifyLabel"),
1113
- hint: t("autoVerifyHint"),
1114
- field: "autoVerify",
1115
- current: deps.autoVerifyValue === true ? "on" : "off",
1116
- disabled: !writable,
1117
- options: [{
1118
- value: "on",
1119
- label: t("onLabel")
1120
- }, {
1121
- value: "off",
1122
- label: t("offLabel")
1123
- }],
1124
- onChange: (field, draft) => {
1125
- setField(field, draft === "on");
1126
- }
1127
- });
1128
- const maxCommentsRow = (0, react.createElement)(NumberRow, {
1661
+ const maxCommentsRow = (0, react.createElement)(v, {
1129
1662
  label: t("maxCommentsLabel"),
1130
- hint: t("maxCommentsHint"),
1663
+ ...l(t("maxCommentsHint")),
1131
1664
  field: "maxComments",
1132
1665
  value: deps.maxCommentsValue,
1133
- min: 0,
1666
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1667
+ step: 1,
1668
+ invalidHint: t("outOfRange"),
1134
1669
  disabled: !writable,
1135
- onCommit: (num) => {
1136
- setField("maxComments", num);
1670
+ onChange: (next) => {
1671
+ setField("maxComments", next);
1137
1672
  }
1138
1673
  });
1139
- const timeoutRow = (0, react.createElement)(NumberRow, {
1674
+ const timeoutRow = (0, react.createElement)(v, {
1140
1675
  label: t("timeoutLabel"),
1141
- hint: t("timeoutHint"),
1676
+ ...l(t("timeoutHint")),
1142
1677
  field: "timeoutMinutes",
1143
1678
  value: deps.timeoutMinutesValue,
1144
- min: 0,
1679
+ ...COUNT_SCHEMA.minValue === null ? {} : { min: COUNT_SCHEMA.minValue },
1680
+ step: 1,
1681
+ invalidHint: t("outOfRange"),
1145
1682
  disabled: !writable,
1146
- onCommit: (num) => {
1147
- setField("timeoutMinutes", num);
1683
+ onChange: (next) => {
1684
+ setField("timeoutMinutes", next);
1148
1685
  }
1149
1686
  });
1150
- const configPathRow = (0, react.createElement)(TextRow, {
1687
+ const configPathRow = (0, react.createElement)(y, {
1151
1688
  label: t("configPathLabel"),
1152
- hint: t("configPathHint"),
1689
+ ...l(t("configPathHint")),
1153
1690
  field: "ocrConfigPath",
1154
1691
  value: deps.ocrConfigPathValue,
1155
1692
  placeholder: "~/.opencodereview/config.json",
1693
+ monospace: true,
1694
+ wide: true,
1156
1695
  disabled: !writable,
1157
- onCommit: (text) => {
1158
- setField("ocrConfigPath", text);
1696
+ onChange: (next) => {
1697
+ setField("ocrConfigPath", next.trim());
1159
1698
  }
1160
1699
  });
1161
- const saveRow = (0, react.createElement)(SaveBar, {
1162
- t,
1700
+ const saveRow = (0, react.createElement)(S, {
1163
1701
  dirty: deps.dirty,
1164
1702
  writable,
1165
1703
  busy: deps.settingsBusy,
1166
1704
  error: deps.saveError,
1705
+ saveLabel: t("save"),
1706
+ savingLabel: t("saving"),
1707
+ revertLabel: t("revert"),
1708
+ dirtyLabel: t("statusDirty"),
1709
+ cleanLabel: t("statusClean"),
1710
+ readOnlyLabel: t("statusReadOnly"),
1167
1711
  onSave: deps.onSave,
1168
1712
  onDiscard: deps.onDiscard
1169
1713
  });
1170
- const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("div", { className: "ocr-hint" }, deps.statusHint) : null;
1171
- return (0, react.createElement)("div", { style: { padding: "2px 0" } }, statusHintNode, effortRow, languageRow, verifyRow, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1714
+ const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("p", { className: "dsu-hint" }, deps.statusHint) : null;
1715
+ return (0, react.createElement)(d, { title: t("reviewSection") }, statusHintNode, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1716
+ }
1717
+ function buildMcpAdmin(props) {
1718
+ const { t } = props;
1719
+ const textarea = (0, react.createElement)("textarea", {
1720
+ "data-field": "mcpJson",
1721
+ value: props.mcpJson,
1722
+ disabled: !props.writable || props.mcpBusy || !props.available,
1723
+ "aria-label": t("mcpTitle"),
1724
+ className: "dsu-input dsu-textarea dsu-mono ocr-adminTextarea",
1725
+ onChange: (event) => {
1726
+ props.onMcpChange(event.target.value);
1727
+ }
1728
+ });
1729
+ const button = (0, react.createElement)("button", {
1730
+ type: "button",
1731
+ className: "dsu-btn",
1732
+ "data-field": "mcpSave",
1733
+ disabled: !props.writable || props.mcpBusy || !props.available,
1734
+ onClick: props.onMcpSave
1735
+ }, props.mcpBusy ? t("mcpSaving") : t("mcpSave"));
1736
+ const message = props.mcpMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.mcpMsg);
1737
+ return (0, react.createElement)(d, {
1738
+ title: t("mcpTitle"),
1739
+ hint: t("mcpHint")
1740
+ }, props.available ? textarea : (0, react.createElement)("p", { className: "dsu-hint" }, t("adminUnavailable")), (0, react.createElement)("div", { className: ACTIONS_CLASS }, button), message);
1741
+ }
1742
+ function buildAuthAdmin(props) {
1743
+ const { t } = props;
1744
+ const check = (0, react.createElement)("button", {
1745
+ type: "button",
1746
+ className: "dsu-btn",
1747
+ "data-field": "authCheck",
1748
+ disabled: props.authBusy || !props.available,
1749
+ onClick: props.onCheckAuth
1750
+ }, props.authBusy ? t("chatGptChecking") : t("chatGptCheck"));
1751
+ const model = (0, react.createElement)("input", {
1752
+ "data-field": "chatGptModel",
1753
+ value: props.authModel,
1754
+ disabled: !props.writable || props.authBusy || !props.available,
1755
+ "aria-label": t("chatGptModelLabel"),
1756
+ className: "dsu-input",
1757
+ onChange: (event) => {
1758
+ props.onAuthModelChange(event.target.value);
1759
+ }
1760
+ });
1761
+ const use = (0, react.createElement)("button", {
1762
+ type: "button",
1763
+ className: "dsu-btn",
1764
+ "data-field": "chatGptUse",
1765
+ disabled: !props.writable || props.authBusy || !props.available,
1766
+ onClick: props.onUseChatGpt
1767
+ }, props.authBusy ? t("chatGptUsing") : t("chatGptUse"));
1768
+ const output = props.authOutput === null ? null : (0, react.createElement)("pre", { className: "ocr-status dsu-mono" }, props.authOutput);
1769
+ const message = props.authMsg === null ? null : (0, react.createElement)("p", { className: "dsu-hint" }, props.authMsg);
1770
+ return (0, react.createElement)(d, {
1771
+ title: t("chatGptTitle"),
1772
+ hint: t("chatGptHint")
1773
+ }, (0, react.createElement)("div", { className: ACTIONS_CLASS }, check), (0, react.createElement)("p", { className: "dsu-hint" }, `${t("chatGptLoginHint")} ${props.loginCommand}`), output, (0, react.createElement)("p", { className: "dsu-hint" }, t("chatGptModelLabel")), model, (0, react.createElement)("div", { className: ACTIONS_CLASS }, use), message);
1774
+ }
1775
+ /** MCP 配置 + ChatGPT 登录态/切换:都写 OCR 自己的配置,放在设置卡下方。 */
1776
+ function buildAdminSection(props) {
1777
+ return (0, react.createElement)("div", null, buildMcpAdmin(props), buildAuthAdmin(props));
1172
1778
  }
1173
1779
  /**
1174
1780
  * 读设置页载荷(卡片唯一的数据入口):失败只落 fetchError 文本,不抛穿渲染。
@@ -1180,8 +1786,8 @@ async function refreshProviders(reader, t, sinks) {
1180
1786
  try {
1181
1787
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1182
1788
  const json = await reader.providers();
1183
- if (n(json, "ok") === false) {
1184
- const detail = strOf(n(json, "error"));
1789
+ if (i$1(json, "ok") === false) {
1790
+ const detail = strOf(i$1(json, "error"));
1185
1791
  throw new Error(detail === "" ? t("remoteUnavailable") : detail);
1186
1792
  }
1187
1793
  const payload = providersPayloadOf(json);
@@ -1198,8 +1804,8 @@ function readerPost(reader, t) {
1198
1804
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1199
1805
  switch (action) {
1200
1806
  case "select": {
1201
- const provider = strOf(n(body, "provider"));
1202
- const model = strOf(n(body, "model"));
1807
+ const provider = strOf(i$1(body, "provider"));
1808
+ const model = strOf(i$1(body, "model"));
1203
1809
  const reply = await reader.select(provider, model);
1204
1810
  return { json: () => Promise.resolve(reply) };
1205
1811
  }
@@ -1236,12 +1842,12 @@ async function runApplySelection(deps) {
1236
1842
  provider: effectiveProvider,
1237
1843
  model: effectiveModel
1238
1844
  })).json();
1239
- if (n(selJson, "ok") === true) {
1845
+ if (i$1(selJson, "ok") === true) {
1240
1846
  let testText;
1241
- const llmTestRaw = n(selJson, "llmTest");
1242
- if (e$2(llmTestRaw)) {
1243
- const testOutput = strOf(n(llmTestRaw, "output")).slice(0, 400);
1244
- testText = n(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1847
+ const llmTestRaw = i$1(selJson, "llmTest");
1848
+ if (e$3(llmTestRaw)) {
1849
+ const testOutput = strOf(i$1(llmTestRaw, "output")).slice(0, 400);
1850
+ testText = i$1(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1245
1851
  } else testText = t("notVerified");
1246
1852
  const appliedText = t("applied", {
1247
1853
  provider: effectiveProvider,
@@ -1254,7 +1860,7 @@ async function runApplySelection(deps) {
1254
1860
  });
1255
1861
  deps.refresh();
1256
1862
  } else {
1257
- const failure = strOf(n(selJson, "error"));
1863
+ const failure = strOf(i$1(selJson, "error"));
1258
1864
  deps.setActionMsg({
1259
1865
  ok: false,
1260
1866
  text: failure === "" ? t("applyFailed") : failure
@@ -1277,11 +1883,11 @@ async function runMigrateKeys(deps) {
1277
1883
  deps.setActionMsg(null);
1278
1884
  try {
1279
1885
  const json = await (await deps.post("migrate")).json();
1280
- const migratedVal = n(json, "migrated");
1281
- const skippedVal = n(json, "skippedUnknown");
1886
+ const migratedVal = i$1(json, "migrated");
1887
+ const skippedVal = i$1(json, "skippedUnknown");
1282
1888
  const migrated = Array.isArray(migratedVal) ? migratedVal.filter((item) => typeof item === "string") : [];
1283
1889
  const skipped = Array.isArray(skippedVal) ? skippedVal.filter((item) => typeof item === "string") : [];
1284
- const ok = n(json, "ok") === true;
1890
+ const ok = i$1(json, "ok") === true;
1285
1891
  const head = ok ? (migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey")) + (skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "") : t("migrateFailed");
1286
1892
  deps.setActionMsg({
1287
1893
  ok,
@@ -1305,8 +1911,8 @@ async function runTestConnection(deps) {
1305
1911
  deps.setActionMsg(null);
1306
1912
  try {
1307
1913
  const json = await (await deps.post("test")).json();
1308
- const ok = n(json, "ok") === true;
1309
- const testOutput = strOf(n(json, "output"));
1914
+ const ok = i$1(json, "ok") === true;
1915
+ const testOutput = strOf(i$1(json, "output"));
1310
1916
  deps.setActionMsg({
1311
1917
  ok,
1312
1918
  text: (ok ? t("llmTestPassed") : t("llmTestFailed")) + testOutput
@@ -1333,12 +1939,12 @@ async function runProbe(deps) {
1333
1939
  deps.setProbeMsg(null);
1334
1940
  try {
1335
1941
  const json = await (await deps.post("probe")).json();
1336
- const ok = n(json, "ok") === true;
1337
- const head = ok ? t("probeVersion", { version: strOf(n(json, "version")) }) : t("probeReason", { reason: strOf(n(json, "reason")) });
1338
- const output = strOf(n(json, "output"));
1942
+ const ok = i$1(json, "ok") === true;
1943
+ const head = ok ? t("probeVersion", { version: strOf(i$1(json, "version")) }) : t("probeReason", { reason: strOf(i$1(json, "reason")) });
1944
+ const output = strOf(i$1(json, "output"));
1339
1945
  const lines = [
1340
1946
  head,
1341
- strOf(n(json, "changeText")),
1947
+ strOf(i$1(json, "changeText")),
1342
1948
  output
1343
1949
  ].filter((line) => line !== "");
1344
1950
  deps.setProbeMsg({
@@ -1361,7 +1967,7 @@ async function runProbe(deps) {
1361
1967
  * 传输失败仍以 rejection 表达。 */
1362
1968
  async function saveSettingsDraft(deps) {
1363
1969
  const { t, value, touched } = deps;
1364
- const keys = e(touched, value);
1970
+ const keys = t$1(touched, value);
1365
1971
  if (keys.length === 0) return;
1366
1972
  deps.setSettingsBusy(true);
1367
1973
  deps.setSaveError(null);
@@ -1369,28 +1975,37 @@ async function saveSettingsDraft(deps) {
1369
1975
  const draft = touched[field];
1370
1976
  return deps.set(field, draft === void 0 ? null : draft);
1371
1977
  });
1372
- try {
1373
- const settled = await Promise.all(ops);
1374
- deps.setSettingsBusy(false);
1375
- const refused = keys.filter((_field, index) => settled[index] !== true);
1376
- if (refused.length > 0) {
1377
- deps.setSaveError(t("saveRefused", { fields: refused.join(", ") }));
1378
- return;
1978
+ const settled = await Promise.allSettled(ops);
1979
+ deps.setSettingsBusy(false);
1980
+ const refused = [];
1981
+ const failures = [];
1982
+ for (const [index, field] of keys.entries()) {
1983
+ const outcome = settled[index];
1984
+ if (outcome !== void 0) {
1985
+ if (outcome.status === "rejected") {
1986
+ failures.push(outcome.reason);
1987
+ refused.push(field);
1988
+ } else if (!outcome.value) refused.push(field);
1379
1989
  }
1380
- deps.setTouched({});
1381
- } catch (error) {
1382
- deps.setSettingsBusy(false);
1383
- deps.setSaveError(`${t("saveFailed")}${errorSummary(error)}`);
1384
1990
  }
1991
+ if (refused.length > 0) {
1992
+ const kept = {};
1993
+ for (const field of refused) kept[field] = touched[field];
1994
+ deps.setTouched(kept);
1995
+ const reason = failures.length > 0 ? `(${errorSummary(failures[0])})` : "";
1996
+ deps.setSaveError(`${t("saveRefused", { fields: refused.join(", ") })}${reason}`);
1997
+ return;
1998
+ }
1999
+ deps.setTouched({});
1385
2000
  }
1386
2001
  function draftFieldsOf(touched, value) {
1387
2002
  const eff = (field, def) => field in touched ? touched[field] : value[field] ?? def;
1388
2003
  return {
1389
- effortValue: eff("effort", "medium"),
1390
- languageValue: eff("language", "中文"),
1391
- autoVerifyValue: eff("autoVerify", true),
1392
- maxCommentsValue: numOf(eff("maxComments", 12), 12),
1393
- timeoutMinutesValue: numOf(eff("timeoutMinutes", 0), 0),
2004
+ effortValue: textOf(eff("effort", "auto"), EFFORT_SCHEMA, "auto"),
2005
+ languageValue: textOf(eff("language", "中文"), LANGUAGE_SCHEMA, "中文"),
2006
+ autoVerifyValue: booleanOf(eff("autoVerify", true), true),
2007
+ maxCommentsValue: numOf(eff("maxComments", 12), 12, COUNT_SCHEMA),
2008
+ timeoutMinutesValue: numOf(eff("timeoutMinutes", 0), 0, COUNT_SCHEMA),
1394
2009
  ocrConfigPathValue: strOf(eff("ocrConfigPath", ""))
1395
2010
  };
1396
2011
  }
@@ -1409,9 +2024,109 @@ function cardHints(t, ready, status, data) {
1409
2024
  /** Provider 区 = provider/model 表单 + 最近一次动作的回执条。 */
1410
2025
  function buildProviderPanel(deps) {
1411
2026
  const { actionMsg } = deps;
1412
- const providerSection = buildProviderSection(deps);
1413
- const actionMsgNode = actionMsg ? (0, react.createElement)("div", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null;
1414
- return (0, react.createElement)("div", { style: { padding: "2px 0" } }, providerSection, actionMsgNode);
2027
+ return [buildProviderSection(deps), actionMsg ? (0, react.createElement)("p", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null];
2028
+ }
2029
+ /** 管理区的状态与动作(MCP 配置 + ChatGPT 登录态/切换);OcrCard 只负责摆位置。 */
2030
+ function useAdminPanel(reader, t) {
2031
+ const [mcpJson, setMcpJson] = (0, react.useState)("{}");
2032
+ const [mcpBusy, setMcpBusy] = (0, react.useState)(false);
2033
+ const [mcpMsg, setMcpMsg] = (0, react.useState)(null);
2034
+ const [authOutput, setAuthOutput] = (0, react.useState)(null);
2035
+ const [authBusy, setAuthBusy] = (0, react.useState)(false);
2036
+ const [authMsg, setAuthMsg] = (0, react.useState)(null);
2037
+ const [authModel, setAuthModel] = (0, react.useState)("");
2038
+ const [loginCommand, setLoginCommand] = (0, react.useState)("ocr auth login --provider chatgpt");
2039
+ const loadMcp = (0, react.useCallback)(async () => {
2040
+ if (reader === void 0) {
2041
+ setMcpMsg(t("adminUnavailable"));
2042
+ return;
2043
+ }
2044
+ try {
2045
+ const reply = await reader.mcp();
2046
+ if ("status" in reply) setMcpMsg(reply.error);
2047
+ else setMcpJson(reply.json);
2048
+ } catch (error) {
2049
+ setMcpMsg(`${t("mcpLoadFailed")}${String(error)}`);
2050
+ }
2051
+ }, [reader, t]);
2052
+ (0, react.useEffect)(() => {
2053
+ loadMcp();
2054
+ }, [loadMcp]);
2055
+ async function saveMcp() {
2056
+ if (reader === void 0) {
2057
+ setMcpMsg(t("adminUnavailable"));
2058
+ return;
2059
+ }
2060
+ setMcpBusy(true);
2061
+ setMcpMsg(null);
2062
+ try {
2063
+ const reply = await reader.saveMcp(mcpJson);
2064
+ setMcpMsg("status" in reply ? reply.error : t("mcpSaved"));
2065
+ } catch (error) {
2066
+ setMcpMsg(String(error));
2067
+ } finally {
2068
+ setMcpBusy(false);
2069
+ }
2070
+ }
2071
+ async function checkAuth() {
2072
+ if (reader === void 0) {
2073
+ setAuthMsg(t("adminUnavailable"));
2074
+ return;
2075
+ }
2076
+ setAuthBusy(true);
2077
+ setAuthMsg(null);
2078
+ try {
2079
+ const reply = await reader.auth();
2080
+ if ("status" in reply) setAuthMsg(reply.error);
2081
+ else {
2082
+ setAuthOutput(reply.output);
2083
+ setLoginCommand(reply.loginCommand);
2084
+ setAuthMsg(reply.ok ? null : t("requestFailed"));
2085
+ }
2086
+ } catch (error) {
2087
+ setAuthMsg(String(error));
2088
+ } finally {
2089
+ setAuthBusy(false);
2090
+ }
2091
+ }
2092
+ async function applyChatGpt() {
2093
+ if (reader === void 0) {
2094
+ setAuthMsg(t("adminUnavailable"));
2095
+ return;
2096
+ }
2097
+ setAuthBusy(true);
2098
+ setAuthMsg(null);
2099
+ try {
2100
+ const reply = await reader.chatGpt(authModel);
2101
+ setAuthMsg("status" in reply ? reply.error : t("chatGptSaved"));
2102
+ } catch (error) {
2103
+ setAuthMsg(String(error));
2104
+ } finally {
2105
+ setAuthBusy(false);
2106
+ }
2107
+ }
2108
+ return {
2109
+ available: reader !== void 0,
2110
+ mcpJson,
2111
+ mcpBusy,
2112
+ mcpMsg,
2113
+ authOutput,
2114
+ authBusy,
2115
+ authMsg,
2116
+ authModel,
2117
+ loginCommand,
2118
+ onMcpChange: setMcpJson,
2119
+ onMcpSave: () => {
2120
+ saveMcp();
2121
+ },
2122
+ onAuthModelChange: setAuthModel,
2123
+ onCheckAuth: () => {
2124
+ checkAuth();
2125
+ },
2126
+ onUseChatGpt: () => {
2127
+ applyChatGpt();
2128
+ }
2129
+ };
1415
2130
  }
1416
2131
  function OcrCard(props) {
1417
2132
  const { t } = props;
@@ -1419,6 +2134,7 @@ function OcrCard(props) {
1419
2134
  const { value } = snap;
1420
2135
  const ready = snap.status === "ready";
1421
2136
  const writable = snap.writable && ready;
2137
+ const adminState = useAdminPanel(props.reader, t);
1422
2138
  const [data, setData] = (0, react.useState)(null);
1423
2139
  const [fetchError, setFetchError] = (0, react.useState)(null);
1424
2140
  const [busy, setBusy] = (0, react.useState)(null);
@@ -1453,7 +2169,7 @@ function OcrCard(props) {
1453
2169
  const [touched, setTouched] = (0, react.useState)({});
1454
2170
  const [settingsBusy, setSettingsBusy] = (0, react.useState)(false);
1455
2171
  const [saveError, setSaveError] = (0, react.useState)(null);
1456
- const dirty = e(touched, value).length > 0;
2172
+ const dirty = t$1(touched, value).length > 0;
1457
2173
  const setField = (field, draft) => {
1458
2174
  setTouched((prev) => ({
1459
2175
  ...prev,
@@ -1520,12 +2236,12 @@ function OcrCard(props) {
1520
2236
  const providerPanel = buildProviderPanel(deps);
1521
2237
  const cliStatus = buildCliStatusRow(deps);
1522
2238
  const settingsSection = buildSettingsSection(deps, writable, setField);
1523
- const headTitle = t("cardTitle");
1524
- const headDesc = t("cardDescription");
1525
- return (0, react.createElement)(OcrPluginCard, {
1526
- title: headTitle,
1527
- description: headDesc
1528
- }, cliStatus, providerPanel, settingsSection);
2239
+ const adminSection = buildAdminSection({
2240
+ ...adminState,
2241
+ t,
2242
+ writable
2243
+ });
2244
+ return (0, react.createElement)(u, null, cliStatus, providerPanel, settingsSection, adminSection);
1529
2245
  }
1530
2246
  const inject = [
1531
2247
  "slots",
@@ -1533,11 +2249,11 @@ const inject = [
1533
2249
  "locale"
1534
2250
  ];
1535
2251
  function apply(ctx) {
1536
- if (!e$1(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
2252
+ if (!n$1(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
1537
2253
  ctx.effect(() => {
1538
2254
  const tag = document.createElement("style");
1539
2255
  tag.id = "ocr-review-card-css";
1540
- tag.textContent = CARD_CSS;
2256
+ tag.textContent = `${CARD_CSS}\n${c}`;
1541
2257
  document.head.append(tag);
1542
2258
  return () => {
1543
2259
  tag.remove();
@@ -1556,7 +2272,7 @@ function apply(ctx) {
1556
2272
  t,
1557
2273
  hooks: { card: store },
1558
2274
  set: (field, value) => scope.set(field, value),
1559
- ...reader === void 0 ? {} : { reader }
2275
+ reader
1560
2276
  })
1561
2277
  }, OcrCard);
1562
2278
  });
@@ -1584,15 +2300,14 @@ function apply(ctx) {
1584
2300
  inject: () => cliShare
1585
2301
  }, OcrCliActivation));
1586
2302
  }
1587
- //#endregion
1588
2303
  exports.OcrCard = OcrCard;
1589
2304
  exports.OcrCliActivation = OcrCliActivation;
1590
2305
  exports.OcrCliBadge = OcrCliBadge;
1591
2306
  exports.OcrCliSection = OcrCliSection;
1592
- exports.SaveBar = SaveBar;
1593
2307
  exports.apply = apply;
1594
2308
  exports.binaryOf = binaryOf;
1595
2309
  exports.binaryStatusText = binaryStatusText;
2310
+ exports.buildAdminSection = buildAdminSection;
1596
2311
  exports.buildCliActivation = buildCliActivation;
1597
2312
  exports.buildCliBadge = buildCliBadge;
1598
2313
  exports.buildCliSection = buildCliSection;
@@ -1609,6 +2324,7 @@ exports.isOwnSubject = isOwnSubject;
1609
2324
  exports.loadCliStatus = loadCliStatus;
1610
2325
  exports.providerOptionLabel = providerOptionLabel;
1611
2326
  exports.runProbe = runProbe;
2327
+ exports.saveSettingsDraft = saveSettingsDraft;
1612
2328
  return module.exports;
1613
2329
  },
1614
2330
  });