@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/README.md +40 -16
- package/README.zh-CN.md +40 -15
- package/client.js +344 -21
- package/cordis.patch.yml +6 -5
- package/host.js +596 -125
- package/package.json +8 -10
- package/scripts/get-cred.mjs +7 -6
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.
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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)(
|
|
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 +
|
|
4
|
-
# webServer 配置端点)与
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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` 维护。行按裸包名
|