picturereader 2.0.1 → 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/vlm.js CHANGED
@@ -10,16 +10,153 @@
10
10
  */
11
11
 
12
12
  import { spawn } from 'node:child_process';
13
- import { stat } from 'node:fs/promises';
13
+ import { stat, readFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import { getRuntimeConfig } from './runtime.js';
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
- // Configuration (all via environment variables, defaults are empty/disabled)
19
+ // Configuration (environment variables first, then DSH settings.yaml fallback)
17
20
  // ---------------------------------------------------------------------------
18
21
 
22
+ /**
23
+ * Try to read DSH settings.yaml and extract tool-vision config.
24
+ * @returns {Promise<{baseURL: string, model: string, apiKey: string}|null>}
25
+ */
26
+ async function readDshToolVisionConfig() {
27
+ try {
28
+ const settingsPath = join(homedir(), '.dsh', 'settings.yaml');
29
+ const content = await readFile(settingsPath, 'utf-8');
30
+ // Simple YAML parsing for tool-vision section
31
+ const lines = content.split('\n');
32
+ let inToolVision = false;
33
+ let baseURL = '';
34
+ let model = '';
35
+ let apiKey = '';
36
+ let indent = -1;
37
+
38
+ for (const line of lines) {
39
+ // Detect start of tool-vision section
40
+ if (line.match(/^tool-vision:\s*$/)) {
41
+ inToolVision = true;
42
+ indent = -1;
43
+ continue;
44
+ }
45
+
46
+ if (inToolVision) {
47
+ // Check if we've left the section (non-empty line with less or equal indent)
48
+ const match = line.match(/^(\s*)\S/);
49
+ if (match) {
50
+ const currentIndent = match[1].length;
51
+ if (indent === -1) {
52
+ indent = currentIndent;
53
+ } else if (currentIndent <= 0) {
54
+ // New top-level key, exit tool-vision section
55
+ break;
56
+ }
57
+ }
58
+
59
+ // Parse key-value pairs
60
+ const kvMatch = line.match(/^\s+(\w+):\s*(.+)$/);
61
+ if (kvMatch) {
62
+ const [, key, value] = kvMatch;
63
+ const cleanValue = value.replace(/^["']|["']$/g, '').trim();
64
+ if (key === 'baseURL') baseURL = cleanValue;
65
+ else if (key === 'model') model = cleanValue;
66
+ else if (key === 'apiKey') apiKey = cleanValue;
67
+ }
68
+ }
69
+ }
70
+
71
+ if (baseURL) {
72
+ return { baseURL, model, apiKey };
73
+ }
74
+ } catch {
75
+ // Settings file not found or parse error, ignore
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Read DSH settings.yaml and extract the `picturereader` namespace's VLM
82
+ * fields (vlm_base / vlm_model / vlm_key). These come from the plugin's own
83
+ * settings card and take priority over the legacy tool-vision namespace,
84
+ * so the user's configured endpoint is honoured even when the plugin packs
85
+ * a newer schema.
86
+ * @returns {Promise<{baseURL: string, model: string, apiKey: string}|null>}
87
+ */
88
+ async function readDshPicturereaderConfig() {
89
+ try {
90
+ const settingsPath = join(homedir(), '.dsh', 'settings.yaml');
91
+ const content = await readFile(settingsPath, 'utf-8');
92
+ const lines = content.split('\n');
93
+ let inNs = false;
94
+ let baseURL = '';
95
+ let model = '';
96
+ let apiKey = '';
97
+ let indent = -1;
98
+ const KEY_MAP = { vlm_base: 'baseURL', vlm_model: 'model', vlm_key: 'apiKey' };
99
+
100
+ for (const line of lines) {
101
+ if (line.match(/^picturereader:\s*$/)) {
102
+ inNs = true;
103
+ indent = -1;
104
+ continue;
105
+ }
106
+ if (inNs) {
107
+ const match = line.match(/^(\s*)\S/);
108
+ if (match) {
109
+ const currentIndent = match[1].length;
110
+ if (indent === -1) {
111
+ indent = currentIndent;
112
+ } else if (currentIndent <= 0) {
113
+ break;
114
+ }
115
+ }
116
+ const kvMatch = line.match(/^\s+(\w+):\s*(.+)$/);
117
+ if (kvMatch) {
118
+ const [, key, value] = kvMatch;
119
+ const target = KEY_MAP[key];
120
+ const cleanValue = value.replace(/^["']|["']$/g, '').trim();
121
+ if (target === 'baseURL') baseURL = cleanValue;
122
+ else if (target === 'model') model = cleanValue;
123
+ else if (target === 'apiKey') apiKey = cleanValue;
124
+ }
125
+ }
126
+ }
127
+ if (baseURL) return { baseURL, model, apiKey };
128
+ } catch {
129
+ // ignore parse errors
130
+ }
131
+ return null;
132
+ }
133
+
134
+ // Read config: priority env → picturereader namespace → legacy tool-vision namespace
135
+ const dshConfig = await readDshToolVisionConfig();
136
+ const dshPictConfig = await readDshPicturereaderConfig();
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // GLM-4V-Flash: free built-in vision model from Zhipu AI
140
+ // https://open.bigmodel.cn — register to get your API key
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /** GLM-4V-Flash default endpoint (OpenAI-compatible). */
144
+ const GLM4V_BASE = 'https://open.bigmodel.cn/api/paas/v4';
145
+ /** GLM-4V-Flash model name. */
146
+ const GLM4V_MODEL = 'glm-4v-flash';
147
+
148
+ /**
149
+ * Resolve the GLM-4V-Flash API key.
150
+ * Priority: env SEE_API_KEY > env GLM_API_KEY > DSH settings.yaml apiKey > empty.
151
+ */
152
+ function resolveGlm4vKey() {
153
+ return process.env.SEE_API_KEY ?? process.env.GLM_API_KEY ?? dshConfig?.apiKey ?? '';
154
+ }
155
+
19
156
  /** OpenAI-compatible VLM endpoint (empty = VLM disabled). */
20
- export const DEFAULT_BASE = process.env.SEE_BASE ?? '';
157
+ export const DEFAULT_BASE = process.env.SEE_BASE ?? dshPictConfig?.baseURL ?? dshConfig?.baseURL ?? GLM4V_BASE;
21
158
  /** VLM model name. */
22
- export const DEFAULT_MODEL = process.env.SEE_MODEL ?? '';
159
+ export const DEFAULT_MODEL = process.env.SEE_MODEL ?? dshPictConfig?.model ?? dshConfig?.model ?? GLM4V_MODEL;
23
160
  /** Local llama-server executable path. */
24
161
  export const DEFAULT_SERVER_EXE = process.env.SEE_SERVER_EXE ?? '';
25
162
  /** Local model GGUF path. */
@@ -32,18 +169,53 @@ export const DEFAULT_PORT = Number(process.env.SEE_SERVER_PORT ?? 8080);
32
169
  export const DEFAULT_NGL = process.env.SEE_SERVER_NGL ?? '20';
33
170
  /** Context size for local server. */
34
171
  export const DEFAULT_CTX = Number(process.env.SEE_SERVER_CTX ?? 16384);
35
- /** API key for remote endpoints. */
36
- export const DEFAULT_API_KEY = process.env.SEE_API_KEY ?? '';
172
+ /** API key for remote endpoints (env > picturereader ns > legacy tool-vision ns > GLM). */
173
+ export const DEFAULT_API_KEY =
174
+ process.env.SEE_API_KEY ?? dshPictConfig?.apiKey ?? dshConfig?.apiKey ?? process.env.GLM_API_KEY ?? '';
37
175
 
38
176
  let serverStartPromise = null;
39
177
  let serverChild = null;
40
178
 
41
179
  /**
42
- * Check if VLM is configured (has a base URL).
43
- * @returns {boolean} true when VLM endpoint is configured.
180
+ * Effective API key: runtime explicit key runtime env var → static default.
181
+ * @param {object} rt - runtime config (may be undefined).
182
+ * @returns {string}
183
+ */
184
+ function effectiveApiKey(rt) {
185
+ if (rt?.vlm?.apiKey) return rt.vlm.apiKey;
186
+ const envName = rt?.vlm?.apiKeyEnv;
187
+ if (envName) {
188
+ const fromEnv = process.env[envName];
189
+ if (fromEnv) return fromEnv;
190
+ }
191
+ return DEFAULT_API_KEY;
192
+ }
193
+
194
+ /**
195
+ * Effective endpoint base URL: runtime explicit → static default.
196
+ * @param {object} rt
197
+ * @returns {string}
198
+ */
199
+ function effectiveBase(rt) {
200
+ return (rt?.vlm?.baseUrl || '').trim() || DEFAULT_BASE;
201
+ }
202
+
203
+ /**
204
+ * Check if VLM is configured (has a base URL and API key for cloud endpoints).
205
+ * 隐私模式(privacy)为硬门禁:即使配置了外部 API 也返回 false,绝不外呼。
206
+ * @returns {boolean} true when VLM endpoint is configured and ready to use.
44
207
  */
45
208
  export function isVlmConfigured() {
46
- return DEFAULT_BASE.length > 0;
209
+ const rt = getRuntimeConfig();
210
+ if (rt?.mode === 'privacy') return false;
211
+ // 选配(vlm_enabled)未勾选:即使配置了端点/Key 也视为未启用外部 VLM。
212
+ if (rt?.vlm?.enabled === false) return false;
213
+ const base = effectiveBase(rt);
214
+ if (base.length === 0) return false;
215
+ // Cloud endpoints require an API key
216
+ const isLocal = isManagedEndpoint(base, DEFAULT_PORT);
217
+ if (!isLocal && effectiveApiKey(rt).length === 0) return false;
218
+ return true;
47
219
  }
48
220
 
49
221
  /**
@@ -224,7 +396,17 @@ export async function sendVisionRequest(config, images, prompt) {
224
396
  headers.authorization = `Bearer ${config.apiKey}`;
225
397
  }
226
398
 
227
- const res = await fetch(`${config.baseURL}/chat/completions`, {
399
+ const base = String(config.baseURL || '').trim().replace(/\/+$/, '');
400
+ let endpoint;
401
+ if (/\/v\d+\/chat\/completions$/.test(base) || /\/chat\/completions$/.test(base)) {
402
+ endpoint = base;
403
+ } else if (/\/v\d+$/.test(base)) {
404
+ endpoint = `${base}/chat/completions`;
405
+ } else {
406
+ // OpenAI 兼容端点统一用 /v1/chat/completions(LM Studio / llama-server / 云端网关等)
407
+ endpoint = `${base}/v1/chat/completions`;
408
+ }
409
+ const res = await fetch(endpoint, {
228
410
  method: 'POST',
229
411
  headers,
230
412
  body: JSON.stringify(body),
@@ -250,10 +432,11 @@ export async function sendVisionRequest(config, images, prompt) {
250
432
  * @returns {object} VLM configuration.
251
433
  */
252
434
  export function defaultVlmConfig(overrides = {}) {
435
+ const rt = getRuntimeConfig();
253
436
  return {
254
- baseURL: DEFAULT_BASE,
255
- apiKey: DEFAULT_API_KEY,
256
- model: DEFAULT_MODEL,
437
+ baseURL: effectiveBase(rt),
438
+ apiKey: effectiveApiKey(rt),
439
+ model: (rt?.vlm?.model || '').trim() || DEFAULT_MODEL,
257
440
  serverExe: DEFAULT_SERVER_EXE,
258
441
  serverModel: DEFAULT_SERVER_MODEL,
259
442
  serverMmproj: DEFAULT_SERVER_MMPROJ,
@@ -262,8 +445,8 @@ export function defaultVlmConfig(overrides = {}) {
262
445
  ctxSize: DEFAULT_CTX,
263
446
  autoStart: true,
264
447
  healthTimeoutMs: 120_000,
265
- requestTimeoutMs: 300_000,
266
- maxTokens: 8192,
448
+ requestTimeoutMs: rt?.vlm?.requestTimeoutMs ?? 300_000,
449
+ maxTokens: rt?.vlm?.maxTokens ?? 8192,
267
450
  ...overrides,
268
451
  };
269
452
  }