@jayyuen66/dsh-ocr-review 0.1.7 → 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/CHANGELOG.md +289 -0
- package/LICENSE +1 -1
- package/README.md +203 -168
- package/README.zh-CN.md +208 -174
- package/THIRD_PARTY_NOTICES.md +10 -7
- package/client.js +475 -132
- package/cordis.patch.yml +4 -2
- package/host.js +2537 -885
- package/package.json +46 -46
- package/scripts/get-cred.mjs +151 -55
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
|
-
|
|
10
|
+
// src/ui-messages.ts
|
|
11
11
|
const UI_MESSAGES = {
|
|
12
12
|
zh: {
|
|
13
13
|
cardTitle: "open-code-review 代码审查",
|
|
@@ -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
|
|
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 =
|
|
65
|
+
maxCommentsHint: "返回给模型的 top 评论条数;0 = 全部(含未锚定行,输出大)(默认见本行设置项的初值)",
|
|
65
66
|
timeoutLabel: "运行超时 (timeoutMinutes)",
|
|
66
|
-
timeoutHint: "前台墙钟超时(分钟);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 无法工作",
|
|
@@ -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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
r
|
|
198
|
-
},
|
|
246
|
+
n.effect(() => () => {
|
|
247
|
+
a[r] === c && t$1(a, r, s);
|
|
248
|
+
}, i);
|
|
199
249
|
} catch (e) {
|
|
200
|
-
throw r
|
|
250
|
+
throw a[r] === c && t$1(a, r, s), e;
|
|
201
251
|
}
|
|
202
252
|
return !0;
|
|
203
253
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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`
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
339
|
-
*
|
|
487
|
+
* 造一个「读/写宿主侧 OCR 配置」的读取器。**当前部署没有远程面时读取会抛**可读错误
|
|
488
|
+
* (卡片那层按「请求失败」显示原因)——remote 不在构造期做快照,网关晚于卡片注册
|
|
489
|
+
* `provide("remote")` 的部署在首次成功读取时就能接上,不存在「apply 期没有就永远
|
|
490
|
+
* 读不到」的路径。
|
|
340
491
|
*
|
|
341
|
-
* `$mount` 是**幂等**的(挂进本 fiber
|
|
342
|
-
*
|
|
492
|
+
* `$mount` 是**幂等**的(挂进本 fiber,随卡片卸载自动撤),所以缓存那枚 promise:
|
|
493
|
+
* 同一次会话里反复拉取只挂一次;拒绝态例外(见 buildOcrReader)。
|
|
343
494
|
*
|
|
344
495
|
* @param ctx - 客户端上下文(只用 `get` 一位)
|
|
345
|
-
* @returns
|
|
496
|
+
* @returns 读取器
|
|
346
497
|
*/
|
|
347
498
|
function createOcrReader(ctx) {
|
|
348
|
-
|
|
349
|
-
return isRemoteMounter(remote) ? buildOcrReader(ctx, remote) : void 0;
|
|
499
|
+
return buildOcrReader(ctx);
|
|
350
500
|
}
|
|
351
|
-
|
|
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
|
-
*
|
|
369
|
-
*
|
|
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 =
|
|
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(
|
|
387
|
-
name: strOf(
|
|
537
|
+
id: strOf(i(raw, "id")),
|
|
538
|
+
name: strOf(i(raw, "name"))
|
|
388
539
|
};
|
|
389
540
|
}
|
|
390
541
|
function providerOf(raw) {
|
|
391
|
-
const modelsArr =
|
|
542
|
+
const modelsArr = i(raw, "models");
|
|
392
543
|
const models = Array.isArray(modelsArr) ? modelsArr.map((item) => modelOf(item)) : [];
|
|
393
|
-
const apiKeyEnvRaw =
|
|
544
|
+
const apiKeyEnvRaw = i(raw, "apiKeyEnv");
|
|
394
545
|
return {
|
|
395
|
-
name: strOf(
|
|
396
|
-
displayName: strOf(
|
|
546
|
+
name: strOf(i(raw, "name")),
|
|
547
|
+
displayName: strOf(i(raw, "displayName")),
|
|
397
548
|
apiKeyEnv: typeof apiKeyEnvRaw === "string" ? apiKeyEnvRaw : null,
|
|
398
|
-
baseURL: strOf(
|
|
549
|
+
baseURL: strOf(i(raw, "baseURL")),
|
|
399
550
|
models,
|
|
400
|
-
hasKey:
|
|
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(
|
|
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 =
|
|
600
|
+
const providersArr = i(json, "providers");
|
|
450
601
|
const providers = Array.isArray(providersArr) ? providersArr.map((item) => providerOf(item)) : [];
|
|
451
|
-
const currentRaw =
|
|
602
|
+
const currentRaw = i(json, "current");
|
|
452
603
|
const current = {
|
|
453
|
-
provider: strOf(
|
|
454
|
-
model: strOf(
|
|
455
|
-
keyIsDynamic:
|
|
456
|
-
url: strOf(
|
|
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 =
|
|
459
|
-
const verifyRaw =
|
|
609
|
+
const sourceRaw = i(json, "source");
|
|
610
|
+
const verifyRaw = i(json, "endpointVerify");
|
|
460
611
|
const endpointVerify = verifyRaw === null || verifyRaw === void 0 ? null : {
|
|
461
|
-
ok:
|
|
462
|
-
message: strOf(
|
|
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:
|
|
618
|
+
getCredReady: i(json, "getCredReady") === true,
|
|
468
619
|
source: {
|
|
469
|
-
status: strOf(
|
|
470
|
-
message: strOf(
|
|
620
|
+
status: strOf(i(sourceRaw, "status")),
|
|
621
|
+
message: strOf(i(sourceRaw, "message"))
|
|
471
622
|
},
|
|
472
623
|
endpointVerify,
|
|
473
|
-
ocrBinary: binaryOf(
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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 (
|
|
1184
|
-
const detail = strOf(
|
|
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(
|
|
1202
|
-
const model = strOf(
|
|
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 (
|
|
1464
|
+
if (i(selJson, "ok") === true) {
|
|
1240
1465
|
let testText;
|
|
1241
|
-
const llmTestRaw =
|
|
1466
|
+
const llmTestRaw = i(selJson, "llmTest");
|
|
1242
1467
|
if (e$2(llmTestRaw)) {
|
|
1243
|
-
const testOutput = strOf(
|
|
1244
|
-
testText =
|
|
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(
|
|
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 =
|
|
1281
|
-
const skippedVal =
|
|
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 =
|
|
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 =
|
|
1309
|
-
const testOutput = strOf(
|
|
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 =
|
|
1337
|
-
const head = ok ? t("probeVersion", { version: strOf(
|
|
1338
|
-
const output = strOf(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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", "
|
|
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 =
|
|
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
|
|
1524
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
});
|