@anionex/dsh-vision-toolkit 0.1.39 → 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 +10 -2
- package/README.zh.md +10 -2
- 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 +147 -4
- 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 +8 -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/web.d.ts.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 +168 -5
- package/src/runtime-manager.ts +93 -10
- package/src/runtime.ts +15 -9
- package/src/storage-history.ts +172 -0
- package/src/web.ts +9 -2
package/src/runtime-manager.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import { resolveConfig, type ResolvedVisionToolkitConfig, type VisionToolkitConfig } from './config.ts'
|
|
10
|
+
import { preflightSharedStorageBase } from './paths.ts'
|
|
10
11
|
import { VisionToolkitRuntime } from './runtime.ts'
|
|
11
12
|
import { UpstreamAdapter, type UpstreamVersionInfo } from './upstream.ts'
|
|
12
13
|
|
|
@@ -26,16 +27,30 @@ export interface RuntimeManagerStatus {
|
|
|
26
27
|
lastError?: string
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** Storage selection paired to the active or validated startup generation. */
|
|
31
|
+
export interface RuntimeStorageGeneration {
|
|
32
|
+
generation: number
|
|
33
|
+
storageDir?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
/** Test seam for preparing one generation. */
|
|
30
37
|
export type RuntimeGenerationFactory = (
|
|
31
38
|
ctx: Context,
|
|
32
39
|
config: ResolvedVisionToolkitConfig,
|
|
40
|
+
readableStorageDirs: readonly string[],
|
|
33
41
|
) => Promise<VisionToolkitRuntime>
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
/** Async commit prerequisite run after preparation and before a generation becomes active. */
|
|
44
|
+
export type RuntimeGenerationBeforePublish = (candidate: PreparedRuntimeGeneration) => Promise<void>
|
|
45
|
+
|
|
46
|
+
async function defaultFactory(
|
|
47
|
+
ctx: Context,
|
|
48
|
+
config: ResolvedVisionToolkitConfig,
|
|
49
|
+
readableStorageDirs: readonly string[],
|
|
50
|
+
): Promise<VisionToolkitRuntime> {
|
|
36
51
|
const adapter = new UpstreamAdapter(ctx, config)
|
|
37
52
|
await adapter.prepare()
|
|
38
|
-
return new VisionToolkitRuntime(ctx, config, adapter)
|
|
53
|
+
return new VisionToolkitRuntime(ctx, config, adapter, readableStorageDirs)
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
function fingerprint(config: ResolvedVisionToolkitConfig): string {
|
|
@@ -57,6 +72,8 @@ export class VisionToolkitRuntimeManager {
|
|
|
57
72
|
private generation = 0
|
|
58
73
|
private reconfigureTicket = 0
|
|
59
74
|
private lastError: string | undefined
|
|
75
|
+
private validatedStartupStorageDir: string | null | undefined
|
|
76
|
+
private readonly readableStorageDirs = new Set<string>()
|
|
60
77
|
|
|
61
78
|
constructor(
|
|
62
79
|
private readonly ctx: Context,
|
|
@@ -69,22 +86,69 @@ export class VisionToolkitRuntimeManager {
|
|
|
69
86
|
return this.active.runtime
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
/** Configuration belonging to the currently serving runtime generation. */
|
|
90
|
+
currentConfig(): ResolvedVisionToolkitConfig {
|
|
91
|
+
if (this.active === undefined) throw new Error('dsh-vision-toolkit runtime is not ready')
|
|
92
|
+
return this.active.config
|
|
93
|
+
}
|
|
94
|
+
|
|
72
95
|
/** Whether at least one generation is available. */
|
|
73
96
|
get ready(): boolean {
|
|
74
97
|
return this.active !== undefined
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
/** Storage config safe for paste writes even when initial runtime preparation failed. */
|
|
101
|
+
storageGeneration(): RuntimeStorageGeneration {
|
|
102
|
+
if (this.active !== undefined) {
|
|
103
|
+
return {
|
|
104
|
+
generation: this.generation,
|
|
105
|
+
...(this.active.config.storageDir === undefined ? {} : { storageDir: this.active.config.storageDir }),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (this.validatedStartupStorageDir === undefined) {
|
|
109
|
+
throw new Error('dsh-vision-toolkit storage configuration is not ready')
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
generation: this.generation,
|
|
113
|
+
...(this.validatedStartupStorageDir === null ? {} : { storageDir: this.validatedStartupStorageDir }),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Validated storage for best-effort consumers; undefined when startup preflight failed. */
|
|
118
|
+
validatedStorageDirectory(): string | undefined {
|
|
119
|
+
if (this.active !== undefined) return this.active.config.storageDir
|
|
120
|
+
return this.validatedStartupStorageDir ?? undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async prepareResolvedCandidate(config: ResolvedVisionToolkitConfig): Promise<PreparedRuntimeGeneration> {
|
|
80
124
|
const resolvedFingerprint = fingerprint(config)
|
|
81
125
|
if (this.active?.fingerprint === resolvedFingerprint) {
|
|
82
126
|
return { ...this.active, config }
|
|
83
127
|
}
|
|
84
|
-
const runtime = await this.factory(
|
|
128
|
+
const runtime = await this.factory(
|
|
129
|
+
this.ctx,
|
|
130
|
+
config,
|
|
131
|
+
[...new Set([...this.readableStorageDirs, ...config.storageHistory])]
|
|
132
|
+
.filter(storageDir => storageDir !== config.storageDir),
|
|
133
|
+
)
|
|
85
134
|
return { config, fingerprint: resolvedFingerprint, runtime }
|
|
86
135
|
}
|
|
87
136
|
|
|
137
|
+
private rememberStorageDirectory(storageDir: string | undefined): void {
|
|
138
|
+
if (storageDir !== undefined) this.readableStorageDirs.add(storageDir)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private rememberStorageDirectories(storageDirs: readonly string[]): void {
|
|
142
|
+
for (const storageDir of storageDirs) this.rememberStorageDirectory(storageDir)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Resolve and fully prepare a candidate without changing the active runtime. */
|
|
146
|
+
async prepareCandidate(raw: VisionToolkitConfig): Promise<PreparedRuntimeGeneration> {
|
|
147
|
+
const config = resolveConfig(raw)
|
|
148
|
+
if (config.storageDir !== undefined) await preflightSharedStorageBase(config.storageDir)
|
|
149
|
+
return this.prepareResolvedCandidate(config)
|
|
150
|
+
}
|
|
151
|
+
|
|
88
152
|
/**
|
|
89
153
|
* Publish one already-prepared generation atomically.
|
|
90
154
|
* @param candidate - generation returned by {@link prepareCandidate}.
|
|
@@ -97,6 +161,8 @@ export class VisionToolkitRuntimeManager {
|
|
|
97
161
|
}
|
|
98
162
|
this.reconfigureTicket += 1
|
|
99
163
|
this.active = candidate
|
|
164
|
+
this.rememberStorageDirectories(candidate.config.storageHistory)
|
|
165
|
+
this.rememberStorageDirectory(candidate.config.storageDir)
|
|
100
166
|
this.generation += 1
|
|
101
167
|
this.lastError = undefined
|
|
102
168
|
this.ctx.logger.info(
|
|
@@ -108,10 +174,21 @@ export class VisionToolkitRuntimeManager {
|
|
|
108
174
|
)
|
|
109
175
|
}
|
|
110
176
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Prepare and publish the initial or explicitly validated generation.
|
|
179
|
+
* @param raw - untrusted Settings generation to resolve and prepare.
|
|
180
|
+
* @param beforePublish - optional durable prerequisite run after preparation.
|
|
181
|
+
*/
|
|
182
|
+
async initialize(raw: VisionToolkitConfig, beforePublish?: RuntimeGenerationBeforePublish): Promise<void> {
|
|
113
183
|
try {
|
|
114
|
-
|
|
184
|
+
const config = resolveConfig(raw)
|
|
185
|
+
if (config.storageDir !== undefined) await preflightSharedStorageBase(config.storageDir)
|
|
186
|
+
this.validatedStartupStorageDir = config.storageDir ?? null
|
|
187
|
+
this.rememberStorageDirectories(config.storageHistory)
|
|
188
|
+
this.rememberStorageDirectory(config.storageDir)
|
|
189
|
+
const candidate = await this.prepareResolvedCandidate(config)
|
|
190
|
+
await beforePublish?.(candidate)
|
|
191
|
+
this.activateCandidate(candidate)
|
|
115
192
|
} catch (error) {
|
|
116
193
|
this.lastError = messageOf(error)
|
|
117
194
|
throw error
|
|
@@ -121,13 +198,17 @@ export class VisionToolkitRuntimeManager {
|
|
|
121
198
|
/**
|
|
122
199
|
* Apply an externally committed Settings generation. Concurrent edits are
|
|
123
200
|
* last-write-wins; a slower obsolete prepare can never overwrite a newer one.
|
|
201
|
+
* @param raw - externally committed Settings generation.
|
|
202
|
+
* @param beforePublish - optional durable prerequisite run after preparation.
|
|
124
203
|
* @returns whether this call published a new active generation.
|
|
125
204
|
*/
|
|
126
|
-
async reconfigure(raw: VisionToolkitConfig): Promise<boolean> {
|
|
205
|
+
async reconfigure(raw: VisionToolkitConfig, beforePublish?: RuntimeGenerationBeforePublish): Promise<boolean> {
|
|
127
206
|
const ticket = ++this.reconfigureTicket
|
|
128
207
|
let candidate: PreparedRuntimeGeneration
|
|
129
208
|
try {
|
|
130
209
|
candidate = await this.prepareCandidate(raw)
|
|
210
|
+
if (ticket !== this.reconfigureTicket) return false
|
|
211
|
+
await beforePublish?.(candidate)
|
|
131
212
|
} catch (error) {
|
|
132
213
|
if (ticket === this.reconfigureTicket) this.lastError = messageOf(error)
|
|
133
214
|
throw error
|
|
@@ -135,6 +216,8 @@ export class VisionToolkitRuntimeManager {
|
|
|
135
216
|
if (ticket !== this.reconfigureTicket) return false
|
|
136
217
|
const changed = this.active?.fingerprint !== candidate.fingerprint
|
|
137
218
|
this.active = candidate
|
|
219
|
+
this.rememberStorageDirectories(candidate.config.storageHistory)
|
|
220
|
+
this.rememberStorageDirectory(candidate.config.storageDir)
|
|
138
221
|
if (changed) {
|
|
139
222
|
this.generation += 1
|
|
140
223
|
this.ctx.logger.info(
|
package/src/runtime.ts
CHANGED
|
@@ -696,6 +696,7 @@ export class VisionToolkitRuntime {
|
|
|
696
696
|
private readonly ctx: Context,
|
|
697
697
|
private readonly config: ResolvedVisionToolkitConfig,
|
|
698
698
|
adapter?: UpstreamAdapter,
|
|
699
|
+
private readonly readableStorageDirs: readonly string[] = [],
|
|
699
700
|
) {
|
|
700
701
|
this.adapter = adapter ?? new UpstreamAdapter(ctx, config)
|
|
701
702
|
}
|
|
@@ -705,6 +706,11 @@ export class VisionToolkitRuntime {
|
|
|
705
706
|
return this.adapter.versionInfo
|
|
706
707
|
}
|
|
707
708
|
|
|
709
|
+
/** Shared storage root belonging to this immutable runtime generation. */
|
|
710
|
+
get storageDirectory(): string | undefined {
|
|
711
|
+
return this.config.storageDir
|
|
712
|
+
}
|
|
713
|
+
|
|
708
714
|
/** Stable identity for persisted image descriptions produced by this runtime. */
|
|
709
715
|
get evidenceFingerprint(): string {
|
|
710
716
|
return evidenceRuntimeFingerprint(this.config, undefined, process.env.VISION_SSL_VERIFY?.trim())
|
|
@@ -880,13 +886,13 @@ export class VisionToolkitRuntime {
|
|
|
880
886
|
}
|
|
881
887
|
|
|
882
888
|
private pathPolicy(workspace: string): Promise<PathPolicy> {
|
|
883
|
-
return createPathPolicy(workspace, this.config.allowedDirs)
|
|
889
|
+
return createPathPolicy(workspace, this.config.allowedDirs, this.config.storageDir, this.readableStorageDirs)
|
|
884
890
|
}
|
|
885
891
|
|
|
886
892
|
private async compressedImageRoot(policy: PathPolicy): Promise<string> {
|
|
887
|
-
const root = join(policy.
|
|
888
|
-
let current = policy.
|
|
889
|
-
for (const segment of ['
|
|
893
|
+
const root = join(policy.storageRoot, 'tmp', 'compressed-images')
|
|
894
|
+
let current = policy.storageRoot
|
|
895
|
+
for (const segment of ['tmp', 'compressed-images']) {
|
|
890
896
|
current = join(current, segment)
|
|
891
897
|
try {
|
|
892
898
|
await mkdir(current, { mode: 0o700 })
|
|
@@ -897,13 +903,13 @@ export class VisionToolkitRuntime {
|
|
|
897
903
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
898
904
|
throw new VisionToolkitError('path', `compressed-image cache path is not a real directory: ${current}`)
|
|
899
905
|
}
|
|
900
|
-
if (!isWithin(policy.
|
|
901
|
-
throw new VisionToolkitError('path', `compressed-image cache path escaped
|
|
906
|
+
if (!isWithin(policy.storageRoot, current)) {
|
|
907
|
+
throw new VisionToolkitError('path', `compressed-image cache path escaped plugin storage: ${current}`)
|
|
902
908
|
}
|
|
903
909
|
}
|
|
904
910
|
const canonical = await realpath(root)
|
|
905
|
-
if (!isWithin(policy.
|
|
906
|
-
throw new VisionToolkitError('path', 'compressed-image cache resolved outside
|
|
911
|
+
if (!isWithin(policy.storageRoot, canonical)) {
|
|
912
|
+
throw new VisionToolkitError('path', 'compressed-image cache resolved outside plugin storage')
|
|
907
913
|
}
|
|
908
914
|
return canonical
|
|
909
915
|
}
|
|
@@ -2078,7 +2084,7 @@ export class VisionToolkitRuntime {
|
|
|
2078
2084
|
let artifactDirectory: HealthCheck
|
|
2079
2085
|
try {
|
|
2080
2086
|
// allowedDirs are session input roots; they do not affect output readiness.
|
|
2081
|
-
const policy = await createPathPolicy(options.workspace, [])
|
|
2087
|
+
const policy = await createPathPolicy(options.workspace, [], this.config.storageDir)
|
|
2082
2088
|
artifactDirectory = await this.writableDirectoryCheck(policy.outputDir, 'Artifact directory')
|
|
2083
2089
|
} catch {
|
|
2084
2090
|
artifactDirectory = { status: 'error', detail: 'Artifact directory could not be prepared' }
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Durable configured-storage history used to authorize persisted image paths after Profile restarts. */
|
|
2
|
+
|
|
3
|
+
import type { Context, Fiber } from '@deepseek-ai/cordis'
|
|
4
|
+
import { defineDomain } from '@deepseek-ai/dsh-storage-domain'
|
|
5
|
+
import type { DomainGlobal } from '@deepseek-ai/dsh-storage-domain'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { resolveConfig, type VisionToolkitConfig } from './config.ts'
|
|
8
|
+
|
|
9
|
+
const storageHistoryStateSchema = z.object({
|
|
10
|
+
roots: z.array(z.string().min(1)),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
type StorageHistoryState = z.infer<typeof storageHistoryStateSchema>
|
|
14
|
+
|
|
15
|
+
/** Plugin-owned storage roots that survive Settings-provider and Profile restarts. */
|
|
16
|
+
export const storageHistoryDomainSpec = defineDomain({
|
|
17
|
+
name: 'vision_toolkit_storage',
|
|
18
|
+
version: 0,
|
|
19
|
+
global: {
|
|
20
|
+
schema: storageHistoryStateSchema,
|
|
21
|
+
initial: { roots: [] },
|
|
22
|
+
},
|
|
23
|
+
tables: {},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
interface StorageBinding {
|
|
27
|
+
accepting: boolean
|
|
28
|
+
global: DomainGlobal<StorageHistoryState>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sameRoots(left: readonly string[], right: readonly string[]): boolean {
|
|
32
|
+
return left.length === right.length && left.every((root, index) => root === right[index])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Return every configured root that must remain readable, including the active root.
|
|
37
|
+
* @param config - Settings generation to summarize.
|
|
38
|
+
* @returns normalized configured roots in retention order.
|
|
39
|
+
*/
|
|
40
|
+
export function configuredStorageRoots(config: VisionToolkitConfig): string[] {
|
|
41
|
+
const resolved = resolveConfig(config)
|
|
42
|
+
return [...new Set([
|
|
43
|
+
...resolved.storageHistory,
|
|
44
|
+
...(resolved.storageDir === undefined ? [] : [resolved.storageDir]),
|
|
45
|
+
])]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge plugin-owned roots into a Settings generation without retaining its active root as history.
|
|
50
|
+
* @param config - Settings generation being restored.
|
|
51
|
+
* @param durableRoots - roots loaded from the plugin-owned sidecar.
|
|
52
|
+
* @returns the original generation when unchanged, otherwise a generation with restored history.
|
|
53
|
+
*/
|
|
54
|
+
export function restoreDurableStorageHistory(
|
|
55
|
+
config: VisionToolkitConfig,
|
|
56
|
+
durableRoots: readonly string[],
|
|
57
|
+
): VisionToolkitConfig {
|
|
58
|
+
const resolved = resolveConfig(config)
|
|
59
|
+
const storageHistory = [...new Set([...resolved.storageHistory, ...durableRoots])]
|
|
60
|
+
.filter(root => root !== resolved.storageDir)
|
|
61
|
+
if (sameRoots(storageHistory, resolved.storageHistory)) return config
|
|
62
|
+
return { ...config, storageHistory }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Optional storage-domain sidecar for storage roots that Settings cannot persist itself. */
|
|
66
|
+
export class StorageHistoryStore {
|
|
67
|
+
private storage: StorageBinding | undefined
|
|
68
|
+
private storageFiber: (Fiber & PromiseLike<Fiber>) | undefined
|
|
69
|
+
private storageReady: Promise<void> | undefined
|
|
70
|
+
private mutationTail: Promise<void> = Promise.resolve()
|
|
71
|
+
private desiredRoots: readonly string[] | undefined
|
|
72
|
+
private persistenceTicket = 0
|
|
73
|
+
private warned = false
|
|
74
|
+
|
|
75
|
+
constructor(private readonly ctx: Context) {
|
|
76
|
+
if (typeof ctx.inject !== 'function') return
|
|
77
|
+
this.storageFiber = ctx.inject(['storageDomain'], async (storageCtx: Context) => {
|
|
78
|
+
const domain = await storageCtx.storageDomain.open(storageHistoryDomainSpec)
|
|
79
|
+
const binding: StorageBinding = { accepting: true, global: domain.global }
|
|
80
|
+
this.storage = binding
|
|
81
|
+
try {
|
|
82
|
+
if (this.desiredRoots !== undefined) {
|
|
83
|
+
await this.write(binding, this.desiredRoots, this.persistenceTicket)
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
this.storage = undefined
|
|
87
|
+
await domain.close()
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
return async () => {
|
|
91
|
+
binding.accepting = false
|
|
92
|
+
if (this.storage === binding) this.storage = undefined
|
|
93
|
+
await this.mutationTail
|
|
94
|
+
await domain.close()
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
this.storageReady = Promise.resolve(this.storageFiber).then(
|
|
98
|
+
() => undefined,
|
|
99
|
+
(error: unknown) => { this.warnOnce(error) },
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Restore durable roots into one Settings generation before runtime preparation.
|
|
105
|
+
* @param config - Settings generation to restore.
|
|
106
|
+
* @returns the generation with available durable roots merged into its history.
|
|
107
|
+
*/
|
|
108
|
+
async restore(config: VisionToolkitConfig): Promise<VisionToolkitConfig> {
|
|
109
|
+
const binding = await this.prepareStorage()
|
|
110
|
+
return restoreDurableStorageHistory(config, binding?.global.get().roots ?? [])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Persist the active and historical configured roots.
|
|
115
|
+
* @param config - validated generation whose roots must survive restart.
|
|
116
|
+
* @returns false when no storage-domain is available; true after persistence or when there are no roots.
|
|
117
|
+
*/
|
|
118
|
+
async persist(config: VisionToolkitConfig): Promise<boolean> {
|
|
119
|
+
const roots = configuredStorageRoots(config)
|
|
120
|
+
const ticket = ++this.persistenceTicket
|
|
121
|
+
this.desiredRoots = roots
|
|
122
|
+
if (roots.length === 0) return true
|
|
123
|
+
const binding = await this.prepareStorage()
|
|
124
|
+
if (binding === undefined) return false
|
|
125
|
+
await this.write(binding, roots, ticket)
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Release the optional storage-domain binding with the plugin lifecycle. */
|
|
130
|
+
dispose(): void {
|
|
131
|
+
const fiber = this.storageFiber
|
|
132
|
+
this.storageFiber = undefined
|
|
133
|
+
this.storageReady = undefined
|
|
134
|
+
if (fiber !== undefined) void fiber.dispose().catch(error => { this.warnOnce(error) })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async prepareStorage(): Promise<StorageBinding | undefined> {
|
|
138
|
+
const current = this.activeStorage()
|
|
139
|
+
if (current !== undefined) return current
|
|
140
|
+
if (this.ctx.get('storageDomain') === undefined) return undefined
|
|
141
|
+
await this.storageReady
|
|
142
|
+
return this.activeStorage()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private activeStorage(): StorageBinding | undefined {
|
|
146
|
+
return this.storage?.accepting === true ? this.storage : undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private write(binding: StorageBinding, roots: readonly string[], ticket: number): Promise<void> {
|
|
150
|
+
return this.enqueueMutation(async () => {
|
|
151
|
+
if (!binding.accepting) throw new Error('the storage-domain provider changed while storage history was pending')
|
|
152
|
+
if (ticket !== this.persistenceTicket) return
|
|
153
|
+
if (sameRoots(binding.global.get().roots, roots)) return
|
|
154
|
+
await binding.global.set({ roots: [...roots] })
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private enqueueMutation(operation: () => Promise<void>): Promise<void> {
|
|
159
|
+
const result = this.mutationTail.then(operation)
|
|
160
|
+
this.mutationTail = result.then(() => undefined, () => undefined)
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private warnOnce(error: unknown): void {
|
|
165
|
+
if (this.warned) return
|
|
166
|
+
this.warned = true
|
|
167
|
+
this.ctx.logger?.warn(
|
|
168
|
+
'dsh-vision-toolkit: configured storage history sidecar is unavailable. %s',
|
|
169
|
+
(error instanceof Error ? error.message : String(error)).slice(0, 500),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/web.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from './paste-images.ts'
|
|
23
23
|
import {
|
|
24
24
|
resolveConfig,
|
|
25
|
+
retainedStorageHistory,
|
|
25
26
|
isBuiltInFreeVisionProvider,
|
|
26
27
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
27
28
|
type ResolvedVisionToolkitConfig,
|
|
@@ -303,16 +304,22 @@ export class VisionToolkitWebBackend {
|
|
|
303
304
|
|
|
304
305
|
private async save(request: SaveRequest): Promise<VisionToolkitSettingsSnapshot> {
|
|
305
306
|
if (!this.ctx.settings.writable) throw new Error('settings provider is read-only')
|
|
307
|
+
const current = resolveConfig(descriptorOf(this.ctx).value as VisionToolkitConfig)
|
|
308
|
+
const storageHistory = retainedStorageHistory(request.value, current)
|
|
309
|
+
const value: VisionToolkitConfig = {
|
|
310
|
+
...request.value,
|
|
311
|
+
...(storageHistory.length === 0 ? {} : { storageHistory }),
|
|
312
|
+
}
|
|
306
313
|
let candidate: PreparedRuntimeGeneration
|
|
307
314
|
try {
|
|
308
|
-
candidate = await this.manager.prepareCandidate(
|
|
315
|
+
candidate = await this.manager.prepareCandidate(value)
|
|
309
316
|
} catch (error) {
|
|
310
317
|
this.manager.recordFailure(error)
|
|
311
318
|
throw error
|
|
312
319
|
}
|
|
313
320
|
await this.ctx.settings.replace(
|
|
314
321
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
315
|
-
|
|
322
|
+
value as object,
|
|
316
323
|
request.expectedRevision,
|
|
317
324
|
)
|
|
318
325
|
this.manager.activateCandidate(candidate)
|