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