picturereader 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -139
- package/client.js +536 -0
- package/package.json +15 -3
- package/scripts/doc-to-image.py +194 -0
- package/scripts/setup-doc-venv.mjs +66 -0
- package/scripts/setup-rapid.mjs +85 -0
- package/src/bridge.js +162 -0
- package/src/config.js +70 -0
- package/src/core.js +94 -2
- package/src/doc-tools.js +326 -0
- package/src/image-batch.js +504 -0
- package/src/index.js +258 -32
- package/src/more-tools.js +695 -0
- package/src/picturereader-vision.mjs +164 -0
- package/src/routing.js +130 -0
- package/src/runtime.js +127 -0
- package/src/settings-expose.js +75 -0
- package/src/tool.js +19 -14
- package/src/vision-analyze.js +35 -13
- package/src/vlm.js +198 -15
package/client.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* picturereader — Web settings card (client half).
|
|
3
|
+
*
|
|
4
|
+
* registers a "图片阅读" section inside the DSH Web settings page:
|
|
5
|
+
* - Top: usage mode dropdown (privacy / smart / strict)
|
|
6
|
+
* - Vision bridge model picker (checkbox list of all text-only models,
|
|
7
|
+
* each with an optional note field; checked models get a "(视觉)" variant)
|
|
8
|
+
* - External vision API fields (base URL / model / key) — gated by "启用外部视觉 API" checkbox
|
|
9
|
+
* - OCR engine selector
|
|
10
|
+
* - Advanced settings (timeout / max tokens / export dir / debug)
|
|
11
|
+
*
|
|
12
|
+
* Hand-written ModuleLoader bundle — no build step.
|
|
13
|
+
*/
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: "picturereader",
|
|
16
|
+
factory: (require) => {
|
|
17
|
+
var module = { exports: {} };
|
|
18
|
+
var exports = module.exports;
|
|
19
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
20
|
+
var react = require("react");
|
|
21
|
+
var h = react.createElement;
|
|
22
|
+
|
|
23
|
+
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
24
|
+
var CSS =
|
|
25
|
+
".__pr_root{max-width:640px;display:flex;flex-direction:column;gap:10px}" +
|
|
26
|
+
".__pr_field{display:flex;flex-direction:column;gap:4px}" +
|
|
27
|
+
".__pr_label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px}" +
|
|
28
|
+
".__pr_hint{font-size:11px;color:var(--dsw-alias-label-tertiary)}" +
|
|
29
|
+
".__pr_row{display:flex;align-items:center;gap:8px}" +
|
|
30
|
+
".__pr_check{accent-color:var(--dsw-alias-state-business-primary)}" +
|
|
31
|
+
".__pr_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px;box-sizing:border-box;width:100%}" +
|
|
32
|
+
".__pr_inputSmall{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:3px 6px;font-size:11px;width:100px;box-sizing:border-box}" +
|
|
33
|
+
".__pr_select{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 8px;font-size:13px}" +
|
|
34
|
+
".__pr_actions{display:flex;gap:8px;align-items:center;margin-top:4px}" +
|
|
35
|
+
".__pr_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 14px;font:inherit;font-size:13px;cursor:pointer}" +
|
|
36
|
+
".__pr_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
|
|
37
|
+
".__pr_btn:disabled{opacity:.5;cursor:default}" +
|
|
38
|
+
".__pr_btnPrimary{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-on-accent)}" +
|
|
39
|
+
".__pr_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
|
|
40
|
+
".__pr_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
|
|
41
|
+
".__pr_advanced{margin-top:8px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:6px}" +
|
|
42
|
+
".__pr_advancedSummary{cursor:pointer;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary);user-select:none;display:flex;align-items:center;gap:5px}" +
|
|
43
|
+
".__pr_advancedArrow{display:inline-block;transition:transform .18s ease;font-size:13px;line-height:1;color:var(--dsw-alias-label-secondary);transform:rotate(0)}" +
|
|
44
|
+
".__pr_advanced[open] .__pr_advancedArrow{transform:rotate(90deg)}" +
|
|
45
|
+
".__pr_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}" +
|
|
46
|
+
".__pr_modelList{display:flex;flex-direction:column;gap:4px;max-height:240px;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;background:var(--dsw-alias-bg-layer-2)}" +
|
|
47
|
+
".__pr_modelRow{display:flex;align-items:center;gap:6px;padding:3px 0;font-size:12px}" +
|
|
48
|
+
".__pr_modelName{flex:1;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
49
|
+
".__pr_modelProvider{font-size:10px;color:var(--dsw-alias-label-tertiary);max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
50
|
+
".__pr_empty{font-size:12px;color:var(--dsw-alias-label-tertiary);font-style:italic;padding:8px 0}";
|
|
51
|
+
var tagId = "picturereader/main.css";
|
|
52
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
|
|
53
|
+
var tag = document.createElement("style");
|
|
54
|
+
tag.dataset.plugin = "picturereader";
|
|
55
|
+
tag.dataset.pluginCss = tagId;
|
|
56
|
+
tag.textContent = CSS;
|
|
57
|
+
document.head.appendChild(tag);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── locale ────────────────────────────────────────────────────────────
|
|
61
|
+
var NS = "picturereader";
|
|
62
|
+
var inject = ["slots", "locale", "settingsScope"];
|
|
63
|
+
var zh = {
|
|
64
|
+
nav: "图片阅读",
|
|
65
|
+
intro: "picturereader:让纯文本模型用本地工具看懂图片。选择使用模式;在下方勾选需要视觉桥的模型;配置外部视觉端点(选配)。修改后即时生效(设置项),视觉桥需重启 DSH。",
|
|
66
|
+
mode: "使用模式",
|
|
67
|
+
modePrivacy: "隐私模式",
|
|
68
|
+
modeSmart: "智能模式",
|
|
69
|
+
modeStrict: "严谨模式",
|
|
70
|
+
modeHint: "隐私=绝不调用外部 API,全走本地;智能=先简单看图再决定是否外呼(省轮数/时间);严谨=自行选择+必要时交叉验证细节。",
|
|
71
|
+
visionBridgeModels: "视觉桥:为以下模型注入视觉孪生",
|
|
72
|
+
visionBridgeModelsHint: "勾选的文本模型会在模型选择器里多一个(视觉)变体,选它即可粘贴图片显示缩略图并自动分析。(需重启 DSH 生效)",
|
|
73
|
+
visionBridgeNote: "备注",
|
|
74
|
+
visionBridgeNotePH: "视觉(可改)",
|
|
75
|
+
vlmEnabled: "启用外部视觉 API(选配)",
|
|
76
|
+
vlmEnabledHint: "勾选后才配置并允许调用外部视觉端点;不勾选一律走本地工具,图片绝不外发。",
|
|
77
|
+
vlmBase: "视觉 API Base URL",
|
|
78
|
+
vlmBasePlaceholder: "https://api.openai.com/v1(留空=禁用外部 VLM)",
|
|
79
|
+
vlmModel: "视觉模型",
|
|
80
|
+
vlmKey: "视觉 API Key",
|
|
81
|
+
vlmKeyHint: "密钥只写不读:留空保持当前值,填写并保存即覆盖,之后不再展示。",
|
|
82
|
+
vlmKeyEnv: "Key 环境变量(apiKey 为空时读取)",
|
|
83
|
+
ocr: "默认 OCR 引擎",
|
|
84
|
+
ocrWindows: "windows(系统内置,无需安装)",
|
|
85
|
+
ocrPaddle: "paddle(PaddleOCR,对发光/弯曲/游戏文字更好)",
|
|
86
|
+
ocrRapid: "rapid(RapidOCR,轻量快速,选装)",
|
|
87
|
+
advanced: "高级设置",
|
|
88
|
+
vlmTimeoutMs: "视觉请求超时(毫秒)",
|
|
89
|
+
vlmMaxTokens: "视觉最大输出 Tokens",
|
|
90
|
+
bridgeExportDir: "图片桥导出目录(空 = 系统临时目录)",
|
|
91
|
+
maxImageBytes: "单张图片大小上限(字节)",
|
|
92
|
+
scanDefaultSize: "扫描默认格子大小(8..64)",
|
|
93
|
+
scanPalette: "默认色板(auto/full/basic/gray)",
|
|
94
|
+
scanMode: "默认扫描模式(auto/ascii/color)",
|
|
95
|
+
ocrLanguage: "OCR 默认语言(BCP-47,如 zh-Hans / en-US)",
|
|
96
|
+
multimodalModels: "多模态白名单(逗号分隔,这些模型直收图片不降级)",
|
|
97
|
+
requestGuard: "请求保护(图片块降级兜底)",
|
|
98
|
+
batchProbeFirst: "批量探测前几张(判断是否文字密集)",
|
|
99
|
+
batchOcrLimitChars: "批量 OCR 截断字符数",
|
|
100
|
+
docDpi: "文档转换 DPI(72..300)",
|
|
101
|
+
docMaxPages: "文档转换最大页数(1..500)",
|
|
102
|
+
debug: "调试日志(输出诊断信息)",
|
|
103
|
+
save: "保存",
|
|
104
|
+
reset: "恢复默认",
|
|
105
|
+
saved: "已保存",
|
|
106
|
+
saving: "保存中…",
|
|
107
|
+
error: "保存失败",
|
|
108
|
+
unavailable: "设置命名空间不可用(服务端未注册 picturereader 命名空间?)",
|
|
109
|
+
loading: "加载中…",
|
|
110
|
+
noModels: "暂无可用模型(重启 DSH 后自动扫描)",
|
|
111
|
+
};
|
|
112
|
+
var en = {
|
|
113
|
+
nav: "Picture Reader",
|
|
114
|
+
intro: "picturereader: local image understanding for text-only models. Pick a usage mode; check models to give them a vision variant; configure external vision endpoint (optional). Settings hot-apply, vision bridge requires DSH restart.",
|
|
115
|
+
mode: "Usage Mode",
|
|
116
|
+
modePrivacy: "Privacy",
|
|
117
|
+
modeSmart: "Smart",
|
|
118
|
+
modeStrict: "Strict",
|
|
119
|
+
modeHint: "Privacy = never call any external API, local-only; Smart = glance first then decide whether to call out (fewer rounds/faster); Strict = self-choice + cross-validate details when needed.",
|
|
120
|
+
visionBridgeModels: "Vision bridge: inject vision twin for these models",
|
|
121
|
+
visionBridgeModelsHint: "Checked text models get a (Vision) variant in the model selector. Pick it to paste images with native thumbnails and auto-analysis. (Requires DSH restart)",
|
|
122
|
+
visionBridgeNote: "Note",
|
|
123
|
+
visionBridgeNotePH: "Vision (editable)",
|
|
124
|
+
vlmEnabled: "Enable external vision API (optional)",
|
|
125
|
+
vlmEnabledHint: "Check to configure & allow external vision calls; unchecked stays fully local (images never leave).",
|
|
126
|
+
vlmBase: "Vision API Base URL",
|
|
127
|
+
vlmBasePlaceholder: "https://api.openai.com/v1 (empty = external VLM disabled)",
|
|
128
|
+
vlmModel: "Vision Model",
|
|
129
|
+
vlmKey: "Vision API Key",
|
|
130
|
+
vlmKeyHint: "Write-only key: leave blank to keep current, fill & save to overwrite, never echoed again.",
|
|
131
|
+
vlmKeyEnv: "Key env var (used when apiKey empty)",
|
|
132
|
+
ocr: "Default OCR Engine",
|
|
133
|
+
ocrWindows: "windows (built-in, no install)",
|
|
134
|
+
ocrPaddle: "paddle (PaddleOCR, best for glowing/curved/game text)",
|
|
135
|
+
ocrRapid: "rapid (RapidOCR, lightweight, optional)",
|
|
136
|
+
advanced: "Advanced",
|
|
137
|
+
vlmTimeoutMs: "Vision request timeout (ms)",
|
|
138
|
+
vlmMaxTokens: "Vision max output tokens",
|
|
139
|
+
bridgeExportDir: "Image bridge export dir (empty = system temp)",
|
|
140
|
+
maxImageBytes: "Max image size (bytes)",
|
|
141
|
+
scanDefaultSize: "Scan default grid size (8..64)",
|
|
142
|
+
scanPalette: "Default palette (auto/full/basic/gray)",
|
|
143
|
+
scanMode: "Default scan mode (auto/ascii/color)",
|
|
144
|
+
ocrLanguage: "OCR default language (BCP-47, e.g. zh-Hans / en-US)",
|
|
145
|
+
multimodalModels: "Multimodal whitelist (comma-separated, receive images directly)",
|
|
146
|
+
requestGuard: "Request guard (image block downgrade fallback)",
|
|
147
|
+
batchProbeFirst: "Batch probe first N images",
|
|
148
|
+
batchOcrLimitChars: "Batch OCR truncation chars",
|
|
149
|
+
docDpi: "Document conversion DPI (72..300)",
|
|
150
|
+
docMaxPages: "Document conversion max pages (1..500)",
|
|
151
|
+
debug: "Debug logging",
|
|
152
|
+
save: "Save",
|
|
153
|
+
reset: "Reset",
|
|
154
|
+
saved: "Saved",
|
|
155
|
+
saving: "Saving…",
|
|
156
|
+
error: "Save failed",
|
|
157
|
+
unavailable: "Settings namespace unavailable (picturereader not registered server-side?)",
|
|
158
|
+
loading: "Loading…",
|
|
159
|
+
noModels: "No models available yet (will appear after DSH restart / model scan)",
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ── field spec ────────────────────────────────────────────────────────
|
|
163
|
+
var MODE_OPTS = [
|
|
164
|
+
{ value: "privacy", labelKey: "modePrivacy" },
|
|
165
|
+
{ value: "smart", labelKey: "modeSmart" },
|
|
166
|
+
{ value: "strict", labelKey: "modeStrict" },
|
|
167
|
+
];
|
|
168
|
+
var OCR_OPTS = [
|
|
169
|
+
{ value: "windows", labelKey: "ocrWindows" },
|
|
170
|
+
{ value: "paddle", labelKey: "ocrPaddle" },
|
|
171
|
+
{ value: "rapid", labelKey: "ocrRapid" },
|
|
172
|
+
];
|
|
173
|
+
// FIELDS: mode, vision_models (custom), vlm_enabled (checkbox), vlm_*, ocr_engine, advanced
|
|
174
|
+
var FIELDS = [
|
|
175
|
+
{ key: "mode", type: "mode" },
|
|
176
|
+
{ key: "vision_models", type: "models" },
|
|
177
|
+
{ key: "vlm_enabled", type: "checkbox", labelKey: "vlmEnabled", hintKey: "vlmEnabledHint" },
|
|
178
|
+
{ key: "vlm_base", type: "text", labelKey: "vlmBase", placeholderKey: "vlmBasePlaceholder" },
|
|
179
|
+
{ key: "vlm_model", type: "text", labelKey: "vlmModel" },
|
|
180
|
+
{ key: "vlm_key", type: "password", secret: true, labelKey: "vlmKey", hintKey: "vlmKeyHint" },
|
|
181
|
+
{ key: "vlm_key_env", type: "text", labelKey: "vlmKeyEnv" },
|
|
182
|
+
{ key: "ocr_engine", type: "ocr" },
|
|
183
|
+
{ key: "vlm_timeout_ms", type: "number", advanced: true, labelKey: "vlmTimeoutMs" },
|
|
184
|
+
{ key: "vlm_max_tokens", type: "number", advanced: true, labelKey: "vlmMaxTokens" },
|
|
185
|
+
{ key: "bridge_export_dir", type: "text", advanced: true, labelKey: "bridgeExportDir" },
|
|
186
|
+
{ key: "max_image_bytes", type: "number", advanced: true, labelKey: "maxImageBytes" },
|
|
187
|
+
{ key: "scan_default_size", type: "number", advanced: true, labelKey: "scanDefaultSize" },
|
|
188
|
+
{ key: "scan_palette", type: "text", advanced: true, labelKey: "scanPalette" },
|
|
189
|
+
{ key: "scan_mode", type: "text", advanced: true, labelKey: "scanMode" },
|
|
190
|
+
{ key: "ocr_language", type: "text", advanced: true, labelKey: "ocrLanguage" },
|
|
191
|
+
{ key: "multimodal_models", type: "text", advanced: true, labelKey: "multimodalModels" },
|
|
192
|
+
{ key: "request_guard", type: "checkbox", advanced: true, labelKey: "requestGuard" },
|
|
193
|
+
{ key: "batch_probe_first", type: "number", advanced: true, labelKey: "batchProbeFirst" },
|
|
194
|
+
{ key: "batch_ocr_limit_chars", type: "number", advanced: true, labelKey: "batchOcrLimitChars" },
|
|
195
|
+
{ key: "doc_dpi", type: "number", advanced: true, labelKey: "docDpi" },
|
|
196
|
+
{ key: "doc_max_pages", type: "number", advanced: true, labelKey: "docMaxPages" },
|
|
197
|
+
{ key: "debug", type: "checkbox", advanced: true, labelKey: "debug" },
|
|
198
|
+
];
|
|
199
|
+
var FIELD_LABELS = {
|
|
200
|
+
mode: "mode", vlm_enabled: "vlmEnabled", vlm_base: "vlmBase", vlm_model: "vlmModel", vlm_key: "vlmKey",
|
|
201
|
+
vlm_key_env: "vlmKeyEnv", ocr_engine: "ocr",
|
|
202
|
+
vlm_timeout_ms: "vlmTimeoutMs", vlm_max_tokens: "vlmMaxTokens", bridge_export_dir: "bridgeExportDir",
|
|
203
|
+
max_image_bytes: "maxImageBytes", scan_default_size: "scanDefaultSize", scan_palette: "scanPalette",
|
|
204
|
+
scan_mode: "scanMode", ocr_language: "ocrLanguage", multimodal_models: "multimodalModels",
|
|
205
|
+
request_guard: "requestGuard", batch_probe_first: "batchProbeFirst", batch_ocr_limit_chars: "batchOcrLimitChars",
|
|
206
|
+
doc_dpi: "docDpi", doc_max_pages: "docMaxPages", debug: "debug",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// ── Vision Bridge Model Picker Component ──────────────────────────────
|
|
210
|
+
function VisionBridgePicker(props) {
|
|
211
|
+
var t = props.t;
|
|
212
|
+
var scope = props.scope;
|
|
213
|
+
// available_text_models (read-only, from host scan)
|
|
214
|
+
var [available, setAvailable] = react.useState([]);
|
|
215
|
+
// vision_models (user-selected, saved to settings)
|
|
216
|
+
var [selected, setSelected] = react.useState([]);
|
|
217
|
+
|
|
218
|
+
// Load available models from host API + current selection from settings
|
|
219
|
+
react.useEffect(function () {
|
|
220
|
+
var alive = true;
|
|
221
|
+
// Fetch available text models from host API (independent of settings.yaml)
|
|
222
|
+
function fetchModels() {
|
|
223
|
+
fetch('/picturereader/models')
|
|
224
|
+
.then(function (res) { return res.ok ? res.json() : []; })
|
|
225
|
+
.then(function (data) {
|
|
226
|
+
if (alive && Array.isArray(data)) setAvailable(data);
|
|
227
|
+
})
|
|
228
|
+
.catch(function () {});
|
|
229
|
+
}
|
|
230
|
+
// Load user selection from settings
|
|
231
|
+
function loadSelection() {
|
|
232
|
+
scope.load().then(function () {
|
|
233
|
+
if (!alive) return;
|
|
234
|
+
var snap = scope.getSnapshot();
|
|
235
|
+
if (snap.status !== "ready" || !snap.value) return;
|
|
236
|
+
var sel = snap.value.vision_models;
|
|
237
|
+
if (Array.isArray(sel)) setSelected(sel);
|
|
238
|
+
}).catch(function () {});
|
|
239
|
+
}
|
|
240
|
+
fetchModels();
|
|
241
|
+
loadSelection();
|
|
242
|
+
// Poll every 5s for model list updates (host scans async)
|
|
243
|
+
var timer = setInterval(function () { fetchModels(); loadSelection(); }, 5000);
|
|
244
|
+
return function () { alive = false; clearInterval(timer); };
|
|
245
|
+
}, [scope]);
|
|
246
|
+
|
|
247
|
+
// Toggle a model on/off
|
|
248
|
+
function toggleModel(model) {
|
|
249
|
+
var idx = selected.findIndex(function (m) { return m.id === model.id && m.provider === model.provider; });
|
|
250
|
+
var next;
|
|
251
|
+
if (idx >= 0) {
|
|
252
|
+
next = selected.slice(0, idx).concat(selected.slice(idx + 1));
|
|
253
|
+
} else {
|
|
254
|
+
next = selected.concat([{ id: model.id, provider: model.provider, note: "" }]);
|
|
255
|
+
}
|
|
256
|
+
setSelected(next);
|
|
257
|
+
saveSelection(next);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Update note for a selected model
|
|
261
|
+
function setNote(model, note) {
|
|
262
|
+
var next = selected.map(function (m) {
|
|
263
|
+
if (m.id === model.id && m.provider === model.provider) return Object.assign({}, m, { note: note });
|
|
264
|
+
return m;
|
|
265
|
+
});
|
|
266
|
+
setSelected(next);
|
|
267
|
+
saveSelection(next);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function saveSelection(list) {
|
|
271
|
+
scope.set("vision_models", list).catch(function () {});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var isSelected = function (m) {
|
|
275
|
+
return selected.some(function (s) { return s.id === m.id && s.provider === m.provider; });
|
|
276
|
+
};
|
|
277
|
+
var noteOf = function (m) {
|
|
278
|
+
var entry = selected.find(function (s) { return s.id === m.id && s.provider === m.provider; });
|
|
279
|
+
return entry ? (entry.note || "") : "";
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return h("div", { className: "__pr_field" },
|
|
283
|
+
h("span", { className: "__pr_label" }, t("visionBridgeModels")),
|
|
284
|
+
h("span", { className: "__pr_hint" }, t("visionBridgeModelsHint")),
|
|
285
|
+
available.length === 0
|
|
286
|
+
? h("p", { className: "__pr_empty" }, t("noModels"))
|
|
287
|
+
: h("div", { className: "__pr_modelList" },
|
|
288
|
+
available.map(function (m) {
|
|
289
|
+
var key = m.provider + "/" + m.id;
|
|
290
|
+
var checked = isSelected(m);
|
|
291
|
+
return h("div", { key: key, className: "__pr_modelRow" },
|
|
292
|
+
h("input", {
|
|
293
|
+
className: "__pr_check",
|
|
294
|
+
type: "checkbox",
|
|
295
|
+
checked: checked,
|
|
296
|
+
onChange: function () { toggleModel(m); },
|
|
297
|
+
}),
|
|
298
|
+
h("span", { className: "__pr_modelName" }, m.name || m.id),
|
|
299
|
+
h("span", { className: "__pr_modelProvider" }, m.provider),
|
|
300
|
+
checked ? h("input", {
|
|
301
|
+
className: "__pr_inputSmall",
|
|
302
|
+
type: "text",
|
|
303
|
+
value: noteOf(m),
|
|
304
|
+
placeholder: t("visionBridgeNotePH"),
|
|
305
|
+
onChange: function (e) { setNote(m, e.target.value); },
|
|
306
|
+
}) : null
|
|
307
|
+
);
|
|
308
|
+
})
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function tOf(props) { return props.t; }
|
|
314
|
+
|
|
315
|
+
// ── Section component ─────────────────────────────────────────────────
|
|
316
|
+
function Section(props) {
|
|
317
|
+
var t = props.t;
|
|
318
|
+
var scope = props.scope;
|
|
319
|
+
var [snapshot, setSnapshot] = react.useState(function () { return scope.getSnapshot(); });
|
|
320
|
+
var ready = snapshot.status === "ready" && snapshot.value !== void 0;
|
|
321
|
+
var [draft, setDraft] = react.useState({});
|
|
322
|
+
var [busy, setBusy] = react.useState(false);
|
|
323
|
+
var [notice, setNotice] = react.useState(null);
|
|
324
|
+
var [error, setError] = react.useState(null);
|
|
325
|
+
|
|
326
|
+
react.useEffect(function () {
|
|
327
|
+
scope.load();
|
|
328
|
+
var alive = true;
|
|
329
|
+
var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
|
|
330
|
+
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
331
|
+
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
332
|
+
}, [scope]);
|
|
333
|
+
react.useEffect(function () {
|
|
334
|
+
// Fill the draft from the resolved value once, but never overwrite keys
|
|
335
|
+
// the user has already edited: the previous merge put `prev` under the
|
|
336
|
+
// fresh defaults, which could wipe in-progress edits if this effect ever
|
|
337
|
+
// re-ran after the user started changing fields.
|
|
338
|
+
if (ready) setDraft(function (prev) {
|
|
339
|
+
var base = valueToDraft(snapshot.value);
|
|
340
|
+
var merged = Object.assign({}, base);
|
|
341
|
+
for (var k in prev) merged[k] = prev[k];
|
|
342
|
+
return merged;
|
|
343
|
+
});
|
|
344
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
345
|
+
}, [ready]);
|
|
346
|
+
|
|
347
|
+
if (snapshot.status === "unavailable") {
|
|
348
|
+
return h("p", { className: "__pr_unavailable" }, t("unavailable"));
|
|
349
|
+
}
|
|
350
|
+
if (!ready) return h("p", { className: "__pr_status" }, t("loading"));
|
|
351
|
+
|
|
352
|
+
var value = snapshot.value;
|
|
353
|
+
|
|
354
|
+
function fieldDraft(f) {
|
|
355
|
+
if (f.type === "checkbox") return draft[f.key] !== void 0 ? !!draft[f.key] : Boolean(value[f.key]);
|
|
356
|
+
return draft[f.key] !== void 0 ? draft[f.key] : String(value[f.key] ?? "");
|
|
357
|
+
}
|
|
358
|
+
function setField(f, v) {
|
|
359
|
+
setDraft(function (prev) { var n = Object.assign({}, prev); n[f.key] = v; return n; });
|
|
360
|
+
setNotice(null); setError(null);
|
|
361
|
+
}
|
|
362
|
+
function fieldValue(f) {
|
|
363
|
+
// The value the form currently shows: the user's draft if touched,
|
|
364
|
+
// otherwise the current resolved value (what is on screen).
|
|
365
|
+
return draft[f.key] !== void 0 ? draft[f.key] : String(value[f.key] ?? "");
|
|
366
|
+
}
|
|
367
|
+
function onSave() {
|
|
368
|
+
setBusy(true); setNotice(null); setError(null);
|
|
369
|
+
// Persist exactly what the form shows RIGHT NOW. We deliberately do not
|
|
370
|
+
// "skip a write when the draft equals the resolved snapshot": that
|
|
371
|
+
// skip-by-equality silently dropped every field whenever the form state
|
|
372
|
+
// matched the loaded defaults (the UI still reported 已保存 while writing
|
|
373
|
+
// nothing), so all main settings reverted to defaults on reopen. Instead
|
|
374
|
+
// every non-model field is written unconditionally from its current form
|
|
375
|
+
// value; writes go through the same shared scope as vision_models, so the
|
|
376
|
+
// host applies them in order with no stale-revision drop. Secrets stay
|
|
377
|
+
// write-only (blank = keep the stored value) and an empty string is
|
|
378
|
+
// recorded as an unset so it falls back to the default.
|
|
379
|
+
var ops = [];
|
|
380
|
+
FIELDS.forEach(function (f) {
|
|
381
|
+
if (f.type === "models") return; // vision_models handled by VisionBridgePicker
|
|
382
|
+
// Resolve each field's typed form value: the draft if the user has
|
|
383
|
+
// touched it, otherwise the current resolved value. Checkboxes keep
|
|
384
|
+
// their boolean so an untouched "false" is not coerced to true.
|
|
385
|
+
if (f.type === "checkbox") {
|
|
386
|
+
ops.push({ op: "set", key: f.key, value: draft[f.key] !== void 0 ? !!draft[f.key] : Boolean(value[f.key]) });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
var dv = fieldValue(f);
|
|
390
|
+
if (f.type === "password") {
|
|
391
|
+
if (String(dv).trim() !== "") ops.push({ op: "set", key: f.key, value: String(dv).trim() });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (f.type === "number") {
|
|
395
|
+
var num = Number(dv);
|
|
396
|
+
if (Number.isFinite(num)) ops.push({ op: "set", key: f.key, value: num });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
var str = String(dv);
|
|
400
|
+
if (str.trim() === "") { ops.push({ op: "unset", key: f.key }); return; }
|
|
401
|
+
ops.push({ op: "set", key: f.key, value: str });
|
|
402
|
+
});
|
|
403
|
+
var writes = ops.map(function (o) {
|
|
404
|
+
return o.op === "set" ? scope.set(o.key, o.value) : scope.unset(o.key);
|
|
405
|
+
});
|
|
406
|
+
Promise.all(writes).then(function () {
|
|
407
|
+
setBusy(false); setNotice(t("saved"));
|
|
408
|
+
if (scope.load) scope.load();
|
|
409
|
+
}).catch(function (e) {
|
|
410
|
+
setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function onReset() {
|
|
414
|
+
setBusy(true);
|
|
415
|
+
Promise.all(FIELDS.filter(function (f) { return f.type !== "models"; }).map(function (f) { return scope.unset(f.key); })).then(function () {
|
|
416
|
+
setBusy(false); setNotice(t("saved"));
|
|
417
|
+
setTimeout(function () {
|
|
418
|
+
var fresh = scope.getSnapshot();
|
|
419
|
+
if (fresh.status === "ready" && fresh.value !== void 0) setDraft(Object.assign({}, valueToDraft(fresh.value)));
|
|
420
|
+
}, 120);
|
|
421
|
+
}).catch(function (e) { setBusy(false); setError(t("error") + ": " + String(e && e.message || e)); });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function renderField(f) {
|
|
425
|
+
// Vision models picker (custom component, not a standard field)
|
|
426
|
+
if (f.type === "models") {
|
|
427
|
+
return h(VisionBridgePicker, { key: f.key, t: t, scope: scope });
|
|
428
|
+
}
|
|
429
|
+
// VLM fields hidden unless vlm_enabled checked
|
|
430
|
+
if (f.key.indexOf("vlm_") === 0 && f.key !== "vlm_enabled") {
|
|
431
|
+
var vlmOn = draft["vlm_enabled"] !== void 0 ? !!draft["vlm_enabled"] : Boolean(value["vlm_enabled"]);
|
|
432
|
+
if (!vlmOn) return null;
|
|
433
|
+
}
|
|
434
|
+
if (f.type === "checkbox") {
|
|
435
|
+
var checked = !!fieldDraft(f);
|
|
436
|
+
return h("label", { key: f.key, className: "__pr_field" },
|
|
437
|
+
h("span", { className: "__pr_row" },
|
|
438
|
+
h("input", { className: "__pr_check", type: "checkbox", checked: checked, onChange: function (e) { setField(f, e.target.checked); } }),
|
|
439
|
+
h("span", { className: "__pr_label" }, t(FIELD_LABELS[f.key]))
|
|
440
|
+
),
|
|
441
|
+
f.hintKey ? h("span", { className: "__pr_hint" }, t(f.hintKey)) : null
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (f.type === "mode") {
|
|
445
|
+
return h("label", { key: f.key, className: "__pr_field" },
|
|
446
|
+
h("span", { className: "__pr_label" }, t("mode")),
|
|
447
|
+
h("select", {
|
|
448
|
+
className: "__pr_select",
|
|
449
|
+
value: fieldDraft(f) || "smart",
|
|
450
|
+
onChange: function (e) { setField(f, e.target.value); },
|
|
451
|
+
}, MODE_OPTS.map(function (o) {
|
|
452
|
+
return h("option", { key: o.value, value: o.value }, t(o.labelKey));
|
|
453
|
+
})),
|
|
454
|
+
h("span", { className: "__pr_hint" }, t("modeHint"))
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (f.type === "ocr") {
|
|
458
|
+
return h("label", { key: f.key, className: "__pr_field" },
|
|
459
|
+
h("span", { className: "__pr_label" }, t("ocr")),
|
|
460
|
+
h("select", {
|
|
461
|
+
className: "__pr_select",
|
|
462
|
+
value: fieldDraft(f) || "windows",
|
|
463
|
+
onChange: function (e) { setField(f, e.target.value); },
|
|
464
|
+
}, OCR_OPTS.map(function (o) {
|
|
465
|
+
return h("option", { key: o.value, value: o.value }, t(o.labelKey));
|
|
466
|
+
})),
|
|
467
|
+
h("span", { className: "__pr_hint" }, null)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return h("label", { key: f.key, className: "__pr_field" },
|
|
471
|
+
h("span", { className: "__pr_label" }, t(FIELD_LABELS[f.key])),
|
|
472
|
+
h("input", {
|
|
473
|
+
className: "__pr_input",
|
|
474
|
+
type: f.type === "password" ? "password" : f.type === "number" ? "number" : "text",
|
|
475
|
+
value: fieldDraft(f),
|
|
476
|
+
placeholder: f.placeholderKey ? t(f.placeholderKey) : (f.type === "password" ? (value[f.key] ? "••••••••" : t("vlmKeyHint")) : ""),
|
|
477
|
+
onChange: function (e) { setField(f, e.target.value); },
|
|
478
|
+
}),
|
|
479
|
+
f.hintKey ? h("span", { className: "__pr_hint" }, t(f.hintKey)) : null
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
var primary = FIELDS.filter(function (f) { return !f.advanced; });
|
|
484
|
+
var advanced = FIELDS.filter(function (f) { return f.advanced; });
|
|
485
|
+
return h("div", { className: "__pr_root" },
|
|
486
|
+
h("p", { className: "__pr_hint", style: { margin: "0 0 4px" } }, t("intro")),
|
|
487
|
+
primary.map(renderField),
|
|
488
|
+
advanced.length ? h("details", { className: "__pr_advanced" },
|
|
489
|
+
h("summary", { className: "__pr_advancedSummary" },
|
|
490
|
+
h("span", null, t("advanced")),
|
|
491
|
+
h("span", { className: "__pr_advancedArrow" }, "\u25b8")
|
|
492
|
+
),
|
|
493
|
+
advanced.map(renderField)
|
|
494
|
+
) : null,
|
|
495
|
+
h("div", { className: "__pr_actions" },
|
|
496
|
+
h("button", { type: "button", className: "__pr_btn __pr_btnPrimary", onClick: onSave, disabled: busy || !snapshot.writable }, t("save")),
|
|
497
|
+
h("button", { type: "button", className: "__pr_btn", onClick: onReset, disabled: busy || !snapshot.writable }, t("reset")),
|
|
498
|
+
notice ? h("span", { className: "__pr_status" }, notice) : null,
|
|
499
|
+
busy ? h("span", { className: "__pr_status" }, t("saving")) : null,
|
|
500
|
+
error ? h("span", { className: "__pr_error" }, error) : null
|
|
501
|
+
)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function valueToDraft(value) {
|
|
506
|
+
var out = {};
|
|
507
|
+
for (var i = 0; i < FIELDS.length; i += 1) {
|
|
508
|
+
var ft = FIELDS[i];
|
|
509
|
+
if (ft.type === "models") continue; // handled separately
|
|
510
|
+
out[ft.key] = ft.type === "checkbox" ? Boolean(value[ft.key]) : String(value[ft.key] ?? "");
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function apply(ctx) {
|
|
516
|
+
var t = ctx.locale.bind(NS);
|
|
517
|
+
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "picturereader: dictionaries");
|
|
518
|
+
var scope = ctx.settingsScope.bind({ namespace: NS });
|
|
519
|
+
ctx.slots.inject("settings.section", function () {
|
|
520
|
+
return ctx.slots.register({
|
|
521
|
+
name: "settings.section",
|
|
522
|
+
id: "picturereader",
|
|
523
|
+
order: 30,
|
|
524
|
+
label: function () { return t("nav"); },
|
|
525
|
+
locale: NS,
|
|
526
|
+
}, function (props) {
|
|
527
|
+
return h(Section, Object.assign({}, props, { scope: scope }));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
exports.apply = apply;
|
|
533
|
+
exports.inject = inject;
|
|
534
|
+
return module.exports;
|
|
535
|
+
},
|
|
536
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picturereader",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Unified image understanding plugin for DeepSeek Harness (DSH).
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Unified image understanding plugin for DeepSeek Harness (DSH). Visual twin adapter for native thumbnails + auto-analysis on any text-only model (incl. pi-ai providers); privacy/smart/strict routing; local tools (scan/OCR×3 engines/crop/palette/compare/batch); document-to-image (pdf/word/excel/ppt); optional external VLM bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
9
10
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
10
11
|
"./package.json": "./package.json"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src",
|
|
14
15
|
"scripts",
|
|
16
|
+
"client.js",
|
|
15
17
|
"cordis.patch.yml",
|
|
16
18
|
"skills",
|
|
17
19
|
"README.md",
|
|
@@ -30,11 +32,13 @@
|
|
|
30
32
|
"ascii",
|
|
31
33
|
"ocr",
|
|
32
34
|
"paddleocr",
|
|
35
|
+
"rapidocr",
|
|
33
36
|
"hue",
|
|
34
37
|
"multimodal",
|
|
35
38
|
"vlm",
|
|
36
39
|
"vision-analyze",
|
|
37
|
-
"llm-vision"
|
|
40
|
+
"llm-vision",
|
|
41
|
+
"document-to-image"
|
|
38
42
|
],
|
|
39
43
|
"license": "MIT",
|
|
40
44
|
"author": "picturereader",
|
|
@@ -44,8 +48,16 @@
|
|
|
44
48
|
"dsh": {
|
|
45
49
|
"bundle": {
|
|
46
50
|
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"platform": "web"
|
|
47
54
|
}
|
|
48
55
|
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
59
|
+
"@deepseek-ai/dsh-llm": "*"
|
|
60
|
+
},
|
|
49
61
|
"dependencies": {
|
|
50
62
|
"jpeg-js": "^0.4.4",
|
|
51
63
|
"omggif": "^1.0.10",
|