@jayyuen66/dsh-ocr-review 0.1.4 → 0.1.5-rc

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
@@ -36,6 +36,7 @@ const UI_MESSAGES = {
36
36
  noProvider: "当前未配置 provider",
37
37
  noReason: "宿主未给出原因(重启 dsh 或重试可恢复)",
38
38
  readProvidersFailed: "未能从 dsh 配置服务读取 provider 列表:{reason}",
39
+ endpointVerifyFailed: "装载期端点自检失败(配置仍可修改):{reason}",
39
40
  applied: "已应用:{provider} / {model}\n{test}",
40
41
  notVerified: "(未自动验证,可点「测试连接」手动验证)",
41
42
  llmTestPassed: "✓ ocr llm test 通过",
@@ -66,7 +67,23 @@ const UI_MESSAGES = {
66
67
  configPathHint: "外部 open-code-review CLI 自己的配置位置;留空 = ~/.opencodereview/config.json。自定义时必须是 <X>/.opencodereview/config.json 布局(OCR 只认该形状,插件经 HOME 重定向让它生效)",
67
68
  settingsLoading: "设置加载中,稍候可改",
68
69
  keyDynamicHint: "key 走 get-cred.mjs 动态读取,OCR 配置不留明文",
69
- keyScriptMissing: "⚠ get-cred 脚本缺失,api_key_cmd 无法工作"
70
+ keyScriptMissing: "⚠ get-cred 脚本缺失,api_key_cmd 无法工作",
71
+ binaryTitle: "外部 CLI(ocr)",
72
+ binaryUnknown: "状态未取到:刷新本页,或确认 dsh 的 web 服务已起来",
73
+ binaryBundled: "随本插件分发已就位,无需再全局安装",
74
+ binaryPath: "用 PATH 上的全局安装:{path}",
75
+ binaryNone: "未找到 ocr:`brew install open-code-review` 或 `npm i -g @alibaba-group/open-code-review`",
76
+ detectCli: "检测版本",
77
+ detecting: "检测中…",
78
+ probeVersion: "ocr 版本:{version}",
79
+ probeReason: "未取到版本号:{reason}",
80
+ badgeBundled: "ocr 随包就位",
81
+ badgePath: "ocr 来自 PATH",
82
+ badgeNone: "未找到 ocr",
83
+ badgeUnknown: "ocr 状态未取到",
84
+ binaryProbeHint: "版本号要在「配置」页点「检测版本」取——那要起一次 ocr 进程,不在本页顺手跑。",
85
+ binaryGuideDetails: "打开插件详情",
86
+ binaryGuideDismiss: "知道了"
70
87
  },
71
88
  en: {
72
89
  cardTitle: "open-code-review code review",
@@ -95,6 +112,7 @@ const UI_MESSAGES = {
95
112
  noProvider: "No provider configured yet",
96
113
  noReason: "the host gave no reason (restart dsh or retry to recover)",
97
114
  readProvidersFailed: "Could not read the provider list from the dsh settings service: {reason}",
115
+ endpointVerifyFailed: "Startup endpoint check failed (the config is still editable): {reason}",
98
116
  applied: "Applied: {provider} / {model}\n{test}",
99
117
  notVerified: "(not verified automatically — use “Test connection” to check manually)",
100
118
  llmTestPassed: "✓ ocr llm test passed",
@@ -125,11 +143,27 @@ const UI_MESSAGES = {
125
143
  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)",
126
144
  settingsLoading: "settings still loading — editable in a moment",
127
145
  keyDynamicHint: "Keys are read dynamically via get-cred.mjs; OCR config keeps no plain text",
128
- keyScriptMissing: "⚠ get-cred script missing, api_key_cmd cannot work"
146
+ keyScriptMissing: "⚠ get-cred script missing, api_key_cmd cannot work",
147
+ binaryTitle: "External CLI (ocr)",
148
+ binaryUnknown: "status not reported: reload this page, or check that the dsh web server is up",
149
+ binaryBundled: "bundled with this plugin, no global install needed",
150
+ binaryPath: "using the global install on PATH: {path}",
151
+ binaryNone: "ocr not found: `brew install open-code-review` or `npm i -g @alibaba-group/open-code-review`",
152
+ detectCli: "Detect version",
153
+ detecting: "Detecting...",
154
+ probeVersion: "ocr version: {version}",
155
+ probeReason: "no version read: {reason}",
156
+ badgeBundled: "ocr bundled",
157
+ badgePath: "ocr from PATH",
158
+ badgeNone: "ocr not found",
159
+ badgeUnknown: "ocr status unavailable",
160
+ binaryProbeHint: "the version comes from the Detect action on the configuration page — that spawns an ocr process, so it is not run here",
161
+ binaryGuideDetails: "Open plugin details",
162
+ binaryGuideDismiss: "Got it"
129
163
  }
130
164
  };
131
165
  //#endregion
132
- //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.4_@deepseek-ai+cordis@4.0.5-alpha.1_@deepseek-ai+dsh-c_472a88c28a1052d4f20b6206ef930fa9/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
166
+ //#region node_modules/.pnpm/@jayyuen66+dsh-plugin-shared@0.1.5-rc_@deepseek-ai+cordis@4.0.5-alpha.1_@deepseek-ai+ds_e716d822a8a24fbc7ac6d086dd04b807/node_modules/@jayyuen66/dsh-plugin-shared/dist/record.js
133
167
  function e(e) {
134
168
  return typeof e == `object` && !!e && !Array.isArray(e);
135
169
  }
@@ -161,6 +195,7 @@ const PROVIDERS_URL = "/_dsh/ocr-review/providers";
161
195
  const SELECT_URL = "/_dsh/ocr-review/select";
162
196
  const MIGRATE_URL = "/_dsh/ocr-review/migrate";
163
197
  const TEST_URL = "/_dsh/ocr-review/test";
198
+ const PROBE_URL = "/_dsh/ocr-review/probe";
164
199
  /** 写端点 CSRF 头名(与 host.ts OCR_CSRF_HEADER 同源,改名要两端一起改)。 */
165
200
  const CSRF_HEADER = "x-ocr-csrf";
166
201
  /** 未知值 → 安全字符串(非字符串给 '')。 */
@@ -194,6 +229,36 @@ function providerOf(raw) {
194
229
  hasKey: t(raw, "hasKey") === true
195
230
  };
196
231
  }
232
+ /** 可得性来源的合法域(与 host 侧 `OcrBinarySource` 同一份取值)。形状不合即「没报」。 */
233
+ const BINARY_SOURCES = /* @__PURE__ */ new Set([
234
+ "bundled",
235
+ "path",
236
+ "none"
237
+ ]);
238
+ /** 解析宿主送的可得性对象。导出是为了让「wire 键名」这条契约能被钉住:卡片与 host
239
+ * 两处各写一份键名时,写错了的症状是状态行永远显示「未取到」,而不是任何报错。 */
240
+ function binaryOf(raw) {
241
+ if (!e(raw)) return null;
242
+ const source = strOf(raw["source"]);
243
+ if (!BINARY_SOURCES.has(source)) return null;
244
+ const path = strOf(raw["path"]);
245
+ return path === "" ? { source } : {
246
+ source,
247
+ path
248
+ };
249
+ }
250
+ /**
251
+ * 详情页徽标/区块/启用指引共用的取数口:读同一条 GET providers 的 `ocrBinary` 那半。
252
+ * 判据只在 host 的 `resolveOcrAvailability` 一处,本包三个显示面都只翻译它的结论。
253
+ */
254
+ async function loadCliStatus() {
255
+ try {
256
+ const res = await fetch(PROVIDERS_URL, { headers: { accept: "application/json" } });
257
+ return res.ok ? binaryOf(t(await res.json(), "ocrBinary")) : null;
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
197
262
  function providersPayloadOf(json) {
198
263
  const providersArr = t(json, "providers");
199
264
  const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
@@ -205,6 +270,11 @@ function providersPayloadOf(json) {
205
270
  url: strOf(t(currentRaw, "url"))
206
271
  };
207
272
  const sourceRaw = t(json, "source");
273
+ const verifyRaw = t(json, "endpointVerify");
274
+ const endpointVerify = verifyRaw === null || verifyRaw === void 0 ? null : {
275
+ ok: t(verifyRaw, "ok") === true,
276
+ message: strOf(t(verifyRaw, "message"))
277
+ };
208
278
  return {
209
279
  providers,
210
280
  current,
@@ -213,13 +283,17 @@ function providersPayloadOf(json) {
213
283
  status: strOf(t(sourceRaw, "status")),
214
284
  message: strOf(t(sourceRaw, "message"))
215
285
  },
216
- csrf: strOf(t(json, "csrf"))
286
+ csrf: strOf(t(json, "csrf")),
287
+ endpointVerify,
288
+ ocrBinary: binaryOf(t(json, "ocrBinary"))
217
289
  };
218
290
  }
219
291
  /** 三行表单控件(数字/文本/下拉)共用的类名 = CARD_CSS 里 `.ocr-select{…}` 那条规则。
220
292
  * 抽出来是因为两处 input 与一处 select 必须同宽同边框:写三遍字面量时改一处漏两处,
221
293
  * 症状是设置卡里某一行的控件比其它行窄。 */
222
294
  const FORM_CONTROL_CLASS = "ocr-select";
295
+ /** 小节标题的类名(卡片头、CLI 状态行、详情页区块共用;sonarjs 的重复字面量门禁)。 */
296
+ const CARD_NAME_CLASS = "ocr-card-name";
223
297
  const CARD_CSS = [
224
298
  ".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}",
225
299
  ".ocr-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
@@ -338,7 +412,7 @@ function OcrPluginCard(props) {
338
412
  const { title, description, children } = props;
339
413
  const [open, setOpen] = (0, react.useState)(false);
340
414
  const pathKey = "d";
341
- const headRow = (0, react.createElement)("span", { className: "ocr-card-head" }, (0, react.createElement)("span", { className: "ocr-card-name" }, title), (0, react.createElement)("span", { className: "ocr-card-desc" }, description));
415
+ 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));
342
416
  const chevron = (0, react.createElement)("svg", {
343
417
  width: 14,
344
418
  height: 14,
@@ -469,6 +543,172 @@ function currentConfigText(t, current) {
469
543
  keyPart: t(current.keyIsDynamic ? "dynamicKey" : "plainKeyWarning")
470
544
  });
471
545
  }
546
+ /**
547
+ * 外部 CLI 可得性那一行的文本。三档来源与「宿主没报」四种说法,判据全在 host 侧
548
+ * (`lib/cli.ts` 的 resolveOcrAvailability),卡片只翻译结论——两处各算各的就会漂。
549
+ */
550
+ function binaryStatusText(t, binary) {
551
+ if (binary === null) return t("binaryUnknown");
552
+ if (binary.source === "bundled") return t("binaryBundled");
553
+ if (binary.source === "path") return t("binaryPath", { path: binary.path ?? "" });
554
+ return t("binaryNone");
555
+ }
556
+ /** 一条错误提示行的类名(多处在用;sonarjs 的重复字面量门禁要求收成常量)。 */
557
+ const STATUS_ERROR_CLASS = "ocr-status ocr-err";
558
+ /** 一条成功/失败回执行的类名(动作条、探测回执共用)。 */
559
+ function statusClass(ok) {
560
+ return `ocr-status ${ok ? "ocr-ok" : "ocr-err"}`;
561
+ }
562
+ /**
563
+ * 外部 CLI 状态行:可得性 + 「检测」按钮 + 最近一次探测回执。
564
+ *
565
+ * 刻意画在 provider 表单**之外**(provider 源不可用时也要看得见)——「这台机器有没有
566
+ * ocr」与「宿主能不能列出 provider」是两件事,混在一支里会让缺 CLI 的机器连原因都看不到。
567
+ * 回执用 `.ocr-status`(`white-space:pre-wrap`)而不是输入框:那段文本里就有上游给的
568
+ * `npm i -g …` 命令,要能选中复制,而本包不提供剪贴板 API(非安全上下文会静默失败)。
569
+ */
570
+ function buildCliStatusRow(deps) {
571
+ const { t } = deps;
572
+ const label = (0, react.createElement)("div", { className: CARD_NAME_CLASS }, t("binaryTitle"));
573
+ const button = (0, react.createElement)("button", {
574
+ className: "ocr-btn",
575
+ type: "button",
576
+ disabled: deps.busy !== null,
577
+ onClick: deps.onProbe,
578
+ "data-field": "probe"
579
+ }, deps.busy === "probe" ? t("detecting") : t("detectCli"));
580
+ const row = (0, react.createElement)("div", {
581
+ className: "ocr-row",
582
+ style: {
583
+ justifyContent: "flex-start",
584
+ gap: 8
585
+ }
586
+ }, label, button);
587
+ const statusText = binaryStatusText(t, deps.binary);
588
+ const status = (0, react.createElement)("div", { className: "ocr-hint" }, statusText);
589
+ const probe = deps.probeMsg ? (0, react.createElement)("div", {
590
+ className: statusClass(deps.probeMsg.ok),
591
+ "data-field": "probe-result"
592
+ }, deps.probeMsg.text) : null;
593
+ return (0, react.createElement)("div", {
594
+ style: { padding: "2px 0" },
595
+ "data-field": "cli-status"
596
+ }, row, status, probe);
597
+ }
598
+ /**
599
+ * 徽标用的短文案(标题旁那只标签放不下整句装法)。与 `binaryStatusText` 同一判据、
600
+ * 不同长度:两处各算一份「哪一档」就会漂,所以入参仍是 host 送的那份 `OcrBinaryView`。
601
+ */
602
+ function cliBadgeText(t, binary) {
603
+ if (binary === null) return t("badgeUnknown");
604
+ if (binary.source === "bundled") return t("badgeBundled");
605
+ if (binary.source === "path") return t("badgePath");
606
+ return t("badgeNone");
607
+ }
608
+ /** 这页说的是不是本包(bundle 页与它的行页都算;官方插件页 `item` 没有包名可比)。
609
+ * 导出是因为「在别人插件的页上白拉一次自己的端点」这条判据只能在这里钉。 */
610
+ function isOwnSubject(subject) {
611
+ return subject.kind !== "item" && subject.pkg.name === BUNDLE_PKG;
612
+ }
613
+ /** 徽标那枚元素(状态已到才画;取状态与 subject 过滤留给组件,用例才不必起渲染器)。 */
614
+ function buildCliBadge(t, status) {
615
+ return (0, react.createElement)("span", {
616
+ className: "ocr-hint",
617
+ "data-field": "cli-badge"
618
+ }, cliBadgeText(t, status));
619
+ }
620
+ /**
621
+ * 订阅一次外部 CLI 可得性(GET providers 的 `ocrBinary` 那半,纯 fs 判据、不起进程)。
622
+ *
623
+ * `active` 为假(这页说的不是本包)时既不发请求也不画东西:官方契约是「对无话可说的
624
+ * subject 渲染 null」,在别人插件的页面上拉一次自己的端点属于白干的活。
625
+ * @returns undefined = 还没回来(渲染 null,不闪「状态未知」);null = 宿主没报。
626
+ */
627
+ function useCliStatus(active, load) {
628
+ const [status, setStatus] = (0, react.useState)(void 0);
629
+ (0, react.useEffect)(() => {
630
+ let alive = true;
631
+ const settle = (value) => {
632
+ if (alive) setStatus(value);
633
+ };
634
+ const run = async () => {
635
+ try {
636
+ settle(await load());
637
+ } catch {
638
+ settle(null);
639
+ }
640
+ };
641
+ if (active) run();
642
+ return () => {
643
+ alive = false;
644
+ };
645
+ }, [active, load]);
646
+ return status;
647
+ }
648
+ /** 详情页标题旁的徽标:一句话说明外部 ocr 从哪条路来(或压根没有)。 */
649
+ function OcrCliBadge(props) {
650
+ const mine = isOwnSubject(props.subject);
651
+ const status = useCliStatus(mine, props.loadCliStatus);
652
+ if (!mine || status === void 0) return null;
653
+ return buildCliBadge(props.t, status);
654
+ }
655
+ /** 区块体:标题 + 完整状态那一行 + 指路那一句。 */
656
+ function buildCliSection(t, status) {
657
+ const title = (0, react.createElement)("div", { className: CARD_NAME_CLASS }, t("binaryTitle"));
658
+ const body = (0, react.createElement)("div", { className: "ocr-hint" }, binaryStatusText(t, status));
659
+ const hint = (0, react.createElement)("div", { className: "ocr-hint" }, t("binaryProbeHint"));
660
+ return (0, react.createElement)("div", {
661
+ style: { padding: "8px 0" },
662
+ "data-field": "cli-section"
663
+ }, title, body, hint);
664
+ }
665
+ /** 详情页下方的区块:完整状态 + 「版本号要在配置卡片里点检测」的指路。 */
666
+ function OcrCliSection(props) {
667
+ const mine = isOwnSubject(props.subject);
668
+ const status = useCliStatus(mine, props.loadCliStatus);
669
+ if (!mine || status === void 0) return null;
670
+ return buildCliSection(props.t, status);
671
+ }
672
+ /**
673
+ * 启用指引的内容:一句「两条路都没找到」+ 去详情页 / 收起这两个动作。
674
+ * 动作回调由宿主给(`onOpenDetails` 顺带把用户送到本包配置页,那里才有「检测」按钮)。
675
+ */
676
+ function buildCliActivation(t, props) {
677
+ const text = (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, t("binaryNone"));
678
+ const details = (0, react.createElement)("button", {
679
+ className: "ocr-btn",
680
+ type: "button",
681
+ onClick: props.onOpenDetails,
682
+ "data-field": "guide-details"
683
+ }, t("binaryGuideDetails"));
684
+ const dismiss = (0, react.createElement)("button", {
685
+ className: "ocr-btn",
686
+ type: "button",
687
+ onClick: props.onDismiss,
688
+ "data-field": "guide-dismiss"
689
+ }, t("binaryGuideDismiss"));
690
+ return (0, react.createElement)("div", {
691
+ style: {
692
+ padding: "8px 0",
693
+ display: "flex",
694
+ gap: 8
695
+ },
696
+ "data-field": "cli-activation"
697
+ }, text, details, dismiss);
698
+ }
699
+ /**
700
+ * 启用后的一次性指引:只在「两条路都没找到 ocr」那一刻说话。
701
+ *
702
+ * 这是官方给第三方 bundle 的最外侧位置(`plugins.bundle.activation`,用户从列表启用本包
703
+ * 后由页面渲染)。列表卡片上那排标签全部来自宿主对条目自身的判定,插件注入不进去,所以
704
+ * 「装没装外部 CLI」能被显示的最外一处就是这里,外加详情页那只徽标。
705
+ */
706
+ function OcrCliActivation(props) {
707
+ const mine = props.packageName === BUNDLE_PKG;
708
+ const status = useCliStatus(mine, props.loadCliStatus);
709
+ if (!mine || status?.source !== "none") return null;
710
+ return buildCliActivation(props.t, props);
711
+ }
472
712
  /** 数据齐(source.status === "ready")那一支:provider/model 两行 + 三个动作按钮 +
473
713
  * 当前配置与 key 两行提示。拆出来是因为这一支画的是整块表单,与上面三条「一行提示」
474
714
  * 的降级支本就不是同一件事。 */
@@ -512,21 +752,21 @@ function buildReadyProviderSection(deps, data) {
512
752
  const applyBtn = (0, react.createElement)("button", {
513
753
  className: "ocr-btn ocr-btn-primary",
514
754
  type: "button",
515
- disabled: deps.busy,
755
+ disabled: deps.busy !== null,
516
756
  onClick: deps.onApply,
517
757
  "data-field": "apply"
518
- }, deps.busy ? t("applying") : t("applyToOcr"));
758
+ }, deps.busy === "apply" ? t("applying") : t("applyToOcr"));
519
759
  const testBtn = (0, react.createElement)("button", {
520
760
  className: "ocr-btn",
521
761
  type: "button",
522
- disabled: deps.busy,
762
+ disabled: deps.busy !== null,
523
763
  onClick: deps.onTest,
524
764
  "data-field": "test"
525
765
  }, t("testConnection"));
526
766
  const migrateBtn = (0, react.createElement)("button", {
527
767
  className: "ocr-btn",
528
768
  type: "button",
529
- disabled: deps.busy,
769
+ disabled: deps.busy !== null,
530
770
  onClick: deps.onMigrate,
531
771
  "data-field": "migrate"
532
772
  }, t("migrateKeys"));
@@ -541,7 +781,12 @@ function buildReadyProviderSection(deps, data) {
541
781
  const currentHint = cur.provider === "" ? t("noProvider") : currentConfigText(t, cur);
542
782
  const currentInfo = (0, react.createElement)("div", { className: "ocr-hint" }, currentHint);
543
783
  const keyInfo = (0, react.createElement)("div", { className: "ocr-hint" }, deps.keyHint);
544
- return (0, react.createElement)("div", {}, providerRow, modelRow, actionRow, currentInfo, keyInfo);
784
+ const verify = data.endpointVerify ?? null;
785
+ const verifyWarning = verify !== null && !verify.ok ? (0, react.createElement)("div", {
786
+ className: STATUS_ERROR_CLASS,
787
+ "data-field": "endpoint-verify"
788
+ }, t("endpointVerifyFailed", { reason: verify.message })) : null;
789
+ return (0, react.createElement)("div", {}, verifyWarning, providerRow, modelRow, actionRow, currentInfo, keyInfo);
545
790
  }
546
791
  /** Provider / Model 选择与应用区:三条降级各出一行提示(数据未就绪/读取失败/
547
792
  * 数据源不可用),只有 source.status === "ready" 才交给 buildReadyProviderSection 画表单。 */
@@ -549,13 +794,13 @@ function buildProviderSection(deps) {
549
794
  const { t } = deps;
550
795
  if (deps.fetchError !== null) {
551
796
  const reason = t("loadProvidersFailed", { error: deps.fetchError });
552
- return (0, react.createElement)("div", { className: "ocr-status ocr-err" }, reason);
797
+ return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, reason);
553
798
  }
554
799
  if (deps.data === null) return (0, react.createElement)("div", { className: "ocr-status ocr-muted" }, t("loadingProviders"));
555
800
  if (deps.data.source.status !== "ready") {
556
801
  const sourceMessage = deps.data.source.message;
557
802
  const line = t("readProvidersFailed", { reason: sourceMessage === "" ? t("noReason") : sourceMessage });
558
- return (0, react.createElement)("div", { className: "ocr-status ocr-err" }, line);
803
+ return (0, react.createElement)("div", { className: STATUS_ERROR_CLASS }, line);
559
804
  }
560
805
  return buildReadyProviderSection(deps, deps.data);
561
806
  }
@@ -738,7 +983,7 @@ async function runApplySelection(deps) {
738
983
  });
739
984
  return;
740
985
  }
741
- deps.setBusy(true);
986
+ deps.setBusy("apply");
742
987
  deps.setActionMsg(null);
743
988
  try {
744
989
  const selJson = await (await deps.post(SELECT_URL, JSON.stringify({
@@ -776,13 +1021,13 @@ async function runApplySelection(deps) {
776
1021
  text: failedText
777
1022
  });
778
1023
  } finally {
779
- deps.setBusy(false);
1024
+ deps.setBusy(null);
780
1025
  }
781
1026
  }
782
1027
  /** POST migrate:明文 api_key 一键迁到 api_key_cmd,回执把迁移/跳过两个名单拼成一行。 */
783
1028
  async function runMigrateKeys(deps) {
784
1029
  const { t: t$2 } = deps;
785
- deps.setBusy(true);
1030
+ deps.setBusy("migrate");
786
1031
  deps.setActionMsg(null);
787
1032
  try {
788
1033
  const json = await (await deps.post(MIGRATE_URL)).json();
@@ -804,13 +1049,13 @@ async function runMigrateKeys(deps) {
804
1049
  text: failedText
805
1050
  });
806
1051
  } finally {
807
- deps.setBusy(false);
1052
+ deps.setBusy(null);
808
1053
  }
809
1054
  }
810
1055
  /** POST test:手动触发 ocr llm test,输出原样回显在动作条上。 */
811
1056
  async function runTestConnection(deps) {
812
1057
  const { t: t$3 } = deps;
813
- deps.setBusy(true);
1058
+ deps.setBusy("test");
814
1059
  deps.setActionMsg(null);
815
1060
  try {
816
1061
  const json = await (await deps.post(TEST_URL)).json();
@@ -827,7 +1072,36 @@ async function runTestConnection(deps) {
827
1072
  text: failedText
828
1073
  });
829
1074
  } finally {
830
- deps.setBusy(false);
1075
+ deps.setBusy(null);
1076
+ }
1077
+ }
1078
+ /**
1079
+ * POST probe:跑一次 `ocr version`。
1080
+ *
1081
+ * 回执里的 `output` 原样附上,不从中抽「有新版本」那句——那是上游的措辞,抽一次就多
1082
+ * 一处会跟着漂的判据;那段文本本来就带在页面上供人选中复制(含上游给的 `npm i -g …`)。
1083
+ */
1084
+ async function runProbe(deps) {
1085
+ const { t: t$4 } = deps;
1086
+ deps.setBusy("probe");
1087
+ deps.setProbeMsg(null);
1088
+ try {
1089
+ const json = await (await deps.post(PROBE_URL)).json();
1090
+ const ok = t(json, "ok") === true;
1091
+ const head = ok ? t$4("probeVersion", { version: strOf(t(json, "version")) }) : t$4("probeReason", { reason: strOf(t(json, "reason")) });
1092
+ const output = strOf(t(json, "output"));
1093
+ deps.setProbeMsg({
1094
+ ok,
1095
+ text: output === "" ? head : `${head}\n${output}`
1096
+ });
1097
+ deps.refresh();
1098
+ } catch (error) {
1099
+ deps.setProbeMsg({
1100
+ ok: false,
1101
+ text: `${t$4("requestFailed")}${errorSummary(error)}`
1102
+ });
1103
+ } finally {
1104
+ deps.setBusy(null);
831
1105
  }
832
1106
  }
833
1107
  /** 只写差异字段:touched 值为 undefined → 写 null(= 清回 schema 默认)。
@@ -885,7 +1159,7 @@ function cardHints(t, ready, status, data) {
885
1159
  function buildProviderPanel(deps) {
886
1160
  const { actionMsg } = deps;
887
1161
  const providerSection = buildProviderSection(deps);
888
- const actionMsgNode = actionMsg ? (0, react.createElement)("div", { className: `ocr-status ${actionMsg.ok ? "ocr-ok" : "ocr-err"}` }, actionMsg.text) : null;
1162
+ const actionMsgNode = actionMsg ? (0, react.createElement)("div", { className: statusClass(actionMsg.ok) }, actionMsg.text) : null;
889
1163
  return (0, react.createElement)("div", { style: { padding: "2px 0" } }, providerSection, actionMsgNode);
890
1164
  }
891
1165
  function OcrCard(props) {
@@ -896,8 +1170,9 @@ function OcrCard(props) {
896
1170
  const writable = snap.writable && ready;
897
1171
  const [data, setData] = (0, react.useState)(null);
898
1172
  const [fetchError, setFetchError] = (0, react.useState)(null);
899
- const [busy, setBusy] = (0, react.useState)(false);
1173
+ const [busy, setBusy] = (0, react.useState)(null);
900
1174
  const [actionMsg, setActionMsg] = (0, react.useState)(null);
1175
+ const [probeMsg, setProbeMsg] = (0, react.useState)(null);
901
1176
  const refresh = async () => refreshProviders({
902
1177
  setData,
903
1178
  setFetchError
@@ -954,6 +1229,8 @@ function OcrCard(props) {
954
1229
  chosenProvider,
955
1230
  modelOptions,
956
1231
  ...cardHints(t, ready, snap.status, data),
1232
+ binary: data?.ocrBinary ?? null,
1233
+ probeMsg,
957
1234
  ...draftFieldsOf(touched, value),
958
1235
  dirty,
959
1236
  settingsBusy,
@@ -969,6 +1246,15 @@ function OcrCard(props) {
969
1246
  onMigrate: () => {
970
1247
  runMigrateKeys(actions);
971
1248
  },
1249
+ onProbe: () => {
1250
+ runProbe({
1251
+ t,
1252
+ post,
1253
+ refresh,
1254
+ setBusy,
1255
+ setProbeMsg
1256
+ });
1257
+ },
972
1258
  onField: setField,
973
1259
  onSave: () => {
974
1260
  saveSettingsDraft({
@@ -984,13 +1270,14 @@ function OcrCard(props) {
984
1270
  onDiscard: discard
985
1271
  };
986
1272
  const providerPanel = buildProviderPanel(deps);
1273
+ const cliStatus = buildCliStatusRow(deps);
987
1274
  const settingsSection = buildSettingsSection(deps, writable, setField);
988
1275
  const headTitle = t("cardTitle");
989
1276
  const headDesc = t("cardDescription");
990
1277
  return (0, react.createElement)(OcrPluginCard, {
991
1278
  title: headTitle,
992
1279
  description: headDesc
993
- }, providerPanel, settingsSection);
1280
+ }, cliStatus, providerPanel, settingsSection);
994
1281
  }
995
1282
  const inject = [
996
1283
  "slots",
@@ -1027,18 +1314,54 @@ function apply(ctx) {
1027
1314
  })
1028
1315
  }, OcrCard);
1029
1316
  });
1317
+ const cliShare = {
1318
+ t,
1319
+ loadCliStatus
1320
+ };
1321
+ ctx.slots.inject("plugins.detail.badge", () => ctx.slots.register({
1322
+ name: "plugins.detail.badge",
1323
+ id: `${NS}-cli-badge`,
1324
+ order: 100,
1325
+ label: "ocr-review CLI badge",
1326
+ inject: () => cliShare
1327
+ }, OcrCliBadge));
1328
+ ctx.slots.inject("plugins.detail.section", () => ctx.slots.register({
1329
+ name: "plugins.detail.section",
1330
+ id: `${NS}-cli-section`,
1331
+ order: 100,
1332
+ label: "ocr-review CLI section",
1333
+ inject: () => cliShare
1334
+ }, OcrCliSection));
1335
+ ctx.slots.inject("plugins.bundle.activation", () => ctx.slots.register({
1336
+ name: "plugins.bundle.activation",
1337
+ key: BUNDLE_PKG,
1338
+ inject: () => cliShare
1339
+ }, OcrCliActivation));
1030
1340
  }
1031
1341
  //#endregion
1032
1342
  exports.OcrCard = OcrCard;
1343
+ exports.OcrCliActivation = OcrCliActivation;
1344
+ exports.OcrCliBadge = OcrCliBadge;
1345
+ exports.OcrCliSection = OcrCliSection;
1033
1346
  exports.SaveBar = SaveBar;
1034
1347
  exports.apply = apply;
1348
+ exports.binaryOf = binaryOf;
1349
+ exports.binaryStatusText = binaryStatusText;
1350
+ exports.buildCliActivation = buildCliActivation;
1351
+ exports.buildCliBadge = buildCliBadge;
1352
+ exports.buildCliSection = buildCliSection;
1353
+ exports.buildCliStatusRow = buildCliStatusRow;
1035
1354
  exports.buildProviderSection = buildProviderSection;
1036
1355
  exports.buildSettingsSection = buildSettingsSection;
1037
1356
  exports.cardStore = cardStore;
1357
+ exports.cliBadgeText = cliBadgeText;
1038
1358
  exports.currentConfigText = currentConfigText;
1039
1359
  exports.diffTouched = diffTouched;
1040
1360
  exports.inject = inject;
1361
+ exports.isOwnSubject = isOwnSubject;
1362
+ exports.loadCliStatus = loadCliStatus;
1041
1363
  exports.providerOptionLabel = providerOptionLabel;
1364
+ exports.runProbe = runProbe;
1042
1365
  return module.exports;
1043
1366
  },
1044
1367
  });
package/cordis.patch.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  # ocr-review:open-code-review 工作区审查插件(bundle)。
2
2
  # 双面包机制:本包含 Host 半(host.ts:5 个工具 ocr_review/ocr_scan/
3
- # ocr_delegate_preview/ocr_delegate_rule/ocr_session + settings namespace「ocr-review」+
4
- # webServer 配置端点)与 Client 半(src/client-entry.ts → client.js:设置卡片,
5
- # 从 settings.yaml 的 llm-pi-ai.providers 选 provider/model 应用到
6
- # ~/.opencodereview/config.json,key 走 api_key_cmd 动态读取 .credentials.yaml
7
- # 的 refs,OCR 配置永不落明文 key)。
3
+ # ocr_delegate_preview/ocr_delegate_rule/ocr_session + 由 `Config` 上 `.volatile()`
4
+ # 隐式投影的设置面(命名空间就是下面这条的 id)+ webServer 配置端点)与
5
+ # Client 半(src/client-entry.ts → client.js:设置卡片,从宿主设置服务的
6
+ # llm-pi-ai.providers 选 provider/model 应用到
7
+ # ~/.opencodereview/config.json,key 走 api_key_cmd 动态读取宿主凭据 ref,
8
+ # OCR 配置永不落明文 key)。
8
9
  #
9
10
  # 组合包层:本文件由 package.json 的 dsh.bundle.patch 指向,随
10
11
  # `dsh plugin --profile web add/remove ./plugins/ocr-review` 维护。行按裸包名