@anionex/dsh-vision-toolkit 0.1.38 → 0.1.40
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.i18n.yaml +2 -2
- package/README.md +21 -42
- package/README.zh.md +20 -41
- package/assets/logo_eapi_dark.png +0 -0
- package/docs/aihubmix-gemini-vision.i18n.yaml +2 -2
- package/docs/aihubmix-gemini-vision.md +2 -2
- package/docs/aihubmix-gemini-vision.zh.md +2 -2
- package/docs/requirements-traceability/README.i18n.yaml +2 -2
- package/docs/requirements-traceability/README.md +1 -1
- package/docs/requirements-traceability/README.zh.md +1 -1
- package/lib/artifact-access.js +20 -2
- package/lib/artifact-access.js.map +1 -1
- package/lib/client.js +11 -3
- package/lib/client.js.map +1 -1
- package/lib/config.js +46 -2
- package/lib/config.js.map +1 -1
- package/lib/evidence-cache.js +2 -1
- package/lib/evidence-cache.js.map +1 -1
- package/lib/image-input-variants.js +61 -26
- package/lib/image-input-variants.js.map +1 -1
- package/lib/index.js +53 -6
- package/lib/index.js.map +1 -1
- package/lib/paste-images.js +67 -19
- package/lib/paste-images.js.map +1 -1
- package/lib/paths.js +214 -28
- package/lib/paths.js.map +1 -1
- package/lib/runtime-install.js +204 -19
- package/lib/runtime-install.js.map +1 -1
- package/lib/runtime-manager.js +76 -10
- package/lib/runtime-manager.js.map +1 -1
- package/lib/runtime.js +16 -10
- package/lib/runtime.js.map +1 -1
- package/lib/storage-history.js +154 -0
- package/lib/storage-history.js.map +1 -0
- package/lib/types/artifact-access.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +6 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +24 -1
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/evidence-cache.d.ts +1 -1
- package/lib/types/evidence-cache.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +7 -4
- package/lib/types/image-input-variants.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/paste-images.d.ts +12 -4
- package/lib/types/paste-images.d.ts.map +1 -1
- package/lib/types/paths.d.ts +31 -5
- package/lib/types/paths.d.ts.map +1 -1
- package/lib/types/runtime-install.d.ts +21 -0
- package/lib/types/runtime-install.d.ts.map +1 -1
- package/lib/types/runtime-manager.d.ts +28 -4
- package/lib/types/runtime-manager.d.ts.map +1 -1
- package/lib/types/runtime.d.ts +4 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/storage-history.d.ts +63 -0
- package/lib/types/storage-history.d.ts.map +1 -0
- package/lib/types/upstream.d.ts.map +1 -1
- package/lib/types/web.d.ts.map +1 -1
- package/lib/upstream.js +10 -0
- package/lib/upstream.js.map +1 -1
- package/lib/web.js +9 -3
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/src/artifact-access.ts +22 -2
- package/src/client/index.tsx +18 -3
- package/src/config.ts +76 -2
- package/src/evidence-cache.ts +2 -1
- package/src/image-input-variants.ts +61 -20
- package/src/index.ts +65 -6
- package/src/paste-images.ts +81 -19
- package/src/paths.ts +249 -28
- package/src/runtime-install.ts +231 -20
- package/src/runtime-manager.ts +93 -10
- package/src/runtime.ts +15 -9
- package/src/storage-history.ts +172 -0
- package/src/upstream.ts +10 -0
- package/src/web.ts +9 -2
package/src/client/index.tsx
CHANGED
|
@@ -67,6 +67,9 @@ const en = {
|
|
|
67
67
|
runtimeMode: 'Runtime mode',
|
|
68
68
|
toolkitPath: 'Pinned checkout path',
|
|
69
69
|
python: 'Python override',
|
|
70
|
+
storage: 'Local files',
|
|
71
|
+
storageDir: 'Default save directory',
|
|
72
|
+
storageDirHint: 'Leave blank to keep .dsh-vision-toolkit in each workspace. On POSIX systems, set an absolute shared root such as /tmp/dsh-vision-toolkit to store artifacts, pasted images, and caches in an automatically generated per-user, workspace-specific child directory. Windows shared roots remain disabled until their ACLs can be verified safely.',
|
|
70
73
|
allowedDirs: 'Additional allowed directories',
|
|
71
74
|
allowedDirsHint: 'One path per line. The session workspace is always allowed.',
|
|
72
75
|
save: 'Save and apply',
|
|
@@ -89,7 +92,7 @@ const en = {
|
|
|
89
92
|
connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
|
|
90
93
|
saveBeforeTesting: 'Save service changes before testing the connection.',
|
|
91
94
|
advanced: 'Advanced settings',
|
|
92
|
-
advancedHint: 'Credential name, provider compatibility, output language, resource limits, runtime source, Python, and additional readable directories.',
|
|
95
|
+
advancedHint: 'Credential name, provider compatibility, output language, resource limits, default save directory, runtime source, Python, and additional readable directories.',
|
|
93
96
|
imageInput: 'Image input',
|
|
94
97
|
hiddenVariants: 'Transparent variant routing',
|
|
95
98
|
hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
|
|
@@ -255,6 +258,9 @@ const zh: Record<LocaleKey, string> = {
|
|
|
255
258
|
runtimeMode: '环境来源',
|
|
256
259
|
toolkitPath: 'agent-vision-toolkit 目录',
|
|
257
260
|
python: 'Python 解释器(可选)',
|
|
261
|
+
storage: '本地文件',
|
|
262
|
+
storageDir: '默认保存目录',
|
|
263
|
+
storageDirHint: '留空时继续在各工作区创建 .dsh-vision-toolkit。在 POSIX 系统上填写绝对路径共享根目录(例如 /tmp/dsh-vision-toolkit)后,产物、粘贴图片和缓存会保存到自动生成的当前用户、工作区专属子目录中。Windows 共享目录会保持禁用,直到能够安全校验 ACL 权限。',
|
|
258
264
|
allowedDirs: '允许读取的其他目录',
|
|
259
265
|
allowedDirsHint: '每行填写一个目录。当前会话的工作目录始终可以读取,无需重复填写。',
|
|
260
266
|
save: '保存设置',
|
|
@@ -277,7 +283,7 @@ const zh: Record<LocaleKey, string> = {
|
|
|
277
283
|
connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
|
|
278
284
|
saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
|
|
279
285
|
advanced: '高级设置',
|
|
280
|
-
advancedHint: '
|
|
286
|
+
advancedHint: '凭据名称、服务兼容参数、结果语言、资源限制、默认保存目录、运行环境来源、Python 和额外可读目录。一般无需修改。',
|
|
281
287
|
imageInput: '图片输入',
|
|
282
288
|
hiddenVariants: '透明变体路由',
|
|
283
289
|
hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
|
|
@@ -478,6 +484,8 @@ interface SettingsValue {
|
|
|
478
484
|
maxImagePixels?: number
|
|
479
485
|
concurrency?: number
|
|
480
486
|
runtime?: { mode?: 'managed' | 'external'; agentVisionToolkitPath?: string; python?: string }
|
|
487
|
+
storageDir?: string
|
|
488
|
+
storageHistory?: string[]
|
|
481
489
|
allowedDirs?: string[]
|
|
482
490
|
imageInputVariants?: {
|
|
483
491
|
enabled?: boolean
|
|
@@ -1085,6 +1093,7 @@ interface Draft {
|
|
|
1085
1093
|
runtimeMode: 'managed' | 'external'
|
|
1086
1094
|
toolkitPath: string
|
|
1087
1095
|
python: string
|
|
1096
|
+
storageDir: string
|
|
1088
1097
|
allowedDirs: string
|
|
1089
1098
|
hiddenVariants: boolean
|
|
1090
1099
|
variantEnabled: boolean
|
|
@@ -1108,6 +1117,7 @@ function draftOf(value: SettingsValue): Draft {
|
|
|
1108
1117
|
runtimeMode: value.runtime?.mode ?? 'managed',
|
|
1109
1118
|
toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
|
|
1110
1119
|
python: value.runtime?.python ?? '',
|
|
1120
|
+
storageDir: value.storageDir ?? '',
|
|
1111
1121
|
allowedDirs: (value.allowedDirs ?? []).join('\n'),
|
|
1112
1122
|
hiddenVariants: value.imageInputVariants?.hidden ?? true,
|
|
1113
1123
|
variantEnabled: value.imageInputVariants?.enabled ?? true,
|
|
@@ -1152,6 +1162,7 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
|
|
|
1152
1162
|
...(draft.runtimeMode === 'external' ? { agentVisionToolkitPath: draft.toolkitPath.trim() } : {}),
|
|
1153
1163
|
...(draft.python.trim().length === 0 ? {} : { python: draft.python.trim() }),
|
|
1154
1164
|
},
|
|
1165
|
+
...(draft.storageDir.trim().length === 0 ? {} : { storageDir: draft.storageDir.trim() }),
|
|
1155
1166
|
allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
|
|
1156
1167
|
imageInputVariants: {
|
|
1157
1168
|
...(draft.variantEnabled ? {} : { enabled: false }),
|
|
@@ -1463,11 +1474,15 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
|
|
|
1463
1474
|
|
|
1464
1475
|
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('imageInput')}</h3></div><label className="dvt-check"><input type="checkbox" checked={draft.hiddenVariants} disabled={!snapshot.writable || busy} onChange={(event) => { update('hiddenVariants', event.target.checked) }} /><span>{t('hiddenVariantsLabel')}</span><small>{t('hiddenVariantsHint')}</small></label></section>
|
|
1465
1476
|
|
|
1477
|
+
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('storage')}</h3></div><div className="dvt-form-grid">
|
|
1478
|
+
<Field label={t('storageDir')} hint={t('storageDirHint')}><Input aria-label={t('storageDir')} placeholder="/tmp/dsh-vision-toolkit" value={draft.storageDir} onChange={(event) => { update('storageDir', event.target.value) }} /></Field>
|
|
1479
|
+
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
1480
|
+
</div></section>
|
|
1481
|
+
|
|
1466
1482
|
<section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('runtime')}</h3><span className={`dvt-badge ${snapshot.runtime.ready ? 'ok' : 'error'}`}>{snapshot.runtime.ready ? snapshot.runtime.upstream?.source === 'managed' ? t('runtimeManaged') : snapshot.runtime.upstream?.source === 'external' ? t('runtimeExternal') : t('runtimeReady') : t('runtimeUnavailable')}</span></div><div className="dvt-form-grid">
|
|
1467
1483
|
<Field label={t('runtimeMode')}><select value={draft.runtimeMode} onChange={(event) => { update('runtimeMode', event.target.value as 'managed' | 'external') }}><option value="managed">{t('runtimeManaged')}</option><option value="external">{t('runtimeExternal')}</option></select></Field>
|
|
1468
1484
|
{draft.runtimeMode === 'external' ? <Field label={t('toolkitPath')}><Input value={draft.toolkitPath} onChange={(event) => { update('toolkitPath', event.target.value) }} /></Field> : null}
|
|
1469
1485
|
<Field label={t('python')}><Input placeholder="python3" value={draft.python} onChange={(event) => { update('python', event.target.value) }} /></Field>
|
|
1470
|
-
<Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
|
|
1471
1486
|
</div>
|
|
1472
1487
|
{snapshot.runtime.upstream === undefined ? null : <div className="dvt-runtime-facts"><code>{snapshot.runtime.upstream.path}</code><code>{snapshot.runtime.upstream.python} · {snapshot.runtime.upstream.pythonVersion}</code><code>{snapshot.runtime.upstream.runtimeHome}</code></div>}
|
|
1473
1488
|
</section>
|
package/src/config.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import z from '@deepseek-ai/schemastery'
|
|
10
10
|
import type Schema from '@deepseek-ai/schemastery'
|
|
11
11
|
import { credentialRef, type CredentialRef } from '@deepseek-ai/dsh-credentials'
|
|
12
|
-
import {
|
|
12
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
13
13
|
import { VisionToolkitError } from './errors.ts'
|
|
14
14
|
import {
|
|
15
15
|
BUILT_IN_FREE_VISION_BASE_URL,
|
|
@@ -24,8 +24,21 @@ export {
|
|
|
24
24
|
BUILT_IN_FREE_VISION_MODEL,
|
|
25
25
|
} from './defaults.ts'
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The namespace pattern the removed `settingsNamespace` helper enforced.
|
|
29
|
+
* dsh 0.1.2-alpha dropped that export; importing a missing named export is a
|
|
30
|
+
* module-evaluation error that stops the host from booting, so the check is
|
|
31
|
+
* inlined here instead of imported. The namespace is a static string, so no
|
|
32
|
+
* runtime dependency on `@deepseek-ai/dsh-settings` is needed.
|
|
33
|
+
*/
|
|
34
|
+
const SETTINGS_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
35
|
+
|
|
27
36
|
/** Settings document namespace owned by this plugin. */
|
|
28
|
-
export const VISION_TOOLKIT_SETTINGS_NAMESPACE =
|
|
37
|
+
export const VISION_TOOLKIT_SETTINGS_NAMESPACE = 'vision-toolkit' as SettingsNamespace
|
|
38
|
+
|
|
39
|
+
if (!SETTINGS_NAMESPACE_PATTERN.test(VISION_TOOLKIT_SETTINGS_NAMESPACE)) {
|
|
40
|
+
throw new TypeError(`settings namespace "${VISION_TOOLKIT_SETTINGS_NAMESPACE}" must match ${String(SETTINGS_NAMESPACE_PATTERN)}`)
|
|
41
|
+
}
|
|
29
42
|
|
|
30
43
|
/** Browser-compatible default shared with the vendored Python client. */
|
|
31
44
|
export const DEFAULT_VISION_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
@@ -78,6 +91,14 @@ export interface VisionToolkitConfig {
|
|
|
78
91
|
/** Optional Python 3.11+ bootstrap/interpreter override. */
|
|
79
92
|
python?: string
|
|
80
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Optional shared storage root. When set, every workspace gets an isolated,
|
|
96
|
+
* automatically generated child directory below this root instead of writing
|
|
97
|
+
* `.dsh-vision-toolkit` into the workspace.
|
|
98
|
+
*/
|
|
99
|
+
storageDir?: string
|
|
100
|
+
/** Internal read-only history used to keep persisted paths valid after storage moves. */
|
|
101
|
+
storageHistory?: string[]
|
|
81
102
|
/** Extra directories (besides the workspace) inputs may come from. */
|
|
82
103
|
allowedDirs?: string[]
|
|
83
104
|
/**
|
|
@@ -132,6 +153,8 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
|
|
|
132
153
|
agentVisionToolkitPath: z.string(),
|
|
133
154
|
python: z.string(),
|
|
134
155
|
}),
|
|
156
|
+
storageDir: z.string(),
|
|
157
|
+
storageHistory: z.array(z.string()).default([]),
|
|
135
158
|
allowedDirs: z.array(z.string()).default([]),
|
|
136
159
|
imageInputVariants: z.object({
|
|
137
160
|
enabled: z.boolean().default(true),
|
|
@@ -161,6 +184,8 @@ export interface ResolvedVisionToolkitConfig {
|
|
|
161
184
|
agentVisionToolkitPath?: string
|
|
162
185
|
python?: string
|
|
163
186
|
}
|
|
187
|
+
storageDir?: string
|
|
188
|
+
storageHistory: string[]
|
|
164
189
|
allowedDirs: string[]
|
|
165
190
|
imageInputVariants: {
|
|
166
191
|
enabled: boolean
|
|
@@ -254,6 +279,10 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
254
279
|
if (python !== undefined && python.length === 0) {
|
|
255
280
|
throw new VisionToolkitError('config', 'runtime.python must not be empty')
|
|
256
281
|
}
|
|
282
|
+
const storageDir = config.storageDir?.trim()
|
|
283
|
+
const storageHistory = [...new Set((config.storageHistory ?? [])
|
|
284
|
+
.map(dir => dir.trim())
|
|
285
|
+
.filter(dir => dir.length > 0 && dir !== storageDir))]
|
|
257
286
|
const allowedDirs = (config.allowedDirs ?? []).map(dir => dir.trim()).filter(dir => dir.length > 0)
|
|
258
287
|
const imageInputVariants = config.imageInputVariants ?? {}
|
|
259
288
|
const variantProviders = (imageInputVariants.providers ?? [])
|
|
@@ -271,6 +300,8 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
271
300
|
...(toolkitPath !== undefined ? { agentVisionToolkitPath: toolkitPath } : {}),
|
|
272
301
|
...(python !== undefined ? { python } : {}),
|
|
273
302
|
},
|
|
303
|
+
...(storageDir === undefined || storageDir.length === 0 ? {} : { storageDir }),
|
|
304
|
+
storageHistory,
|
|
274
305
|
allowedDirs,
|
|
275
306
|
imageInputVariants: {
|
|
276
307
|
enabled: imageInputVariants.enabled ?? true,
|
|
@@ -281,6 +312,49 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
|
|
|
281
312
|
}
|
|
282
313
|
}
|
|
283
314
|
|
|
315
|
+
/** Merge prior storage roots into the next resolved generation's read-only history. */
|
|
316
|
+
export function retainedStorageHistory(
|
|
317
|
+
next: VisionToolkitConfig,
|
|
318
|
+
previous: VisionToolkitConfig,
|
|
319
|
+
): string[] {
|
|
320
|
+
const resolvedNext = resolveConfig(next)
|
|
321
|
+
const resolvedPrevious = resolveConfig(previous)
|
|
322
|
+
return [...new Set([
|
|
323
|
+
...resolvedPrevious.storageHistory,
|
|
324
|
+
...resolvedNext.storageHistory,
|
|
325
|
+
...(resolvedPrevious.storageDir === undefined ? [] : [resolvedPrevious.storageDir]),
|
|
326
|
+
])].filter(storageDir => storageDir !== resolvedNext.storageDir)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface WatchedSettingsGeneration {
|
|
330
|
+
/** Configuration to activate now; omitted after a successful history writeback. */
|
|
331
|
+
config?: VisionToolkitConfig
|
|
332
|
+
/** Whether the derived history still needs plugin-owned durable persistence. */
|
|
333
|
+
requiresDurableStorageHistory?: boolean
|
|
334
|
+
/** Non-fatal internal-history persistence error. */
|
|
335
|
+
persistenceError?: unknown
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Prepare one live Settings generation without letting internal history writeback block activation. */
|
|
339
|
+
export async function prepareWatchedSettingsGeneration(
|
|
340
|
+
next: VisionToolkitConfig,
|
|
341
|
+
previous: VisionToolkitConfig,
|
|
342
|
+
writable: boolean,
|
|
343
|
+
persistStorageHistory: (storageHistory: string[]) => Promise<void>,
|
|
344
|
+
): Promise<WatchedSettingsGeneration> {
|
|
345
|
+
const storageHistory = retainedStorageHistory(next, previous)
|
|
346
|
+
if (JSON.stringify(storageHistory) === JSON.stringify(resolveConfig(next).storageHistory)) return { config: next }
|
|
347
|
+
|
|
348
|
+
const config = { ...next, storageHistory }
|
|
349
|
+
if (!writable) return { config, requiresDurableStorageHistory: true }
|
|
350
|
+
try {
|
|
351
|
+
await persistStorageHistory(storageHistory)
|
|
352
|
+
return {}
|
|
353
|
+
} catch (persistenceError) {
|
|
354
|
+
return { config, requiresDurableStorageHistory: true, persistenceError }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
284
358
|
/** Whether a resolved provider should use the bundled public key instead of DSH credentials. */
|
|
285
359
|
export function isBuiltInFreeVisionProvider(provider: ResolvedVisionToolkitConfig['provider']): boolean {
|
|
286
360
|
return String(provider.credential) === BUILT_IN_FREE_VISION_CREDENTIAL
|
package/src/evidence-cache.ts
CHANGED
|
@@ -73,7 +73,7 @@ function hash(value: string): string {
|
|
|
73
73
|
return createHash('sha256').update(value).digest('hex')
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/** Fingerprint every runtime setting that can change
|
|
76
|
+
/** Fingerprint every runtime setting that can change generated evidence or its embedded path. */
|
|
77
77
|
export function evidenceRuntimeFingerprint(
|
|
78
78
|
config: ResolvedVisionToolkitConfig,
|
|
79
79
|
credentialSha256?: string,
|
|
@@ -99,6 +99,7 @@ export function evidenceRuntimeFingerprint(
|
|
|
99
99
|
maxImageBytes: config.maxImageBytes,
|
|
100
100
|
maxImagePixels: config.maxImagePixels,
|
|
101
101
|
runtime: config.runtime,
|
|
102
|
+
storageDir: config.storageDir ?? null,
|
|
102
103
|
}))
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
LlmProviderInfo,
|
|
24
24
|
LlmResolvedModelInfo,
|
|
25
25
|
Message,
|
|
26
|
+
ResolvedRetryPolicy,
|
|
26
27
|
StreamChunk,
|
|
27
28
|
} from '@deepseek-ai/dsh-llm'
|
|
28
29
|
// Type-only imports activate the host service declarations on Context.
|
|
@@ -94,6 +95,18 @@ export function variantProviderId(upstream: string): string {
|
|
|
94
95
|
return `${VARIANT_PROVIDER_PREFIX}${upstream}`
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/** Stable semantic key for detecting a provider retry-policy replacement. */
|
|
99
|
+
function retryPolicyKey(policy: ResolvedRetryPolicy): string {
|
|
100
|
+
const backoff = [policy.initialDelayMs, policy.maxDelayMs, policy.jitterRatio]
|
|
101
|
+
if (policy.mode === 'always') return JSON.stringify([policy.mode, ...backoff])
|
|
102
|
+
return JSON.stringify([
|
|
103
|
+
policy.mode,
|
|
104
|
+
policy.maxRetries,
|
|
105
|
+
[...policy.retryableCodes].sort(),
|
|
106
|
+
...backoff,
|
|
107
|
+
])
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
/**
|
|
98
111
|
* Whether one model earns an image-input variant: the host must positively
|
|
99
112
|
* declare it text-only. A model with unknown modalities is left alone — its
|
|
@@ -129,18 +142,21 @@ async function materializeImage(
|
|
|
129
142
|
data: Uint8Array,
|
|
130
143
|
extension: string,
|
|
131
144
|
sessionId: string | undefined,
|
|
145
|
+
storageDir: string | undefined,
|
|
132
146
|
): Promise<MaterializedImage> {
|
|
133
147
|
const session = sessionId === undefined ? undefined : ctx.sessions.get(sessionId as never)
|
|
134
148
|
const cwd = session?.header.cwd
|
|
135
149
|
if (sessionId !== undefined && cwd !== undefined && isAbsolute(cwd)) {
|
|
136
|
-
const root = await sessionPasteRoot(ctx, sessionId)
|
|
150
|
+
const root = await sessionPasteRoot(ctx, sessionId, storageDir)
|
|
137
151
|
const identity = createHash('sha256')
|
|
138
152
|
.update(`${sessionId}\u0000${String(block.attachment.attachmentId)}`)
|
|
139
153
|
.digest('hex')
|
|
140
154
|
.slice(0, 32)
|
|
141
|
-
const
|
|
155
|
+
const filename = `attachment-${identity}${extension}`
|
|
156
|
+
const writePath = join(root.writeRoot, filename)
|
|
157
|
+
const file = join(root.visibleRoot, filename)
|
|
142
158
|
try {
|
|
143
|
-
await writeFile(
|
|
159
|
+
await writeFile(writePath, Buffer.from(data), { mode: 0o600, flag: 'wx' })
|
|
144
160
|
} catch (error) {
|
|
145
161
|
if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error
|
|
146
162
|
}
|
|
@@ -344,6 +360,7 @@ async function readImageBlock(
|
|
|
344
360
|
block: ImageBlock,
|
|
345
361
|
query: string,
|
|
346
362
|
sessionId?: string,
|
|
363
|
+
storageDir?: string,
|
|
347
364
|
): Promise<ContentBlock> {
|
|
348
365
|
const attachments = ctx.get('attachments')
|
|
349
366
|
const current = runtime()
|
|
@@ -358,7 +375,7 @@ async function readImageBlock(
|
|
|
358
375
|
let pathEvidence = ''
|
|
359
376
|
try {
|
|
360
377
|
const stored = await attachments.readImage(block.attachment)
|
|
361
|
-
const materialized = await materializeImage(ctx, block, stored.data, extension, sessionId)
|
|
378
|
+
const materialized = await materializeImage(ctx, block, stored.data, extension, sessionId, storageDir)
|
|
362
379
|
temporaryDirectory = materialized.temporaryDirectory
|
|
363
380
|
if (materialized.persistent) pathEvidence = imagePathEvidence(materialized.file)
|
|
364
381
|
|
|
@@ -401,6 +418,7 @@ async function readImageBlock(
|
|
|
401
418
|
* @param signal - the caller's cancellation for this conversion pass.
|
|
402
419
|
* @param sessionId - the live Session identity, when available.
|
|
403
420
|
* @param runtimeHash - stable fingerprint of the vision provider and evidence runtime.
|
|
421
|
+
* @param storageDir - optional shared plugin storage root.
|
|
404
422
|
* @returns the rewritten message list.
|
|
405
423
|
*/
|
|
406
424
|
export async function convertImagesToEvidence(
|
|
@@ -411,6 +429,7 @@ export async function convertImagesToEvidence(
|
|
|
411
429
|
signal?: AbortSignal,
|
|
412
430
|
sessionId?: string,
|
|
413
431
|
runtimeHash = 'process-only-runtime',
|
|
432
|
+
storageDir?: string,
|
|
414
433
|
): Promise<Message[]> {
|
|
415
434
|
const session = sessionId === undefined ? undefined : ctx.sessions.get(sessionId as never)
|
|
416
435
|
const sessionIdentity = session === undefined
|
|
@@ -467,7 +486,7 @@ export async function convertImagesToEvidence(
|
|
|
467
486
|
prompt: query,
|
|
468
487
|
runtimeHash,
|
|
469
488
|
}), () =>
|
|
470
|
-
readImageBlock(ctx, runtime, block, query, sessionId)),
|
|
489
|
+
readImageBlock(ctx, runtime, block, query, sessionId, storageDir)),
|
|
471
490
|
signal,
|
|
472
491
|
),
|
|
473
492
|
signal,
|
|
@@ -504,6 +523,7 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
504
523
|
private readonly runtime: () => VisionToolkitRuntime | undefined,
|
|
505
524
|
private readonly cache: EvidenceCache,
|
|
506
525
|
private readonly hidden: () => boolean = () => false,
|
|
526
|
+
private readonly startupStorageDirectory: () => string | undefined = () => undefined,
|
|
507
527
|
) {
|
|
508
528
|
super()
|
|
509
529
|
}
|
|
@@ -515,6 +535,10 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
515
535
|
}
|
|
516
536
|
}
|
|
517
537
|
|
|
538
|
+
override providerRetryPolicy(_provider: string): ResolvedRetryPolicy {
|
|
539
|
+
return this.llm.providerRetryPolicy(this.upstream)
|
|
540
|
+
}
|
|
541
|
+
|
|
518
542
|
override async listModels(provider: string): Promise<readonly LlmModelInfo[]> {
|
|
519
543
|
const models = await this.llm.listModels(this.upstream)
|
|
520
544
|
return models.filter(shouldWrapModel).map((model) => ({
|
|
@@ -552,6 +576,7 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
552
576
|
|
|
553
577
|
override async *stream(options: GenerateOptions): AsyncGenerator<StreamChunk> {
|
|
554
578
|
const current = this.runtime()
|
|
579
|
+
const storageDir = current === undefined ? this.startupStorageDirectory() : current.storageDirectory
|
|
555
580
|
let captured: CapturedEvidenceRuntime | undefined
|
|
556
581
|
if (current !== undefined && options.messages.some(message => contentHasImage(message.content))) {
|
|
557
582
|
try {
|
|
@@ -573,7 +598,12 @@ export class ImageInputVariantAdapter extends LlmAdapter {
|
|
|
573
598
|
options.messages,
|
|
574
599
|
options.signal,
|
|
575
600
|
options.sessionId === undefined ? undefined : String(options.sessionId),
|
|
576
|
-
captured?.evidenceFingerprint ?? '
|
|
601
|
+
captured?.evidenceFingerprint ?? createHash('sha256')
|
|
602
|
+
.update('process-only-runtime')
|
|
603
|
+
.update('\0')
|
|
604
|
+
.update(storageDir ?? '')
|
|
605
|
+
.digest('hex'),
|
|
606
|
+
storageDir,
|
|
577
607
|
)
|
|
578
608
|
// Delegate through the host service under the upstream route: the variant
|
|
579
609
|
// is a wire-only facade, and the upstream route owns retry and replay.
|
|
@@ -809,13 +839,13 @@ export function installImageInputVariants(
|
|
|
809
839
|
ctx: Context,
|
|
810
840
|
getConfig: () => ResolvedVisionToolkitConfig,
|
|
811
841
|
getRuntime: () => VisionToolkitRuntime | undefined,
|
|
842
|
+
getStartupStorageDirectory: () => string | undefined = () => undefined,
|
|
812
843
|
): { dispose: () => void; reconcile: () => void } {
|
|
813
844
|
const evidenceStore = new SessionEvidenceStore(ctx)
|
|
814
845
|
const evidenceCache = new EvidenceCache(EVIDENCE_CACHE_LIMIT, evidenceStore)
|
|
815
|
-
const registrations = new Map<string, () => void>()
|
|
816
|
-
// The host snapshots adapter provider metadata
|
|
817
|
-
//
|
|
818
|
-
// every wrapper for the new names to reach the model selector.
|
|
846
|
+
const registrations = new Map<string, { dispose: () => void; retryPolicyKey: string }>()
|
|
847
|
+
// The host snapshots adapter provider metadata and retry policy at registration
|
|
848
|
+
// time, so changes to either require rebuilding the wrapper route.
|
|
819
849
|
let lastHidden = false
|
|
820
850
|
// Upstream ids observed missing and the timestamp of the first missing
|
|
821
851
|
// observation. A wrapper is only released after its upstream stays absent
|
|
@@ -832,12 +862,12 @@ export function installImageInputVariants(
|
|
|
832
862
|
let sweepQueued = false
|
|
833
863
|
|
|
834
864
|
const release = (upstream: string): void => {
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
865
|
+
const registration = registrations.get(upstream)
|
|
866
|
+
if (registration === undefined) return
|
|
837
867
|
registrations.delete(upstream)
|
|
838
868
|
stale.delete(upstream)
|
|
839
869
|
try {
|
|
840
|
-
dispose()
|
|
870
|
+
registration.dispose()
|
|
841
871
|
} catch (error) {
|
|
842
872
|
ctx.logger.warn(
|
|
843
873
|
'dsh-vision-toolkit: image-input variant release failed for "%s": %s',
|
|
@@ -916,18 +946,28 @@ export function installImageInputVariants(
|
|
|
916
946
|
continue
|
|
917
947
|
}
|
|
918
948
|
const eligible = models.some(shouldWrapModel)
|
|
919
|
-
|
|
920
|
-
|
|
949
|
+
let upstreamRetryPolicyKey: string
|
|
950
|
+
try {
|
|
951
|
+
upstreamRetryPolicyKey = retryPolicyKey(llm.providerRetryPolicy(upstream))
|
|
952
|
+
} catch {
|
|
953
|
+
// Preserve an existing wrapper through a transient upstream registry
|
|
954
|
+
// gap; the next topology event or periodic sweep will retry the probe.
|
|
955
|
+
continue
|
|
956
|
+
}
|
|
957
|
+
let registration = registrations.get(upstream)
|
|
958
|
+
if (registration !== undefined && live.has(variantId)) {
|
|
921
959
|
// Re-read the live registry after the await: a rebuild inside the
|
|
922
960
|
// probe window can drop our wrapper without touching our map.
|
|
923
961
|
const liveNow = new Set(llm.listProviders().map(provider => provider.id))
|
|
924
962
|
if (liveNow.has(variantId)) {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
963
|
+
if (eligible && registration.retryPolicyKey === upstreamRetryPolicyKey) continue
|
|
964
|
+
// Eligibility and retry policy are registration-time facts. Rebuild
|
|
965
|
+
// whenever either no longer matches the live upstream route.
|
|
966
|
+
release(upstream)
|
|
967
|
+
registration = undefined
|
|
928
968
|
}
|
|
929
969
|
}
|
|
930
|
-
if (
|
|
970
|
+
if (registration !== undefined) {
|
|
931
971
|
// Dead handle from a registry rebuild/reset: drop it and register a
|
|
932
972
|
// fresh wrapper below.
|
|
933
973
|
release(upstream)
|
|
@@ -945,9 +985,10 @@ export function installImageInputVariants(
|
|
|
945
985
|
getRuntime,
|
|
946
986
|
evidenceCache,
|
|
947
987
|
() => getConfig().imageInputVariants.hidden,
|
|
988
|
+
getStartupStorageDirectory,
|
|
948
989
|
),
|
|
949
990
|
)
|
|
950
|
-
registrations.set(upstream, dispose)
|
|
991
|
+
registrations.set(upstream, { dispose, retryPolicyKey: upstreamRetryPolicyKey })
|
|
951
992
|
stale.delete(upstream)
|
|
952
993
|
} catch (error) {
|
|
953
994
|
ctx.logger.warn(
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,16 @@ import { ArtifactAccessController, prepareArtifactAccessKey } from './artifact-a
|
|
|
18
18
|
import {
|
|
19
19
|
Config,
|
|
20
20
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
21
|
+
prepareWatchedSettingsGeneration,
|
|
21
22
|
resolveConfig,
|
|
23
|
+
type ResolvedVisionToolkitConfig,
|
|
22
24
|
type VisionToolkitConfig,
|
|
23
25
|
} from './config.ts'
|
|
24
26
|
import { VisionToolExposure } from './exposure.ts'
|
|
25
27
|
import { createPasteTakeoverResolver, installImageInputVariants } from './image-input-variants.ts'
|
|
26
28
|
import { VisionToolkitRuntimeManager } from './runtime-manager.ts'
|
|
27
29
|
import { VISION_SKILLS_SKILL } from './skill.ts'
|
|
30
|
+
import { StorageHistoryStore } from './storage-history.ts'
|
|
28
31
|
import { createVisionTools } from './tools.ts'
|
|
29
32
|
import { PLUGIN_VERSION } from './version.ts'
|
|
30
33
|
import { installVisionToolkitWeb, VisionToolkitWebBackend } from './web.ts'
|
|
@@ -51,8 +54,33 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
|
|
|
51
54
|
const artifacts = new ArtifactAccessController(await prepareArtifactAccessKey())
|
|
52
55
|
const lifecycle = new AbortController()
|
|
53
56
|
const disposers: Array<() => void> = []
|
|
57
|
+
const storageHistory = new StorageHistoryStore(ctx)
|
|
58
|
+
disposers.push(() => { storageHistory.dispose() })
|
|
59
|
+
let storageHistoryWarningReported = false
|
|
54
60
|
let operationalDisposers: { activationTool: () => void; exposure: () => void; skill: () => void } | undefined
|
|
55
61
|
|
|
62
|
+
const persistStorageHistory = async (candidate: VisionToolkitConfig, required: boolean): Promise<void> => {
|
|
63
|
+
try {
|
|
64
|
+
const persisted = await storageHistory.persist(candidate)
|
|
65
|
+
if (persisted) return
|
|
66
|
+
const error = new Error(
|
|
67
|
+
'configured storage history requires @deepseek-ai/dsh-storage-domain when Settings cannot persist it',
|
|
68
|
+
)
|
|
69
|
+
if (required) throw error
|
|
70
|
+
if (!storageHistoryWarningReported) {
|
|
71
|
+
storageHistoryWarningReported = true
|
|
72
|
+
ctx.logger.warn('dsh-vision-toolkit: %s', error.message)
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (required) throw error
|
|
76
|
+
if (!storageHistoryWarningReported) {
|
|
77
|
+
storageHistoryWarningReported = true
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
79
|
+
ctx.logger.warn('dsh-vision-toolkit: configured storage history was not persisted. %s', message)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
const ensureOperational = (): void => {
|
|
57
85
|
if (!manager.ready || operationalDisposers !== undefined) return
|
|
58
86
|
const exposure = new VisionToolExposure(ctx, () => createVisionTools(
|
|
@@ -84,10 +112,16 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
|
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
114
|
|
|
115
|
+
const initialConfig = await storageHistory.restore(settings.get())
|
|
87
116
|
try {
|
|
88
|
-
await manager.initialize(
|
|
117
|
+
await manager.initialize(initialConfig, candidate => persistStorageHistory(candidate.config, false))
|
|
89
118
|
ensureOperational()
|
|
90
119
|
} catch (error) {
|
|
120
|
+
const resolvedInitial = resolveConfig(initialConfig)
|
|
121
|
+
if (resolvedInitial.storageDir === undefined
|
|
122
|
+
|| manager.validatedStorageDirectory() === resolvedInitial.storageDir) {
|
|
123
|
+
await persistStorageHistory(initialConfig, false)
|
|
124
|
+
}
|
|
91
125
|
const message = error instanceof Error ? error.message : String(error)
|
|
92
126
|
ctx.logger.error(
|
|
93
127
|
'dsh-vision-toolkit %s: runtime not ready; the vision-skills skill, activation bootstrap, and Agent-scoped visual tools are NOT registered. Settings remain available for repair. %s',
|
|
@@ -97,29 +131,54 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
|
|
|
97
131
|
}
|
|
98
132
|
|
|
99
133
|
const backend = new VisionToolkitWebBackend(ctx, manager, artifacts, ensureOperational)
|
|
134
|
+
const currentConfig = (): ResolvedVisionToolkitConfig => manager.ready
|
|
135
|
+
? manager.currentConfig()
|
|
136
|
+
: resolveConfig(settings.get())
|
|
100
137
|
const pastedImages = new PastedImageBackend(ctx, {
|
|
101
138
|
maxUploadBytes: () => MAX_PASTE_IMAGE_BYTES,
|
|
139
|
+
storageGeneration: () => manager.storageGeneration(),
|
|
102
140
|
})
|
|
103
141
|
// Image-input variants register asynchronously once eligible routes exist;
|
|
104
142
|
// the runtime getter stays lazy so variants appear even when the runtime
|
|
105
143
|
// becomes ready after the first sweep.
|
|
106
144
|
const variants = installImageInputVariants(
|
|
107
145
|
ctx,
|
|
108
|
-
|
|
146
|
+
currentConfig,
|
|
109
147
|
() => manager.ready ? manager.current() : undefined,
|
|
148
|
+
() => manager.validatedStorageDirectory(),
|
|
110
149
|
)
|
|
111
150
|
installVisionToolkitWeb(
|
|
112
151
|
ctx,
|
|
113
152
|
backend,
|
|
114
153
|
artifacts,
|
|
115
154
|
pastedImages,
|
|
116
|
-
createPasteTakeoverResolver(ctx,
|
|
117
|
-
() => ({ hidden:
|
|
155
|
+
createPasteTakeoverResolver(ctx, currentConfig),
|
|
156
|
+
() => ({ hidden: currentConfig().imageInputVariants.hidden }),
|
|
118
157
|
)
|
|
119
158
|
disposers.push(variants.dispose)
|
|
120
|
-
disposers.push(settings.watch(async (next) => {
|
|
159
|
+
disposers.push(settings.watch(async (next, previous) => {
|
|
121
160
|
try {
|
|
122
|
-
await
|
|
161
|
+
const prepared = await prepareWatchedSettingsGeneration(
|
|
162
|
+
next,
|
|
163
|
+
previous,
|
|
164
|
+
ctx.settings.writable,
|
|
165
|
+
storageHistory => settings.update({ storageHistory }),
|
|
166
|
+
)
|
|
167
|
+
if (prepared.persistenceError !== undefined) {
|
|
168
|
+
const message = prepared.persistenceError instanceof Error
|
|
169
|
+
? prepared.persistenceError.message
|
|
170
|
+
: String(prepared.persistenceError)
|
|
171
|
+
ctx.logger.warn('dsh-vision-toolkit: activating Settings without persisting internal storage history. %s', message)
|
|
172
|
+
}
|
|
173
|
+
if (prepared.config === undefined) return
|
|
174
|
+
const candidate = await storageHistory.restore(prepared.config)
|
|
175
|
+
await manager.reconfigure(
|
|
176
|
+
candidate,
|
|
177
|
+
generation => persistStorageHistory(
|
|
178
|
+
generation.config,
|
|
179
|
+
prepared.requiresDurableStorageHistory === true,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
123
182
|
ensureOperational()
|
|
124
183
|
variants.reconcile()
|
|
125
184
|
} catch (error) {
|