@jayyuen66/dsh-ocr-review 0.1.6 → 0.1.8

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,7 +7,7 @@ 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
+ // src/ui-messages.ts
11
11
  const UI_MESSAGES = {
12
12
  zh: {
13
13
  cardTitle: "open-code-review 代码审查",
@@ -37,7 +37,7 @@ const UI_MESSAGES = {
37
37
  noProvider: "当前未配置 provider",
38
38
  noReason: "宿主未给出原因(重启 dsh 或重试可恢复)",
39
39
  readProvidersFailed: "未能从 dsh 配置服务读取 provider 列表:{reason}",
40
- endpointVerifyFailed: "装载期端点自检失败(配置仍可修改):{reason}",
40
+ endpointVerifyFailed: "装载期端点自检未通过:{reason}(这条是端点给的回执,不是本插件的故障;配置照样能改,也可点「测试连接」重跑)",
41
41
  applied: "已应用:{provider} / {model}\n{test}",
42
42
  notVerified: "(未自动验证,可点「测试连接」手动验证)",
43
43
  llmTestPassed: "✓ ocr llm test 通过",
@@ -50,9 +50,10 @@ const UI_MESSAGES = {
50
50
  skippedPrefix: "\n跳过(settings 中无对应 apiKeyEnv):",
51
51
  migrateFailed: "迁移失败",
52
52
  effortLabel: "审查深度 (effort)",
53
- effortHint: "low=1 轮快速反馈 / medium=2 轮默认 / high=3 轮深查(越深越贵)",
53
+ effortHint: "auto=跟随 OCR 自身配置 / low=1 轮快速反馈 / medium=2 轮 / high=3 轮深查(越深越贵)",
54
+ effortAuto: "auto — 跟随 OCR 配置",
54
55
  effortLow: "low — 快速",
55
- effortMedium: "medium — 均衡(默认)",
56
+ effortMedium: "medium — 均衡",
56
57
  effortHigh: "high — 深入",
57
58
  languageLabel: "评审语言",
58
59
  languageHint: "OCR 评论输出语言(写入 config.language)",
@@ -61,11 +62,27 @@ const UI_MESSAGES = {
61
62
  onLabel: "开启",
62
63
  offLabel: "关闭",
63
64
  maxCommentsLabel: "摘要评论条数 (maxComments)",
64
- maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认 12)",
65
+ maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认见本行设置项的初值)",
65
66
  timeoutLabel: "运行超时 (timeoutMinutes)",
66
- timeoutHint: "前台墙钟超时(分钟);0 = 宿主上限(shell.maxTimeoutMs,当前 60 分钟);更长的评审用工具 wait=false 后台模式(不设宿主 deadline,2 小时后兜底回收)(默认 0)",
67
+ timeoutHint: "前台墙钟超时(分钟);0 = 请求宿主上限(实际生效值由 shell 执行器自己的 maxTimeoutMs 设置项决定,可在 bash-local / pwsh-local 的设置行里改);更长的评审用工具 wait=false 后台模式(不设宿主 deadline,兜底窗口看设置项 ocrBackgroundMaxMs,默认 0 = 不设窗口)(默认 0)",
67
68
  configPathLabel: "OCR 配置文件 (ocrConfigPath)",
68
69
  configPathHint: "外部 open-code-review CLI 自己的配置位置;留空 = ~/.opencodereview/config.json。自定义时必须是 <X>/.opencodereview/config.json 布局(OCR 只认该形状,插件经 HOME 重定向让它生效)",
70
+ mcpTitle: "MCP servers",
71
+ mcpHint: "编辑外部 OCR 的 mcp_servers(JSON)。stdio server 会在评审时执行命令,只在这里由人配置;保存只改这一支,其他配置原样保留。",
72
+ mcpSave: "保存 MCP",
73
+ mcpSaving: "保存中…",
74
+ mcpSaved: "MCP servers 已保存",
75
+ mcpLoadFailed: "读取 MCP 配置失败:",
76
+ chatGptTitle: "ChatGPT(OCR auth)",
77
+ chatGptHint: "登录态由 ocr auth 管理;本卡片只读查询状态,并在登录后把 provider 切到 chatgpt。",
78
+ chatGptCheck: "检查登录态",
79
+ chatGptChecking: "检查中…",
80
+ chatGptModelLabel: "ChatGPT 模型 slug",
81
+ chatGptUse: "使用 ChatGPT",
82
+ chatGptUsing: "应用中…",
83
+ chatGptSaved: "已切换到 ChatGPT provider",
84
+ chatGptLoginHint: "登录请在终端运行:",
85
+ adminUnavailable: "当前部署没有远程面,MCP/ChatGPT 管理区不可用",
69
86
  settingsLoading: "设置加载中,稍候可改",
70
87
  keyDynamicHint: "key 走 get-cred.mjs 动态读取,OCR 配置不留明文",
71
88
  keyScriptMissing: "⚠ get-cred 脚本缺失,api_key_cmd 无法工作",
@@ -121,7 +138,7 @@ const UI_MESSAGES = {
121
138
  noProvider: "No provider configured yet",
122
139
  noReason: "the host gave no reason (restart dsh or retry to recover)",
123
140
  readProvidersFailed: "Could not read the provider list from the dsh settings service: {reason}",
124
- endpointVerifyFailed: "Startup endpoint check failed (the config is still editable): {reason}",
141
+ endpointVerifyFailed: "Startup endpoint check did not pass: {reason} (this is the endpoint's reply, not a fault of this plugin; the config stays editable, and “Test connection” reruns it)",
125
142
  applied: "Applied: {provider} / {model}\n{test}",
126
143
  notVerified: "(not verified automatically — use “Test connection” to check manually)",
127
144
  llmTestPassed: "✓ ocr llm test passed",
@@ -134,9 +151,10 @@ const UI_MESSAGES = {
134
151
  skippedPrefix: "\nSkipped (no apiKeyEnv in settings): ",
135
152
  migrateFailed: "migration failed",
136
153
  effortLabel: "Review depth (effort)",
137
- effortHint: "low = 1 pass quick feedback / medium = 2 passes default / high = 3 passes deep review (deeper costs more)",
154
+ effortHint: "auto = follow the OCR config / low = 1 pass quick feedback / medium = 2 passes / high = 3 passes deep review (deeper costs more)",
155
+ effortAuto: "auto — follow the OCR config",
138
156
  effortLow: "low — quick",
139
- effortMedium: "medium — balanced (default)",
157
+ effortMedium: "medium — balanced",
140
158
  effortHigh: "high — thorough",
141
159
  languageLabel: "Review language",
142
160
  languageHint: "Output language of OCR comments (written into config.language)",
@@ -145,11 +163,27 @@ const UI_MESSAGES = {
145
163
  onLabel: "On",
146
164
  offLabel: "Off",
147
165
  maxCommentsLabel: "Summary comment cap (maxComments)",
148
- maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default 12)",
166
+ maxCommentsHint: "Top comments returned to the model; 0 = all (including unanchored lines, large output) (default: the row's initial value)",
149
167
  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, reclaimed by a 2-hour backstop) (default 0)",
168
+ 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
169
  configPathLabel: "OCR config file (ocrConfigPath)",
152
170
  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)",
171
+ mcpTitle: "MCP servers",
172
+ 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.",
173
+ mcpSave: "Save MCP",
174
+ mcpSaving: "Saving…",
175
+ mcpSaved: "MCP servers saved",
176
+ mcpLoadFailed: "Failed to read the MCP config: ",
177
+ chatGptTitle: "ChatGPT (OCR auth)",
178
+ chatGptHint: "Sign-in is managed by ocr auth; this card only reports the status and switches OCR's provider to chatgpt after you sign in.",
179
+ chatGptCheck: "Check sign-in",
180
+ chatGptChecking: "Checking…",
181
+ chatGptModelLabel: "ChatGPT model slug",
182
+ chatGptUse: "Use ChatGPT",
183
+ chatGptUsing: "Applying…",
184
+ chatGptSaved: "Switched to the ChatGPT provider",
185
+ chatGptLoginHint: "Run the sign-in in a terminal:",
186
+ adminUnavailable: "No remote surface in this deployment; the MCP/ChatGPT admin section is unavailable",
153
187
  settingsLoading: "settings still loading — editable in a moment",
154
188
  keyDynamicHint: "Keys are read dynamically via get-cred.mjs; OCR config keeps no plain text",
155
189
  keyScriptMissing: "⚠ get-cred script missing, api_key_cmd cannot work",
@@ -178,48 +212,77 @@ const UI_MESSAGES = {
178
212
  binaryGuideDismiss: "Got it"
179
213
  }
180
214
  };
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
215
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
183
216
  function e$2(e) {
184
217
  return typeof e == `object` && !!e && !Array.isArray(e);
185
218
  }
186
- function n(t, n) {
187
- return e$2(t) ? t[n] : void 0;
219
+ const t$2 = { absent: !0 };
220
+ function n$1(e, n) {
221
+ try {
222
+ return {
223
+ absent: !1,
224
+ value: e[n]
225
+ };
226
+ } catch {
227
+ return t$2;
228
+ }
188
229
  }
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;
230
+ function i(r, i) {
231
+ let a = e$2(r) ? n$1(r, i) : t$2;
232
+ return a.absent ? void 0 : a.value;
233
+ }
234
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-apply.js
235
+ const e$1 = (e) => e.length > 0 && !(e in Object.prototype);
236
+ function t$1(e, t, n) {
237
+ n === void 0 ? e[t] = void 0 : Object.defineProperty(e, t, n);
238
+ }
239
+ function n(n, r, i) {
240
+ if (!e$1(r)) throw Error(`claimApply 的 flag 不得为空、也不得命中 Object.prototype 的成员(__proto__ 一类会让占位静默失灵或遮蔽全局成员):${r}`);
241
+ let a = globalThis, o = Object.hasOwn(a, r) ? a[r] : void 0;
242
+ if (o === !0 || typeof o == `symbol`) return !1;
243
+ let s = Object.getOwnPropertyDescriptor(a, r), c = Symbol(r);
244
+ a[r] = c;
195
245
  try {
196
- e.effect(() => () => {
197
- r[t] = void 0;
198
- }, n);
246
+ n.effect(() => () => {
247
+ a[r] === c && t$1(a, r, s);
248
+ }, i);
199
249
  } catch (e) {
200
- throw r[t] = void 0, e;
250
+ throw a[r] === c && t$1(a, r, s), e;
201
251
  }
202
252
  return !0;
203
253
  }
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;
254
+ // node_modules/@jayyuen66/dsh-plugin-shared/dist/card-diff.js
255
+ function e(t, n, r) {
256
+ if (t == null || n == null) return (t ?? null) === (n ?? null);
257
+ if (Object.is(t, n)) return !0;
258
+ if (typeof t != `object` || typeof n != `object`) return !1;
259
+ if (r.get(t) === n) return !0;
260
+ if (r.set(t, n), Array.isArray(t) || Array.isArray(n)) return !Array.isArray(t) || !Array.isArray(n) || t.length !== n.length ? !1 : t.every((t, i) => e(t, n[i], r));
261
+ let i = Object.keys(t), a = Object.keys(n);
262
+ return i.length === a.length && i.every((i) => Object.hasOwn(n, i) && e(Reflect.get(t, i), Reflect.get(n, i), r));
210
263
  }
211
- //#endregion
212
- //#region lib/remote-shape.ts
264
+ function t(t, n) {
265
+ let r = [];
266
+ for (let i of Object.keys(t)) e(t[i], n[i], /* @__PURE__ */ new WeakMap()) || r.push(i);
267
+ return r;
268
+ }
269
+ // lib/remote-shape.ts
213
270
  /** 宿主服务键 = 客户端线上命名空间(两侧共用的唯一来源)。 */
214
271
  const OCR_REMOTE_NAMESPACE = "ocrReview";
215
- //#endregion
216
- //#region src/remote.ts
272
+ /**
273
+ * 本包 bundle 的 npm 包名。descriptor `id`、`$mount` 的 `package` 位、客户端槽位键与
274
+ * package.json 的 `name` 必须逐字相等(宿主按 bundle 包名派发设置卡槽位,改名漏一处
275
+ * 的症状是卡片静默不渲染而不是报错),因此只有这一个定义处,其余各方引它。
276
+ */
277
+ const OCR_BUNDLE_PACKAGE = "@jayyuen66/dsh-ocr-review";
278
+ // src/remote.ts
217
279
  /** 网关给命名空间注册的**服务键**:`remote.<namespace>`。它在 `$mount` 完成之后才存在,
218
280
  * 所以只能在挂载后按这个键去读,而不是提前 inject。 */
219
281
  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` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
282
+ /** 卡片所在 bundle 的包名(`$mount` 的 `package` 位与槽位键同源;唯一定义在
283
+ * lib/remote-shape.ts,client-entry.ts 的槽位键引的是同一个常量)。 */
284
+ const PACKAGE_NAME = OCR_BUNDLE_PACKAGE;
285
+ /** 把宿主那九个方法挂到客户端远程面上的描述符(`id` 的形状照生成器:`<包>#<命名空间>/<方法>`)。
223
286
  * 导出它只为一件事:`test/remote.test.ts` 拿它与宿主侧 `remoteMethods()` 的标记对质
224
287
  * —— 两侧分叉时卡片不会报错,只会"读不到",所以那条比较必须在门禁里。 */
225
288
  const OCR_DESCRIPTORS = [
@@ -284,31 +347,105 @@ const OCR_DESCRIPTORS = [
284
347
  invocation: { kind: "direct" },
285
348
  parameters: [],
286
349
  result: { mode: "src-json" }
350
+ },
351
+ {
352
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/mcp`,
353
+ service: OCR_REMOTE_NAMESPACE,
354
+ namespace: OCR_REMOTE_NAMESPACE,
355
+ method: "mcp",
356
+ invocation: { kind: "direct" },
357
+ parameters: [],
358
+ result: { mode: "src-json" }
359
+ },
360
+ {
361
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/saveMcp`,
362
+ service: OCR_REMOTE_NAMESPACE,
363
+ namespace: OCR_REMOTE_NAMESPACE,
364
+ method: "saveMcp",
365
+ invocation: { kind: "direct" },
366
+ parameters: [{
367
+ name: "request",
368
+ wire: "request",
369
+ source: "json",
370
+ codec: {
371
+ mode: "strict",
372
+ typeSymbol: `${PACKAGE_NAME}#mcp-save-request`,
373
+ create: () => ({ parse(value) {
374
+ if (!e$2(value) || typeof value["json"] !== "string") throw new TypeError("mcp save request requires a string json");
375
+ return { json: value["json"] };
376
+ } })
377
+ }
378
+ }],
379
+ result: { mode: "src-json" }
380
+ },
381
+ {
382
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/auth`,
383
+ service: OCR_REMOTE_NAMESPACE,
384
+ namespace: OCR_REMOTE_NAMESPACE,
385
+ method: "auth",
386
+ invocation: { kind: "direct" },
387
+ parameters: [],
388
+ result: { mode: "src-json" }
389
+ },
390
+ {
391
+ id: `${PACKAGE_NAME}#${OCR_REMOTE_NAMESPACE}/chatGpt`,
392
+ service: OCR_REMOTE_NAMESPACE,
393
+ namespace: OCR_REMOTE_NAMESPACE,
394
+ method: "chatGpt",
395
+ invocation: { kind: "direct" },
396
+ parameters: [{
397
+ name: "request",
398
+ wire: "request",
399
+ source: "json",
400
+ codec: {
401
+ mode: "strict",
402
+ typeSymbol: `${PACKAGE_NAME}#chatgpt-request`,
403
+ create: () => ({ parse(value) {
404
+ if (!e$2(value) || typeof value["model"] !== "string") throw new TypeError("chatgpt request requires a string model");
405
+ return { model: value["model"] };
406
+ } })
407
+ }
408
+ }],
409
+ result: { mode: "src-json" }
287
410
  }
288
411
  ];
289
412
  /** 这个值能当远程面用吗(只核我们要调的那一位;服务来自别的实现或版本时保守当作没有)。 */
290
413
  const isRemoteMounter = (value) => e$2(value) && typeof value["$mount"] === "function";
291
414
  /** 同上:命名空间服务(挂载之后才有,缺它说明网关没认这枚贡献)。 */
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";
415
+ const isOcrNamespace = (value) => e$2(value) && typeof value["providers"] === "function" && typeof value["select"] === "function" && typeof value["migrate"] === "function" && typeof value["test"] === "function" && typeof value["probe"] === "function" && typeof value["mcp"] === "function" && typeof value["saveMcp"] === "function" && typeof value["auth"] === "function" && typeof value["chatGpt"] === "function";
293
416
  /** 网关回执 → 值;载体/网关失败抛(卡片那层按「请求失败」显示)。 */
294
417
  function unwrap(result) {
295
418
  if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
296
419
  return result.value;
297
420
  }
298
421
  /**
299
- * 造出真正的读取器(挂载一次 + 每次读命名空间服务)。
422
+ * 造出真正的读取器。`remote` 不再是构造期快照:本插件与官方网关之间没有加载顺序
423
+ * 保证(本文件刻意不把 `remote` 写进 `inject`),网关可能在卡片注册之后才
424
+ * `provide("remote")`,所以**每次读取都现取**——缺席报可读错误,晚到也能被下一次
425
+ * 读取接上。挂载 promise 仍缓存(`$mount` 幂等),但拒绝态不缓存:失败后清空,
426
+ * 下一次读取重新挂载,没有「错一次就永远错」的路径。
300
427
  * @param ctx - 客户端上下文
301
- * @param remote - 已核过形状的远程面
302
428
  * @returns 读取器
303
429
  */
304
- function buildOcrReader(ctx, remote) {
430
+ function buildOcrReader(ctx) {
305
431
  let mounted;
432
+ const remoteOf = () => {
433
+ const remote = ctx.get?.("remote", false);
434
+ if (!isRemoteMounter(remote)) throw new Error("remote 未提供:当前部署没有远程读面(每次读取都会重新解析)");
435
+ return remote;
436
+ };
306
437
  const namespaceOf = async () => {
438
+ const remote = remoteOf();
307
439
  mounted ??= remote.$mount({
308
440
  package: PACKAGE_NAME,
309
441
  descriptors: OCR_DESCRIPTORS
310
442
  });
311
- await mounted;
443
+ try {
444
+ await mounted;
445
+ } catch (error) {
446
+ mounted = void 0;
447
+ throw error;
448
+ }
312
449
  const namespace = ctx.get?.(OCR_SERVICE_KEY, false);
313
450
  if (!isOcrNamespace(namespace)) throw new Error(`${OCR_SERVICE_KEY} 未提供:宿主网关没有挂上这枚命名空间`);
314
451
  return namespace;
@@ -331,25 +468,37 @@ function buildOcrReader(ctx, remote) {
331
468
  },
332
469
  async probe() {
333
470
  return unwrap(await (await namespaceOf()).probe());
471
+ },
472
+ async mcp() {
473
+ return unwrap(await (await namespaceOf()).mcp());
474
+ },
475
+ async saveMcp(json) {
476
+ return unwrap(await (await namespaceOf()).saveMcp({ json }));
477
+ },
478
+ async auth() {
479
+ return unwrap(await (await namespaceOf()).auth());
480
+ },
481
+ async chatGpt(model) {
482
+ return unwrap(await (await namespaceOf()).chatGpt({ model }));
334
483
  }
335
484
  };
336
485
  }
337
486
  /**
338
- * 造一个「读/写宿主侧 OCR 配置」的读取器;**当前部署没有远程面时返回 `undefined`**
339
- * (卡片照常渲染,只是数据面显示读不到 —— 与 host 侧可选读面同一条口径)。
487
+ * 造一个「读/写宿主侧 OCR 配置」的读取器。**当前部署没有远程面时读取会抛**可读错误
488
+ * (卡片那层按「请求失败」显示原因)——remote 不在构造期做快照,网关晚于卡片注册
489
+ * `provide("remote")` 的部署在首次成功读取时就能接上,不存在「apply 期没有就永远
490
+ * 读不到」的路径。
340
491
  *
341
- * `$mount` 是**幂等**的(挂进本 fiber,随卡片卸载自动撤),所以这里缓存那枚 promise:
342
- * 同一次会话里反复拉取只挂一次。
492
+ * `$mount` 是**幂等**的(挂进本 fiber,随卡片卸载自动撤),所以缓存那枚 promise:
493
+ * 同一次会话里反复拉取只挂一次;拒绝态例外(见 buildOcrReader)。
343
494
  *
344
495
  * @param ctx - 客户端上下文(只用 `get` 一位)
345
- * @returns 读取器;没有远程面时 `undefined`
496
+ * @returns 读取器
346
497
  */
347
498
  function createOcrReader(ctx) {
348
- const remote = ctx.get?.("remote");
349
- return isRemoteMounter(remote) ? buildOcrReader(ctx, remote) : void 0;
499
+ return buildOcrReader(ctx);
350
500
  }
351
- //#endregion
352
- //#region src/client-entry.ts
501
+ // src/client-entry.ts
353
502
  const NS = "ocr-review";
354
503
  /**
355
504
  * `plugins.bundle.config` 是 keyed 槽,宿主派发的 entryKey 就是 bundle 的 **npm 包名**:
@@ -365,10 +514,12 @@ const NS = "ocr-review";
365
514
  * (不是回落、不是报错)。值 = 本包 package.json 的 name,且必须出现在 profile 的
366
515
  * `dsh.profile.bundles` 里;test/build-client.test.ts 从
367
516
  * `~/.dsh/profiles/web/package.json` 解析校验,不在此处硬抄一份到测试。
368
- * 设置命名空间**不跟着改**:下面 `ctx.configForms.get(NS)` 仍吃裸条目 id(installed
369
- * dsh-client-ui-settings/lib/client.js:1309-1315 把入参原样当 settings 命名空间用)。
517
+ * 唯一定义在 lib/remote-shape.ts 的 OCR_BUNDLE_PACKAGE(descriptor id / $mount 位
518
+ * 与这里是同一份值),设置命名空间**不跟着改**:下面 `ctx.configForms.get(NS)` 仍吃
519
+ * 裸条目 id(installed dsh-client-ui-settings/lib/client.js:1309-1315 把入参原样当
520
+ * settings 命名空间用)。
370
521
  */
371
- const BUNDLE_PKG = "@jayyuen66/dsh-ocr-review";
522
+ const BUNDLE_PKG = OCR_BUNDLE_PACKAGE;
372
523
  /** 未知值 → 安全字符串(非字符串给 '')。 */
373
524
  function strOf(value) {
374
525
  return typeof value === "string" ? value : "";
@@ -383,21 +534,21 @@ function errorSummary(error) {
383
534
  }
384
535
  function modelOf(raw) {
385
536
  return {
386
- id: strOf(n(raw, "id")),
387
- name: strOf(n(raw, "name"))
537
+ id: strOf(i(raw, "id")),
538
+ name: strOf(i(raw, "name"))
388
539
  };
389
540
  }
390
541
  function providerOf(raw) {
391
- const modelsArr = n(raw, "models");
542
+ const modelsArr = i(raw, "models");
392
543
  const models = Array.isArray(modelsArr) ? modelsArr.map((item) => modelOf(item)) : [];
393
- const apiKeyEnvRaw = n(raw, "apiKeyEnv");
544
+ const apiKeyEnvRaw = i(raw, "apiKeyEnv");
394
545
  return {
395
- name: strOf(n(raw, "name")),
396
- displayName: strOf(n(raw, "displayName")),
546
+ name: strOf(i(raw, "name")),
547
+ displayName: strOf(i(raw, "displayName")),
397
548
  apiKeyEnv: typeof apiKeyEnvRaw === "string" ? apiKeyEnvRaw : null,
398
- baseURL: strOf(n(raw, "baseURL")),
549
+ baseURL: strOf(i(raw, "baseURL")),
399
550
  models,
400
- hasKey: n(raw, "hasKey") === true
551
+ hasKey: i(raw, "hasKey") === true
401
552
  };
402
553
  }
403
554
  /** 可得性来源的合法域(与 host 侧 `OcrBinarySource` 同一份取值)。形状不合即「没报」。 */
@@ -440,37 +591,37 @@ function binaryOf(raw) {
440
591
  async function loadCliStatus(reader) {
441
592
  if (reader === void 0) return null;
442
593
  try {
443
- return binaryOf(n(await reader.providers(), "ocrBinary"));
594
+ return binaryOf(i(await reader.providers(), "ocrBinary"));
444
595
  } catch {
445
596
  return null;
446
597
  }
447
598
  }
448
599
  function providersPayloadOf(json) {
449
- const providersArr = n(json, "providers");
600
+ const providersArr = i(json, "providers");
450
601
  const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
451
- const currentRaw = n(json, "current");
602
+ const currentRaw = i(json, "current");
452
603
  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"))
604
+ provider: strOf(i(currentRaw, "provider")),
605
+ model: strOf(i(currentRaw, "model")),
606
+ keyIsDynamic: i(currentRaw, "keyIsDynamic") === true,
607
+ url: strOf(i(currentRaw, "url"))
457
608
  };
458
- const sourceRaw = n(json, "source");
459
- const verifyRaw = n(json, "endpointVerify");
609
+ const sourceRaw = i(json, "source");
610
+ const verifyRaw = i(json, "endpointVerify");
460
611
  const endpointVerify = verifyRaw === null || verifyRaw === void 0 ? null : {
461
- ok: n(verifyRaw, "ok") === true,
462
- message: strOf(n(verifyRaw, "message"))
612
+ ok: i(verifyRaw, "ok") === true,
613
+ message: strOf(i(verifyRaw, "message"))
463
614
  };
464
615
  return {
465
616
  providers,
466
617
  current,
467
- getCredReady: n(json, "getCredReady") === true,
618
+ getCredReady: i(json, "getCredReady") === true,
468
619
  source: {
469
- status: strOf(n(sourceRaw, "status")),
470
- message: strOf(n(sourceRaw, "message"))
620
+ status: strOf(i(sourceRaw, "status")),
621
+ message: strOf(i(sourceRaw, "message"))
471
622
  },
472
623
  endpointVerify,
473
- ocrBinary: binaryOf(n(json, "ocrBinary"))
624
+ ocrBinary: binaryOf(i(json, "ocrBinary"))
474
625
  };
475
626
  }
476
627
  /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
@@ -624,8 +775,10 @@ function NumberRow(props) {
624
775
  (0, react.useEffect)(() => {
625
776
  setDraft(String(value));
626
777
  }, [value]);
627
- const stage = () => {
628
- const parsed = Number(draft);
778
+ /** 用**本次输入的值**上报,不读 `draft`:state 更新是异步的,`onChange` 里
779
+ * `setDraft(x)` 之后立刻读 `draft` 拿到的还是上一个字符,草稿会永远滞后一格。 */
780
+ const stage = (text) => {
781
+ const parsed = Number(text);
629
782
  if (Number.isInteger(parsed) && parsed >= min && (max === void 0 || parsed <= max)) onCommit(parsed);
630
783
  };
631
784
  return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
@@ -641,12 +794,12 @@ function NumberRow(props) {
641
794
  "aria-label": label,
642
795
  onChange: (ev) => {
643
796
  setDraft(ev.target.value);
644
- stage();
797
+ stage(ev.target.value);
645
798
  },
646
799
  onKeyDown: (ev) => {
647
800
  if (ev.key === "Enter") {
648
801
  ev.preventDefault();
649
- stage();
802
+ stage(draft);
650
803
  }
651
804
  },
652
805
  className: FORM_CONTROL_CLASS
@@ -660,8 +813,9 @@ function TextRow(props) {
660
813
  (0, react.useEffect)(() => {
661
814
  setDraft(value);
662
815
  }, [value]);
663
- const stage = () => {
664
- onCommit(draft.trim());
816
+ /** 同 NumberRow:读参数而不是 `draft`,否则草稿永远滞后一格。 */
817
+ const stage = (text) => {
818
+ onCommit(text.trim());
665
819
  };
666
820
  return (0, react.createElement)("div", { className: "ocr-row" }, (0, react.createElement)("div", { style: {
667
821
  minWidth: 0,
@@ -677,12 +831,12 @@ function TextRow(props) {
677
831
  style: { width: "min(60%,320px)" },
678
832
  onChange: (ev) => {
679
833
  setDraft(ev.target.value);
680
- stage();
834
+ stage(ev.target.value);
681
835
  },
682
836
  onKeyDown: (ev) => {
683
837
  if (ev.key === "Enter") {
684
838
  ev.preventDefault();
685
- stage();
839
+ stage(draft);
686
840
  }
687
841
  }
688
842
  }));
@@ -1074,6 +1228,10 @@ function buildSettingsSection(deps, writable, setField) {
1074
1228
  current: deps.effortValue,
1075
1229
  disabled: !writable,
1076
1230
  options: [
1231
+ {
1232
+ value: "auto",
1233
+ label: t("effortAuto")
1234
+ },
1077
1235
  {
1078
1236
  value: "low",
1079
1237
  label: t("effortLow")
@@ -1170,6 +1328,73 @@ function buildSettingsSection(deps, writable, setField) {
1170
1328
  const statusHintNode = deps.statusHint !== null && deps.statusHint !== "" ? (0, react.createElement)("div", { className: "ocr-hint" }, deps.statusHint) : null;
1171
1329
  return (0, react.createElement)("div", { style: { padding: "2px 0" } }, statusHintNode, effortRow, languageRow, verifyRow, maxCommentsRow, timeoutRow, configPathRow, saveRow);
1172
1330
  }
1331
+ const sectionStyle = { padding: "6px 0" };
1332
+ const titleStyle = {
1333
+ fontWeight: "600",
1334
+ fontSize: "13px"
1335
+ };
1336
+ const hintStyle = {
1337
+ fontSize: "12px",
1338
+ opacity: "0.8",
1339
+ margin: "2px 0 6px"
1340
+ };
1341
+ const textareaStyle = {
1342
+ width: "100%",
1343
+ minHeight: "120px",
1344
+ fontFamily: "monospace",
1345
+ fontSize: "12px",
1346
+ boxSizing: "border-box"
1347
+ };
1348
+ function buildMcpAdmin(props) {
1349
+ const { t } = props;
1350
+ const textarea = (0, react.createElement)("textarea", {
1351
+ "data-field": "mcpJson",
1352
+ value: props.mcpJson,
1353
+ disabled: !props.writable || props.mcpBusy || !props.available,
1354
+ onChange: (event) => {
1355
+ props.onMcpChange(event.target.value);
1356
+ },
1357
+ style: textareaStyle
1358
+ });
1359
+ const button = (0, react.createElement)("button", {
1360
+ type: "button",
1361
+ "data-field": "mcpSave",
1362
+ disabled: !props.writable || props.mcpBusy || !props.available,
1363
+ onClick: props.onMcpSave
1364
+ }, props.mcpBusy ? t("mcpSaving") : t("mcpSave"));
1365
+ const message = props.mcpMsg === null ? null : (0, react.createElement)("div", { className: "ocr-hint" }, props.mcpMsg);
1366
+ return (0, react.createElement)("div", { style: sectionStyle }, (0, react.createElement)("div", { style: titleStyle }, t("mcpTitle")), (0, react.createElement)("div", { style: hintStyle }, t("mcpHint")), props.available ? textarea : (0, react.createElement)("div", { className: "ocr-hint" }, t("adminUnavailable")), button, message);
1367
+ }
1368
+ function buildAuthAdmin(props) {
1369
+ const { t } = props;
1370
+ const check = (0, react.createElement)("button", {
1371
+ type: "button",
1372
+ "data-field": "authCheck",
1373
+ disabled: props.authBusy || !props.available,
1374
+ onClick: props.onCheckAuth
1375
+ }, props.authBusy ? t("chatGptChecking") : t("chatGptCheck"));
1376
+ const model = (0, react.createElement)("input", {
1377
+ "data-field": "chatGptModel",
1378
+ value: props.authModel,
1379
+ disabled: !props.writable || props.authBusy || !props.available,
1380
+ onChange: (event) => {
1381
+ props.onAuthModelChange(event.target.value);
1382
+ }
1383
+ });
1384
+ const use = (0, react.createElement)("button", {
1385
+ type: "button",
1386
+ "data-field": "chatGptUse",
1387
+ disabled: !props.writable || props.authBusy || !props.available,
1388
+ onClick: props.onUseChatGpt
1389
+ }, props.authBusy ? t("chatGptUsing") : t("chatGptUse"));
1390
+ const output = props.authOutput === null ? null : (0, react.createElement)("pre", { style: textareaStyle }, props.authOutput);
1391
+ const message = props.authMsg === null ? null : (0, react.createElement)("div", { className: "ocr-hint" }, props.authMsg);
1392
+ return (0, react.createElement)("div", { style: sectionStyle }, (0, react.createElement)("div", { style: titleStyle }, t("chatGptTitle")), (0, react.createElement)("div", { style: hintStyle }, t("chatGptHint")), check, (0, react.createElement)("div", { style: hintStyle }, `${t("chatGptLoginHint")} ${props.loginCommand}`), output, (0, react.createElement)("div", { style: hintStyle }, t("chatGptModelLabel")), model, use, message);
1393
+ }
1394
+ /** MCP 配置 + ChatGPT 登录态/切换:都写 OCR 自己的配置,放在设置卡下方。 */
1395
+ function buildAdminSection(props) {
1396
+ return (0, react.createElement)("div", { style: sectionStyle }, buildMcpAdmin(props), buildAuthAdmin(props));
1397
+ }
1173
1398
  /**
1174
1399
  * 读设置页载荷(卡片唯一的数据入口):失败只落 fetchError 文本,不抛穿渲染。
1175
1400
  * 拒绝回执(`ok:false`)里的 error 就是「为什么读不到」的可读原因;只报一句
@@ -1180,8 +1405,8 @@ async function refreshProviders(reader, t, sinks) {
1180
1405
  try {
1181
1406
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1182
1407
  const json = await reader.providers();
1183
- if (n(json, "ok") === false) {
1184
- const detail = strOf(n(json, "error"));
1408
+ if (i(json, "ok") === false) {
1409
+ const detail = strOf(i(json, "error"));
1185
1410
  throw new Error(detail === "" ? t("remoteUnavailable") : detail);
1186
1411
  }
1187
1412
  const payload = providersPayloadOf(json);
@@ -1198,8 +1423,8 @@ function readerPost(reader, t) {
1198
1423
  if (reader === void 0) throw new Error(t("remoteUnavailable"));
1199
1424
  switch (action) {
1200
1425
  case "select": {
1201
- const provider = strOf(n(body, "provider"));
1202
- const model = strOf(n(body, "model"));
1426
+ const provider = strOf(i(body, "provider"));
1427
+ const model = strOf(i(body, "model"));
1203
1428
  const reply = await reader.select(provider, model);
1204
1429
  return { json: () => Promise.resolve(reply) };
1205
1430
  }
@@ -1236,12 +1461,12 @@ async function runApplySelection(deps) {
1236
1461
  provider: effectiveProvider,
1237
1462
  model: effectiveModel
1238
1463
  })).json();
1239
- if (n(selJson, "ok") === true) {
1464
+ if (i(selJson, "ok") === true) {
1240
1465
  let testText;
1241
- const llmTestRaw = n(selJson, "llmTest");
1466
+ const llmTestRaw = i(selJson, "llmTest");
1242
1467
  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}`;
1468
+ const testOutput = strOf(i(llmTestRaw, "output")).slice(0, 400);
1469
+ testText = i(llmTestRaw, "ok") === true ? t("llmTestPassed") : `${t("llmTestFailed")}${testOutput}`;
1245
1470
  } else testText = t("notVerified");
1246
1471
  const appliedText = t("applied", {
1247
1472
  provider: effectiveProvider,
@@ -1254,7 +1479,7 @@ async function runApplySelection(deps) {
1254
1479
  });
1255
1480
  deps.refresh();
1256
1481
  } else {
1257
- const failure = strOf(n(selJson, "error"));
1482
+ const failure = strOf(i(selJson, "error"));
1258
1483
  deps.setActionMsg({
1259
1484
  ok: false,
1260
1485
  text: failure === "" ? t("applyFailed") : failure
@@ -1277,11 +1502,11 @@ async function runMigrateKeys(deps) {
1277
1502
  deps.setActionMsg(null);
1278
1503
  try {
1279
1504
  const json = await (await deps.post("migrate")).json();
1280
- const migratedVal = n(json, "migrated");
1281
- const skippedVal = n(json, "skippedUnknown");
1505
+ const migratedVal = i(json, "migrated");
1506
+ const skippedVal = i(json, "skippedUnknown");
1282
1507
  const migrated = Array.isArray(migratedVal) ? migratedVal.filter((item) => typeof item === "string") : [];
1283
1508
  const skipped = Array.isArray(skippedVal) ? skippedVal.filter((item) => typeof item === "string") : [];
1284
- const ok = n(json, "ok") === true;
1509
+ const ok = i(json, "ok") === true;
1285
1510
  const head = ok ? (migrated.length > 0 ? t("migratedPrefix") + migrated.join(", ") : t("noPlainKey")) + (skipped.length > 0 ? t("skippedPrefix") + skipped.join(", ") : "") : t("migrateFailed");
1286
1511
  deps.setActionMsg({
1287
1512
  ok,
@@ -1305,8 +1530,8 @@ async function runTestConnection(deps) {
1305
1530
  deps.setActionMsg(null);
1306
1531
  try {
1307
1532
  const json = await (await deps.post("test")).json();
1308
- const ok = n(json, "ok") === true;
1309
- const testOutput = strOf(n(json, "output"));
1533
+ const ok = i(json, "ok") === true;
1534
+ const testOutput = strOf(i(json, "output"));
1310
1535
  deps.setActionMsg({
1311
1536
  ok,
1312
1537
  text: (ok ? t("llmTestPassed") : t("llmTestFailed")) + testOutput
@@ -1333,12 +1558,12 @@ async function runProbe(deps) {
1333
1558
  deps.setProbeMsg(null);
1334
1559
  try {
1335
1560
  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"));
1561
+ const ok = i(json, "ok") === true;
1562
+ const head = ok ? t("probeVersion", { version: strOf(i(json, "version")) }) : t("probeReason", { reason: strOf(i(json, "reason")) });
1563
+ const output = strOf(i(json, "output"));
1339
1564
  const lines = [
1340
1565
  head,
1341
- strOf(n(json, "changeText")),
1566
+ strOf(i(json, "changeText")),
1342
1567
  output
1343
1568
  ].filter((line) => line !== "");
1344
1569
  deps.setProbeMsg({
@@ -1360,8 +1585,8 @@ async function runProbe(deps) {
1360
1585
  * 这时不能清 touched、也不能报成功——否则用户看到「已保存」而配置原封未动。
1361
1586
  * 传输失败仍以 rejection 表达。 */
1362
1587
  async function saveSettingsDraft(deps) {
1363
- const { t, value, touched } = deps;
1364
- const keys = e(touched, value);
1588
+ const { t: t$3, value, touched } = deps;
1589
+ const keys = t(touched, value);
1365
1590
  if (keys.length === 0) return;
1366
1591
  deps.setSettingsBusy(true);
1367
1592
  deps.setSaveError(null);
@@ -1369,24 +1594,33 @@ async function saveSettingsDraft(deps) {
1369
1594
  const draft = touched[field];
1370
1595
  return deps.set(field, draft === void 0 ? null : draft);
1371
1596
  });
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;
1597
+ const settled = await Promise.allSettled(ops);
1598
+ deps.setSettingsBusy(false);
1599
+ const refused = [];
1600
+ const failures = [];
1601
+ for (const [index, field] of keys.entries()) {
1602
+ const outcome = settled[index];
1603
+ if (outcome !== void 0) {
1604
+ if (outcome.status === "rejected") {
1605
+ failures.push(outcome.reason);
1606
+ refused.push(field);
1607
+ } else if (!outcome.value) refused.push(field);
1379
1608
  }
1380
- deps.setTouched({});
1381
- } catch (error) {
1382
- deps.setSettingsBusy(false);
1383
- deps.setSaveError(`${t("saveFailed")}${errorSummary(error)}`);
1384
1609
  }
1610
+ if (refused.length > 0) {
1611
+ const kept = {};
1612
+ for (const field of refused) kept[field] = touched[field];
1613
+ deps.setTouched(kept);
1614
+ const reason = failures.length > 0 ? `(${errorSummary(failures[0])})` : "";
1615
+ deps.setSaveError(`${t$3("saveRefused", { fields: refused.join(", ") })}${reason}`);
1616
+ return;
1617
+ }
1618
+ deps.setTouched({});
1385
1619
  }
1386
1620
  function draftFieldsOf(touched, value) {
1387
1621
  const eff = (field, def) => field in touched ? touched[field] : value[field] ?? def;
1388
1622
  return {
1389
- effortValue: eff("effort", "medium"),
1623
+ effortValue: eff("effort", "auto"),
1390
1624
  languageValue: eff("language", "中文"),
1391
1625
  autoVerifyValue: eff("autoVerify", true),
1392
1626
  maxCommentsValue: numOf(eff("maxComments", 12), 12),
@@ -1413,25 +1647,128 @@ function buildProviderPanel(deps) {
1413
1647
  const actionMsgNode = actionMsg ? (0, react.createElement)("div", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null;
1414
1648
  return (0, react.createElement)("div", { style: { padding: "2px 0" } }, providerSection, actionMsgNode);
1415
1649
  }
1650
+ /** 管理区的状态与动作(MCP 配置 + ChatGPT 登录态/切换);OcrCard 只负责摆位置。 */
1651
+ function useAdminPanel(reader, t) {
1652
+ const [mcpJson, setMcpJson] = (0, react.useState)("{}");
1653
+ const [mcpBusy, setMcpBusy] = (0, react.useState)(false);
1654
+ const [mcpMsg, setMcpMsg] = (0, react.useState)(null);
1655
+ const [authOutput, setAuthOutput] = (0, react.useState)(null);
1656
+ const [authBusy, setAuthBusy] = (0, react.useState)(false);
1657
+ const [authMsg, setAuthMsg] = (0, react.useState)(null);
1658
+ const [authModel, setAuthModel] = (0, react.useState)("");
1659
+ const [loginCommand, setLoginCommand] = (0, react.useState)("ocr auth login --provider chatgpt");
1660
+ const loadMcp = (0, react.useCallback)(async () => {
1661
+ if (reader === void 0) {
1662
+ setMcpMsg(t("adminUnavailable"));
1663
+ return;
1664
+ }
1665
+ try {
1666
+ const reply = await reader.mcp();
1667
+ if ("status" in reply) setMcpMsg(reply.error);
1668
+ else setMcpJson(reply.json);
1669
+ } catch (error) {
1670
+ setMcpMsg(`${t("mcpLoadFailed")}${String(error)}`);
1671
+ }
1672
+ }, [reader, t]);
1673
+ (0, react.useEffect)(() => {
1674
+ loadMcp();
1675
+ }, [loadMcp]);
1676
+ async function saveMcp() {
1677
+ if (reader === void 0) {
1678
+ setMcpMsg(t("adminUnavailable"));
1679
+ return;
1680
+ }
1681
+ setMcpBusy(true);
1682
+ setMcpMsg(null);
1683
+ try {
1684
+ const reply = await reader.saveMcp(mcpJson);
1685
+ setMcpMsg("status" in reply ? reply.error : t("mcpSaved"));
1686
+ } catch (error) {
1687
+ setMcpMsg(String(error));
1688
+ } finally {
1689
+ setMcpBusy(false);
1690
+ }
1691
+ }
1692
+ async function checkAuth() {
1693
+ if (reader === void 0) {
1694
+ setAuthMsg(t("adminUnavailable"));
1695
+ return;
1696
+ }
1697
+ setAuthBusy(true);
1698
+ setAuthMsg(null);
1699
+ try {
1700
+ const reply = await reader.auth();
1701
+ if ("status" in reply) setAuthMsg(reply.error);
1702
+ else {
1703
+ setAuthOutput(reply.output);
1704
+ setLoginCommand(reply.loginCommand);
1705
+ setAuthMsg(reply.ok ? null : t("requestFailed"));
1706
+ }
1707
+ } catch (error) {
1708
+ setAuthMsg(String(error));
1709
+ } finally {
1710
+ setAuthBusy(false);
1711
+ }
1712
+ }
1713
+ async function applyChatGpt() {
1714
+ if (reader === void 0) {
1715
+ setAuthMsg(t("adminUnavailable"));
1716
+ return;
1717
+ }
1718
+ setAuthBusy(true);
1719
+ setAuthMsg(null);
1720
+ try {
1721
+ const reply = await reader.chatGpt(authModel);
1722
+ setAuthMsg("status" in reply ? reply.error : t("chatGptSaved"));
1723
+ } catch (error) {
1724
+ setAuthMsg(String(error));
1725
+ } finally {
1726
+ setAuthBusy(false);
1727
+ }
1728
+ }
1729
+ return {
1730
+ available: reader !== void 0,
1731
+ mcpJson,
1732
+ mcpBusy,
1733
+ mcpMsg,
1734
+ authOutput,
1735
+ authBusy,
1736
+ authMsg,
1737
+ authModel,
1738
+ loginCommand,
1739
+ onMcpChange: setMcpJson,
1740
+ onMcpSave: () => {
1741
+ saveMcp();
1742
+ },
1743
+ onAuthModelChange: setAuthModel,
1744
+ onCheckAuth: () => {
1745
+ checkAuth();
1746
+ },
1747
+ onUseChatGpt: () => {
1748
+ applyChatGpt();
1749
+ }
1750
+ };
1751
+ }
1416
1752
  function OcrCard(props) {
1417
- const { t } = props;
1753
+ const { t: t$4 } = props;
1418
1754
  const snap = props.useCard((state) => state);
1419
1755
  const { value } = snap;
1420
1756
  const ready = snap.status === "ready";
1421
1757
  const writable = snap.writable && ready;
1758
+ const adminState = useAdminPanel(props.reader, t$4);
1422
1759
  const [data, setData] = (0, react.useState)(null);
1423
1760
  const [fetchError, setFetchError] = (0, react.useState)(null);
1424
1761
  const [busy, setBusy] = (0, react.useState)(null);
1425
1762
  const [actionMsg, setActionMsg] = (0, react.useState)(null);
1426
1763
  const [probeMsg, setProbeMsg] = (0, react.useState)(null);
1427
- const refresh = (0, react.useCallback)(async () => refreshProviders(props.reader, t, {
1764
+ const refresh = (0, react.useCallback)(async () => refreshProviders(props.reader, t$4, {
1428
1765
  setData,
1429
1766
  setFetchError
1430
- }), [props.reader, t]);
1767
+ }), [props.reader, t$4]);
1431
1768
  (0, react.useEffect)(() => {
1432
1769
  refresh();
1433
1770
  }, [refresh]);
1434
- const post = readerPost(props.reader, t);
1771
+ const post = readerPost(props.reader, t$4);
1435
1772
  const [selProvider, setSelProvider] = (0, react.useState)("");
1436
1773
  const [selModel, setSelModel] = (0, react.useState)("");
1437
1774
  const { effectiveProvider, effectiveModel, chosenProvider, modelOptions } = resolveSelection(data?.providers ?? [], data?.current, selProvider, selModel);
@@ -1442,7 +1779,7 @@ function OcrCard(props) {
1442
1779
  setSelModel(first);
1443
1780
  }
1444
1781
  const actions = {
1445
- t,
1782
+ t: t$4,
1446
1783
  post,
1447
1784
  refresh,
1448
1785
  setBusy,
@@ -1453,7 +1790,7 @@ function OcrCard(props) {
1453
1790
  const [touched, setTouched] = (0, react.useState)({});
1454
1791
  const [settingsBusy, setSettingsBusy] = (0, react.useState)(false);
1455
1792
  const [saveError, setSaveError] = (0, react.useState)(null);
1456
- const dirty = e(touched, value).length > 0;
1793
+ const dirty = t(touched, value).length > 0;
1457
1794
  const setField = (field, draft) => {
1458
1795
  setTouched((prev) => ({
1459
1796
  ...prev,
@@ -1465,7 +1802,7 @@ function OcrCard(props) {
1465
1802
  setSaveError(null);
1466
1803
  };
1467
1804
  const deps = {
1468
- t,
1805
+ t: t$4,
1469
1806
  data,
1470
1807
  fetchError,
1471
1808
  busy,
@@ -1476,7 +1813,7 @@ function OcrCard(props) {
1476
1813
  effectiveModel,
1477
1814
  chosenProvider,
1478
1815
  modelOptions,
1479
- ...cardHints(t, ready, snap.status, data),
1816
+ ...cardHints(t$4, ready, snap.status, data),
1480
1817
  binary: data?.ocrBinary ?? null,
1481
1818
  probeMsg,
1482
1819
  ...draftFieldsOf(touched, value),
@@ -1496,7 +1833,7 @@ function OcrCard(props) {
1496
1833
  },
1497
1834
  onProbe: () => {
1498
1835
  runProbe({
1499
- t,
1836
+ t: t$4,
1500
1837
  post,
1501
1838
  refresh,
1502
1839
  setBusy,
@@ -1506,7 +1843,7 @@ function OcrCard(props) {
1506
1843
  onField: setField,
1507
1844
  onSave: () => {
1508
1845
  saveSettingsDraft({
1509
- t,
1846
+ t: t$4,
1510
1847
  value,
1511
1848
  touched,
1512
1849
  setTouched,
@@ -1520,12 +1857,17 @@ function OcrCard(props) {
1520
1857
  const providerPanel = buildProviderPanel(deps);
1521
1858
  const cliStatus = buildCliStatusRow(deps);
1522
1859
  const settingsSection = buildSettingsSection(deps, writable, setField);
1523
- const headTitle = t("cardTitle");
1524
- const headDesc = t("cardDescription");
1860
+ const adminSection = buildAdminSection({
1861
+ ...adminState,
1862
+ t: t$4,
1863
+ writable
1864
+ });
1865
+ const headTitle = t$4("cardTitle");
1866
+ const headDesc = t$4("cardDescription");
1525
1867
  return (0, react.createElement)(OcrPluginCard, {
1526
1868
  title: headTitle,
1527
1869
  description: headDesc
1528
- }, cliStatus, providerPanel, settingsSection);
1870
+ }, cliStatus, providerPanel, settingsSection, adminSection);
1529
1871
  }
1530
1872
  const inject = [
1531
1873
  "slots",
@@ -1533,7 +1875,7 @@ const inject = [
1533
1875
  "locale"
1534
1876
  ];
1535
1877
  function apply(ctx) {
1536
- if (!e$1(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
1878
+ if (!n(ctx, "ocrReviewCardApplied", "ocr-review-card: apply claim")) return;
1537
1879
  ctx.effect(() => {
1538
1880
  const tag = document.createElement("style");
1539
1881
  tag.id = "ocr-review-card-css";
@@ -1556,7 +1898,7 @@ function apply(ctx) {
1556
1898
  t,
1557
1899
  hooks: { card: store },
1558
1900
  set: (field, value) => scope.set(field, value),
1559
- ...reader === void 0 ? {} : { reader }
1901
+ reader
1560
1902
  })
1561
1903
  }, OcrCard);
1562
1904
  });
@@ -1584,7 +1926,6 @@ function apply(ctx) {
1584
1926
  inject: () => cliShare
1585
1927
  }, OcrCliActivation));
1586
1928
  }
1587
- //#endregion
1588
1929
  exports.OcrCard = OcrCard;
1589
1930
  exports.OcrCliActivation = OcrCliActivation;
1590
1931
  exports.OcrCliBadge = OcrCliBadge;
@@ -1593,6 +1934,7 @@ exports.SaveBar = SaveBar;
1593
1934
  exports.apply = apply;
1594
1935
  exports.binaryOf = binaryOf;
1595
1936
  exports.binaryStatusText = binaryStatusText;
1937
+ exports.buildAdminSection = buildAdminSection;
1596
1938
  exports.buildCliActivation = buildCliActivation;
1597
1939
  exports.buildCliBadge = buildCliBadge;
1598
1940
  exports.buildCliSection = buildCliSection;
@@ -1609,6 +1951,7 @@ exports.isOwnSubject = isOwnSubject;
1609
1951
  exports.loadCliStatus = loadCliStatus;
1610
1952
  exports.providerOptionLabel = providerOptionLabel;
1611
1953
  exports.runProbe = runProbe;
1954
+ exports.saveSettingsDraft = saveSettingsDraft;
1612
1955
  return module.exports;
1613
1956
  },
1614
1957
  });