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/src/index.js CHANGED
@@ -1,32 +1,258 @@
1
- /**
2
- * picturereader — pixel-to-text image reading for text-only DeepSeek Harness
3
- * models. One plugin row registers the `image_scan` tool: decode the image,
4
- * downscale it into a coarse cell grid, quantize colors against a small named
5
- * palette, and feed the rendered grids back into the conversation so DeepSeek
6
- * can describe layout, colors and rough shapes without a vision model.
7
- *
8
- * Mount with one row:
9
- *
10
- * ```yaml
11
- * - id: picturereader
12
- * name: 'picturereader'
13
- * ```
14
- * @module picturereader
15
- */
16
-
17
- import { createImageScanTool, createImageOcrTool, createImageSampleTool } from './tool.js';
18
- import { createVisionAnalyzeTool } from './vision-analyze.js';
19
-
20
- export const name = 'picturereader';
21
-
22
- /** Services required at runtime: the tool registry and the filesystem seam. */
23
- export const inject = ['tools', 'fs'];
24
-
25
- export function apply(ctx) {
26
- ctx.effect(() => {
27
- ctx.tools.register(createImageScanTool(ctx));
28
- ctx.tools.register(createImageOcrTool(ctx));
29
- ctx.tools.register(createImageSampleTool(ctx));
30
- ctx.tools.register(createVisionAnalyzeTool(ctx));
31
- });
32
- }
1
+ /**
2
+ * picturereader — pixel-to-text image reading for text-only DeepSeek Harness
3
+ * models.
4
+ *
5
+ * One plugin row registers a full local image-understanding toolset plus an
6
+ * optional external vision API bridge, governed by the user's chosen usage
7
+ * mode(设置页"图片阅读"卡片):
8
+ *
9
+ * - 隐私模式(privacy):绝不调用外部 API,全走本地工具。
10
+ * - 智能模式(smart):先简单看图再决定是否外呼,省轮数/时间。
11
+ * - 严谨模式(strict):自行选择 + 必要时交叉验证细节。
12
+ *
13
+ * Tools registered:
14
+ * image_scan / image_ocr / image_sample — 本地像素理解(原有)
15
+ * image_crop / image_palette / image_compare — 本地工具链扩充
16
+ * image_batch — 批量规模/上下文验证
17
+ * vision_analyze — 统一图像理解(按模式路由)
18
+ * document_to_image — 文档(pdf/word/excel/ppt)转图片
19
+ *
20
+ * Settings: host 侧把 `picturereader` 命名空间写入 DSH settings.yaml;client.js
21
+ * 在 Web 设置页注册"图片阅读"卡片。mode / VLM 端点热加载。
22
+ *
23
+ * @module picturereader
24
+ */
25
+
26
+ import { createImageScanTool, createImageOcrTool, createImageSampleTool } from './tool.js';
27
+ import { createVisionAnalyzeTool } from './vision-analyze.js';
28
+ import { registerMoreTools } from './more-tools.js';
29
+ import { createImageBatchTool } from './image-batch.js';
30
+ import { createDocumentToImageTool } from './doc-tools.js';
31
+ import { NS } from './config.js';
32
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
33
+ import z from '@deepseek-ai/schemastery';
34
+ import { ensureSettingsNamespaceExposed } from './settings-expose.js';
35
+ import { setRuntimeSource } from './runtime.js';
36
+ import { attachImageBridge } from './bridge.js';
37
+ import { registerTwinAdapters } from './picturereader-vision.mjs';
38
+ import { writeFile, readFile, mkdir } from 'node:fs/promises';
39
+ import { join } from 'node:path';
40
+ import { homedir } from 'node:os';
41
+
42
+ /** 扫描结果存储路径(独立文件,不干扰 settings.yaml 的用户配置)。 */
43
+ const MODELS_CACHE = join(homedir(), '.dsh', 'picturereader-models.json');
44
+
45
+ export const name = 'picturereader';
46
+
47
+ /** 设置命名空间的运行时 schema(schemastery)。 */
48
+ const Config = z.object({
49
+ mode: z
50
+ .string()
51
+ .default('smart')
52
+ .description('使用模式:privacy 隐私 / smart 智能 / strict 严谨'),
53
+ vlm_enabled: z
54
+ .boolean()
55
+ .default(false)
56
+ .description('选配:是否启用外部视觉 API。勾选后才显示并允许调用外部视觉端点;未勾选一律走本地'),
57
+ vision_bridge_enabled: z
58
+ .boolean()
59
+ .default(false)
60
+ .description('(已废弃,改用 vision_models)'),
61
+ vision_models: z
62
+ .array(z.object({
63
+ id: z.string(),
64
+ provider: z.string().default(''),
65
+ note: z.string().default(''),
66
+ }))
67
+ .default([])
68
+ .description('视觉桥模型列表:被勾选的文本模型会生成「(视觉)」变体'),
69
+ vlm_base: z
70
+ .string()
71
+ .default('')
72
+ .description('OpenAI 兼容视觉端点 URL(如 https://api.openai.com/v1;空=禁用外部 VLM)'),
73
+ vlm_model: z.string().default('gpt-4o-mini').description('视觉模型名'),
74
+ vlm_key: z.string().default('').role('secret').description('视觉 API key(只写不读,不会回显)'),
75
+ vlm_key_env: z
76
+ .string()
77
+ .default('')
78
+ .description('环境变量名(vlm_key 为空时回退读取,如 VISUAL_API_KEY)'),
79
+ ocr_engine: z
80
+ .string()
81
+ .default('windows')
82
+ .description('默认 OCR 引擎:windows / paddle / rapid'),
83
+ vlm_timeout_ms: z
84
+ .number()
85
+ .default(300000)
86
+ .description('高级:外部视觉请求超时(毫秒)'),
87
+ vlm_max_tokens: z
88
+ .number()
89
+ .default(8192)
90
+ .description('高级:外部视觉最大输出 Tokens'),
91
+ bridge_export_dir: z
92
+ .string()
93
+ .default('')
94
+ .description('高级:图片桥导出目录(空=系统临时目录)'),
95
+ max_image_bytes: z
96
+ .number()
97
+ .default(52428800)
98
+ .description('高级:单张图片大小上限(字节,默认50MB)'),
99
+ scan_default_size: z
100
+ .number()
101
+ .default(32)
102
+ .description('高级:image_scan 默认格子大小(8..64)'),
103
+ scan_palette: z
104
+ .string()
105
+ .default('auto')
106
+ .description('高级:image_scan 默认色板(auto/full/basic/gray)'),
107
+ scan_mode: z
108
+ .string()
109
+ .default('auto')
110
+ .description('高级:image_scan 默认模式(auto/ascii/color)'),
111
+ ocr_language: z
112
+ .string()
113
+ .default('')
114
+ .description('高级:OCR 默认语言(BCP-47,如 zh-Hans / en-US)'),
115
+ multimodal_models: z
116
+ .string()
117
+ .default('')
118
+ .description('高级:多模态白名单(逗号分隔,这些模型直收图片不降级)'),
119
+ request_guard: z
120
+ .boolean()
121
+ .default(true)
122
+ .description('高级:请求保护(llm/stream 最后防线降级 image block)'),
123
+ batch_probe_first: z
124
+ .number()
125
+ .default(3)
126
+ .description('高级:image_batch 探测前几张(判断是否文字密集)'),
127
+ batch_ocr_limit_chars: z
128
+ .number()
129
+ .default(800)
130
+ .description('高级:image_batch 每张 OCR 截断字符数'),
131
+ doc_dpi: z
132
+ .number()
133
+ .default(150)
134
+ .description('高级:document_to_image 渲染 DPI(72..300)'),
135
+ doc_max_pages: z
136
+ .number()
137
+ .default(50)
138
+ .description('高级:document_to_image 最大页数(1..500)'),
139
+ debug: z
140
+ .boolean()
141
+ .default(false)
142
+ .description('高级:调试日志'),
143
+ });
144
+
145
+ /** Services required at runtime. */
146
+ export const inject = ['tools', 'fs', 'llm', 'attachments'];
147
+
148
+ export function apply(ctx, config) {
149
+ // ── 把命名空间加进 dsh-host-apiproxy 白名单 ──
150
+ try {
151
+ ensureSettingsNamespaceExposed(ctx, NS, ctx.logger);
152
+ } catch (error) {
153
+ ctx.logger?.warn?.(`[picturereader] settings-expose failed: ${String(error)}`);
154
+ }
155
+
156
+ // ── 运行时快照:工具执行时惰性读最新 mode / VLM 配置 ──
157
+ let sourceGetter = null;
158
+ const getConfig = () => (sourceGetter ? sourceGetter() : config);
159
+ setRuntimeSource(getConfig);
160
+
161
+ // ── 注册工具(不需要 settings/llm 服务)──
162
+ ctx.effect(() => {
163
+ ctx.tools.register(createImageScanTool(ctx));
164
+ ctx.tools.register(createImageOcrTool(ctx));
165
+ ctx.tools.register(createImageSampleTool(ctx));
166
+ ctx.tools.register(createVisionAnalyzeTool(ctx));
167
+ registerMoreTools(ctx);
168
+ ctx.tools.register(createImageBatchTool(ctx));
169
+ ctx.tools.register(createDocumentToImageTool(ctx));
170
+ });
171
+
172
+ // ── 注册模型列表 API 路由(供 client 设置卡读取扫描结果)──
173
+ try {
174
+ ctx.inject(['webServer'], (sctx) => {
175
+ const webServer = sctx.webServer;
176
+ if (!webServer || typeof webServer.register !== 'function') return;
177
+ const handler = async (req, res) => {
178
+ try {
179
+ const data = await readFile(MODELS_CACHE, 'utf-8');
180
+ console.log('[picturereader] models route: read', data.length, 'bytes from', MODELS_CACHE);
181
+ res.writeHead(200, { 'content-type': 'application/json' });
182
+ res.end(data);
183
+ } catch (err) {
184
+ console.log('[picturereader] models route: read failed:', String(err));
185
+ res.writeHead(200, { 'content-type': 'application/json' });
186
+ res.end('[]');
187
+ }
188
+ };
189
+ ctx.effect(() => webServer.register({ kind: 'exact', path: '/picturereader/models', handler }), 'picturereader: models route');
190
+ });
191
+ } catch {}
192
+
193
+ // ── 图片桥:等 attachments 服务就绪后再注册(读图需要它)──
194
+ try {
195
+ ctx.inject(['attachments'], (sctx) => {
196
+ attachImageBridge(ctx);
197
+ });
198
+ } catch (error) {
199
+ ctx.logger?.warn?.(`[picturereader] image bridge disabled: ${String(error)}`);
200
+ }
201
+
202
+ // ── 设置命名空间 + 模型扫描 + 视觉孪生路由(需要 settings 和 llm 服务)──
203
+ ctx.inject(['settings', 'llm'], (sctx) => {
204
+ const llm = sctx.llm;
205
+ const settingsNs = settingsNamespace(NS);
206
+ const scope = sctx.settings.register(settingsNs, Config, { base: config });
207
+ sourceGetter = () => scope.get();
208
+ scope.watch(() => { /* 触发热更 */ });
209
+
210
+ // ── 扫描所有 provider 的文本模型 → 写入 available_text_models ──
211
+ (async () => {
212
+ try {
213
+ if (!llm || typeof llm.listProviders !== 'function') {
214
+ return;
215
+ }
216
+ const providers = llm.listProviders();
217
+ const textModels = [];
218
+ for (const p of providers) {
219
+ try {
220
+ const models = await llm.listModels(p.id);
221
+ for (const m of models) {
222
+ const mods = m.inputModalities || [];
223
+ if (!mods.includes('image')) {
224
+ textModels.push({ provider: p.id, id: m.id, name: m.name || m.id });
225
+ }
226
+ }
227
+ } catch { /* 跳过 */ }
228
+ }
229
+ // 兜底:把用户已勾选的模型并入列表(即使某 provider 的模型扫描漏了,
230
+ // 只要在 vision_models 里就应显示+打钩,与孪生保持一致)。
231
+ try {
232
+ const cfg = scope.get();
233
+ const vms = Array.isArray(cfg?.vision_models) ? cfg.vision_models : [];
234
+ for (const entry of vms) {
235
+ const id = typeof entry === 'string' ? entry : entry?.id;
236
+ const provider = typeof entry === 'object' ? (entry?.provider || '') : '';
237
+ if (!id) continue;
238
+ const exists = textModels.some((t) => t.provider === provider && t.id === id);
239
+ if (!exists) textModels.push({ provider, id, name: id });
240
+ }
241
+ } catch { /* 兜底失败忽略 */ }
242
+ if (textModels.length > 0) {
243
+ await mkdir(join(MODELS_CACHE, '..'), { recursive: true });
244
+ await writeFile(MODELS_CACHE, JSON.stringify(textModels, null, 2));
245
+ }
246
+ } catch {
247
+ // 模型扫描失败静默
248
+ }
249
+ })();
250
+
251
+ // ── 视觉孪生:包裹被勾选模型所属 provider 的 adapter,声明支持图片 + stream 拦截图片 ──
252
+ try {
253
+ registerTwinAdapters(ctx, llm, getConfig);
254
+ } catch (e) {
255
+ ctx.logger?.warn?.(`[picturereader] twin adapters failed: ${String(e?.message || e)}`);
256
+ }
257
+ });
258
+ }