@goodandready/dsh-image-gen 0.10.35 → 0.11.1
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/lib/client.js +994 -161
- package/lib/comfy-workflow-helpers.js +161 -0
- package/lib/fallback-router.js +25 -3
- package/lib/index.js +259 -138
- package/lib/provider-utils.js +0 -2
- package/lib/providers/backends/local.js +33 -11
- package/lib/providers.js +9 -30
- package/lib/register-tools.js +3 -1
- package/lib/settings-route.js +112 -0
- package/lib/theme-pair-helpers.js +83 -0
- package/lib/tools/anchor.js +1 -2
- package/lib/tools/editing.js +2 -34
- package/lib/tools/frontend.js +2 -35
- package/lib/tools/generation-pack.js +2 -4
- package/lib/tools/generation.js +2 -23
- package/lib/tools/inspect.js +6 -45
- package/lib/tools/pattern.js +10 -0
- package/lib/tools/processing-advanced.js +0 -32
- package/lib/tools/processing-basic.js +2 -25
- package/lib/tools/style-matrix.js +1 -2
- package/lib/tools/theme-pair.js +250 -0
- package/lib/tools/ui-asset.js +1 -1
- package/lib/updater.js +0 -9
- package/lib/vault.js +183 -0
- package/package.json +7 -1
package/lib/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import z from '@deepseek-ai/schemastery'
|
|
16
16
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { readFile } from 'node:fs/promises'
|
|
18
|
+
import { isTrustedLocalRequest } from './security.js'
|
|
19
19
|
import { saveAndAttachResult } from './attachment-helper.js'
|
|
20
20
|
import {
|
|
21
21
|
historyFile,
|
|
@@ -28,8 +28,7 @@ import {
|
|
|
28
28
|
writeHistory,
|
|
29
29
|
filterHistory,
|
|
30
30
|
} from './history.js'
|
|
31
|
-
import { existsSync
|
|
32
|
-
import os from 'node:os'
|
|
31
|
+
import { existsSync } from 'node:fs'
|
|
33
32
|
import path from 'node:path'
|
|
34
33
|
import {
|
|
35
34
|
IMAGE_SIZES,
|
|
@@ -38,15 +37,16 @@ import {
|
|
|
38
37
|
buildSidecar,
|
|
39
38
|
normalizeMediaType,
|
|
40
39
|
resolveApiKeyCandidates,
|
|
41
|
-
toLosslessJson,
|
|
42
|
-
saveAttachmentSafe,
|
|
43
40
|
testProviderConnection,
|
|
44
41
|
} from './providers.js'
|
|
45
42
|
|
|
46
|
-
import {
|
|
43
|
+
import { resolveConversationImage, analyzeImageWithVision } from './resolve-image.js'
|
|
47
44
|
import { registerAllTools } from './register-tools.js'
|
|
48
45
|
import { registerPluginUpdater } from './updater.js'
|
|
49
|
-
|
|
46
|
+
import { registerVaultRoutes } from './vault.js'
|
|
47
|
+
import { registerSettingsRoutes } from './settings-route.js'
|
|
48
|
+
import { clearAllAnchors } from './anchor-helpers.js'
|
|
49
|
+
import { resetLoopGuard, getLoopGuardState } from './loop-guard.js'
|
|
50
50
|
|
|
51
51
|
export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMediaType, resolveConversationImage, analyzeImageWithVision }
|
|
52
52
|
export { saveAndAttachResult }
|
|
@@ -62,13 +62,6 @@ export {
|
|
|
62
62
|
filterHistory,
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Saves generated asset to workspace, registers with attachments, and returns Dual-Output (#150).
|
|
68
|
-
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
65
|
export const name = '@goodandready/dsh-image-gen'
|
|
73
66
|
|
|
74
67
|
/** Settings namespace the Web card edits. */
|
|
@@ -79,45 +72,62 @@ const NS = 'dsh-image-gen'
|
|
|
79
72
|
const LEGACY_NS = 'dsh-fal-image-gen'
|
|
80
73
|
export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm', 'systemPrompt']
|
|
81
74
|
|
|
75
|
+
// Polyfill for .volatile() schema annotation if runtime schemastery lacks it (#295)
|
|
76
|
+
if (typeof z.prototype?.volatile !== 'function') {
|
|
77
|
+
z.prototype.volatile = function volatile() {
|
|
78
|
+
if (this.meta && this.meta.volatile) throw new TypeError('volatile schema is already wrapped')
|
|
79
|
+
return typeof this.extra === 'function' ? this.extra('volatile', true) : this
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
82
83
|
export const Config = z.object({
|
|
83
84
|
enabled: z
|
|
84
85
|
.boolean()
|
|
85
86
|
.description('Master switch for image generation. When false, tools refuse to execute.')
|
|
86
|
-
.default(true)
|
|
87
|
+
.default(true)
|
|
88
|
+
.volatile(),
|
|
87
89
|
provider: z
|
|
88
90
|
.string()
|
|
89
91
|
.description(`Which provider generates the image. One of: ${PROVIDER_KEYS.join(', ')}. `
|
|
90
92
|
+ '"fal" uses the FAL queue below; "custom" uses the OpenAI-compatible API configured under it.')
|
|
91
|
-
.default('fal')
|
|
93
|
+
.default('fal')
|
|
94
|
+
.volatile(),
|
|
92
95
|
model: z
|
|
93
96
|
.string()
|
|
94
97
|
.description('FAL model id, called as {baseURL}/{model}.')
|
|
95
|
-
.default('fal-ai/flux-2/klein/9b')
|
|
98
|
+
.default('fal-ai/flux-2/klein/9b')
|
|
99
|
+
.volatile(),
|
|
96
100
|
apiKeyEnv: z
|
|
97
101
|
.string()
|
|
98
102
|
.role('credential-ref')
|
|
99
103
|
.description('Credential reference / env var holding the FAL API key (the "Key " auth prefix is added automatically when missing).')
|
|
100
|
-
.default('FAL_API_KEY')
|
|
104
|
+
.default('FAL_API_KEY')
|
|
105
|
+
.volatile(),
|
|
101
106
|
baseURL: z
|
|
102
107
|
.string()
|
|
103
108
|
.description('FAL queue base URL.')
|
|
104
|
-
.default('https://queue.fal.run')
|
|
109
|
+
.default('https://queue.fal.run')
|
|
110
|
+
.volatile(),
|
|
105
111
|
defaultSize: z
|
|
106
112
|
.string()
|
|
107
113
|
.description(`Default image size when the tool call omits image_size. One of: ${IMAGE_SIZES.join(', ')}.`)
|
|
108
|
-
.default('landscape_4_3')
|
|
114
|
+
.default('landscape_4_3')
|
|
115
|
+
.volatile(),
|
|
109
116
|
defaultFormat: z
|
|
110
117
|
.string()
|
|
111
118
|
.description(`Default output format. One of: ${OUTPUT_FORMATS.join(', ')}.`)
|
|
112
|
-
.default('png')
|
|
119
|
+
.default('png')
|
|
120
|
+
.volatile(),
|
|
113
121
|
pollIntervalMs: z
|
|
114
122
|
.number()
|
|
115
123
|
.description('Status polling interval in milliseconds.')
|
|
116
|
-
.default(2000)
|
|
124
|
+
.default(2000)
|
|
125
|
+
.volatile(),
|
|
117
126
|
timeoutMs: z
|
|
118
127
|
.number()
|
|
119
128
|
.description('Total generation timeout in milliseconds (submit + poll + download).')
|
|
120
|
-
.default(180000)
|
|
129
|
+
.default(180000)
|
|
130
|
+
.volatile(),
|
|
121
131
|
deliverAs: z
|
|
122
132
|
.string()
|
|
123
133
|
.description(
|
|
@@ -125,151 +135,216 @@ export const Config = z.object({
|
|
|
125
135
|
+ '"link": the tool returns a link and the card renders the picture from it — the chat model only ever sees text, so this works with any model. '
|
|
126
136
|
+ '"image": the tool returns the image itself — the picture is part of the result, which a text-only chat model cannot read, so this mode needs dsh-vision-bridge (or a vision-capable chat model).'
|
|
127
137
|
)
|
|
128
|
-
.default('link')
|
|
138
|
+
.default('link')
|
|
139
|
+
.volatile(),
|
|
129
140
|
customBaseURL: z
|
|
130
141
|
.string()
|
|
131
142
|
.description('provider=custom: API root without a trailing slash, e.g. https://api.openai.com/v1. '
|
|
132
143
|
+ 'The request goes to {customBaseURL}/images/generations.')
|
|
133
|
-
.default('')
|
|
144
|
+
.default('')
|
|
145
|
+
.volatile(),
|
|
134
146
|
customModel: z
|
|
135
147
|
.string()
|
|
136
148
|
.description('provider=custom: model id, e.g. gpt-image-1.')
|
|
137
|
-
.default('')
|
|
149
|
+
.default('')
|
|
150
|
+
.volatile(),
|
|
138
151
|
customKeyEnv: z
|
|
139
152
|
.string()
|
|
140
153
|
.role('credential-ref')
|
|
141
154
|
.description('provider=custom: credential reference / env var holding the API key. '
|
|
142
155
|
+ 'Empty means no authorization header, for gateways that need none.')
|
|
143
|
-
.default('OPENAI_API_KEY')
|
|
156
|
+
.default('OPENAI_API_KEY')
|
|
157
|
+
.volatile(),
|
|
144
158
|
|
|
145
159
|
replicateModel: z
|
|
146
160
|
.string()
|
|
147
161
|
.description('provider=replicate: model identifier on Replicate.')
|
|
148
|
-
.default('black-forest-labs/flux-schnell')
|
|
162
|
+
.default('black-forest-labs/flux-schnell')
|
|
163
|
+
.volatile(),
|
|
149
164
|
replicateKeyEnv: z
|
|
150
165
|
.string()
|
|
151
166
|
.role('credential-ref')
|
|
152
167
|
.description('provider=replicate: credential reference / env var holding API token.')
|
|
153
|
-
.default('REPLICATE_API_TOKEN')
|
|
168
|
+
.default('REPLICATE_API_TOKEN')
|
|
169
|
+
.volatile(),
|
|
154
170
|
|
|
155
171
|
subscriptionQuality: z
|
|
156
172
|
.string()
|
|
157
173
|
.description('provider=codex or grok: quality asked of the subscription — low, medium, high or empty '
|
|
158
174
|
+ 'for the provider default.')
|
|
159
|
-
.default('')
|
|
175
|
+
.default('')
|
|
176
|
+
.volatile(),
|
|
160
177
|
customSize: z
|
|
161
178
|
.string()
|
|
162
179
|
.description('provider=custom: fixed size sent to the API, e.g. 1024x1024. '
|
|
163
180
|
+ 'Empty means the named size is translated automatically — set this only for an API picky about sizes.')
|
|
164
|
-
.default('')
|
|
181
|
+
.default('')
|
|
182
|
+
.volatile(),
|
|
165
183
|
localKind: z
|
|
166
184
|
.string()
|
|
167
185
|
.description('provider=local: which local API to use — "comfyui" or "a1111".')
|
|
168
|
-
.default('comfyui')
|
|
186
|
+
.default('comfyui')
|
|
187
|
+
.volatile(),
|
|
169
188
|
localBaseURL: z
|
|
170
189
|
.string()
|
|
171
190
|
.description('provider=local: server address, e.g. http://127.0.0.1:8188 (ComfyUI) or http://127.0.0.1:7860 (A1111).')
|
|
172
|
-
.default('')
|
|
191
|
+
.default('')
|
|
192
|
+
.volatile(),
|
|
173
193
|
localModel: z
|
|
174
194
|
.string()
|
|
175
195
|
.description('provider=local: model id (A1111) or workflow/schema name (ComfyUI). Empty means the server default.')
|
|
176
|
-
.default('')
|
|
196
|
+
.default('')
|
|
197
|
+
.volatile(),
|
|
177
198
|
localSteps: z
|
|
178
199
|
.number()
|
|
179
200
|
.description('provider=local: sampling steps.')
|
|
180
|
-
.default(20)
|
|
201
|
+
.default(20)
|
|
202
|
+
.volatile(),
|
|
181
203
|
localCfg: z
|
|
182
204
|
.number()
|
|
183
205
|
.description('provider=local: CFG scale.')
|
|
184
|
-
.default(7)
|
|
206
|
+
.default(7)
|
|
207
|
+
.volatile(),
|
|
185
208
|
seedreamBaseURL: z
|
|
186
209
|
.string()
|
|
187
210
|
.description('provider=seedream: base URL, e.g. https://api.bytedanceapi.com/v1 or Volcengine Ark.')
|
|
188
|
-
.default('https://api.bytedanceapi.com/v1')
|
|
211
|
+
.default('https://api.bytedanceapi.com/v1')
|
|
212
|
+
.volatile(),
|
|
189
213
|
seedreamKeyEnv: z
|
|
190
214
|
.string()
|
|
191
215
|
.role('credential-ref')
|
|
192
216
|
.description('provider=seedream: credential reference / env var holding the API key.')
|
|
193
|
-
.default('SEEDREAM_API_KEY')
|
|
217
|
+
.default('SEEDREAM_API_KEY')
|
|
218
|
+
.volatile(),
|
|
194
219
|
seedreamModel: z
|
|
195
220
|
.string()
|
|
196
221
|
.description('provider=seedream: model id, e.g. seedream-4.0.')
|
|
197
|
-
.default('seedream-4.0')
|
|
222
|
+
.default('seedream-4.0')
|
|
223
|
+
.volatile(),
|
|
198
224
|
geminiKeyEnv: z
|
|
199
225
|
.string()
|
|
200
226
|
.role('credential-ref')
|
|
201
227
|
.description('provider=gemini: credential reference / env var holding the Google API key.')
|
|
202
|
-
.default('GEMINI_API_KEY')
|
|
228
|
+
.default('GEMINI_API_KEY')
|
|
229
|
+
.volatile(),
|
|
203
230
|
geminiModel: z
|
|
204
231
|
.string()
|
|
205
232
|
.description('provider=gemini: model id, e.g. gemini-2.0-flash-exp-image-generation.')
|
|
206
|
-
.default('gemini-2.0-flash-exp-image-generation')
|
|
233
|
+
.default('gemini-2.0-flash-exp-image-generation')
|
|
234
|
+
.volatile(),
|
|
207
235
|
outputDir: z
|
|
208
236
|
.string()
|
|
209
237
|
.description('Where generated images are saved. A relative path resolves against the session working directory; an absolute path is used as given.')
|
|
210
|
-
.default('generated/images')
|
|
238
|
+
.default('generated/images')
|
|
239
|
+
.volatile(),
|
|
211
240
|
historyLimit: z
|
|
212
241
|
.number()
|
|
213
242
|
.description('How many recent generations to keep in the in-memory history list.')
|
|
214
|
-
.default(50)
|
|
243
|
+
.default(50)
|
|
244
|
+
.volatile(),
|
|
215
245
|
pruneDays: z
|
|
216
246
|
.number()
|
|
217
247
|
.description('Delete generated files and history entries older than this many days. 0 (default) disables pruning.')
|
|
218
|
-
.default(0)
|
|
248
|
+
.default(0)
|
|
249
|
+
.volatile(),
|
|
219
250
|
enhancePrompt: z
|
|
220
251
|
.boolean()
|
|
221
252
|
.description('Expand a short prompt into a detailed one through the chat model before generating. Off by default.')
|
|
222
|
-
.default(false)
|
|
253
|
+
.default(false)
|
|
254
|
+
.volatile(),
|
|
223
255
|
enhanceModel: z
|
|
224
256
|
.string()
|
|
225
257
|
.description('Model used to enhance the prompt. Empty means the same model that leads the conversation.')
|
|
226
|
-
.default('')
|
|
258
|
+
.default('')
|
|
259
|
+
.volatile(),
|
|
227
260
|
autoEnhancePrompt: z
|
|
228
261
|
.boolean()
|
|
229
262
|
.description('Enrich prompts with photography, lighting, and composition quality tokens.')
|
|
230
|
-
.default(false)
|
|
263
|
+
.default(false)
|
|
264
|
+
.volatile(),
|
|
231
265
|
defaultStylePreset: z
|
|
232
266
|
.string()
|
|
233
267
|
.description('Default style preset applied to generations (e.g. cinematic, anime, photorealistic).')
|
|
234
|
-
.default('none')
|
|
268
|
+
.default('none')
|
|
269
|
+
.volatile(),
|
|
235
270
|
enhanceBelowChars: z
|
|
236
271
|
.number()
|
|
237
272
|
.description('Only enhance prompts shorter than this many characters.')
|
|
238
|
-
.default(200)
|
|
273
|
+
.default(200)
|
|
274
|
+
.volatile(),
|
|
239
275
|
stylePreset: z
|
|
240
276
|
.string()
|
|
241
277
|
.description('Optional style suffix appended to the prompt before generation. Empty (default) means no style is applied and the prompt is used as-is.')
|
|
242
|
-
.default('')
|
|
278
|
+
.default('')
|
|
279
|
+
.volatile(),
|
|
243
280
|
cacheBySeed: z
|
|
244
281
|
.boolean()
|
|
245
282
|
.description('If the same seed+prompt was already generated (present in history), return the cached result instead of generating again. Off by default.')
|
|
246
|
-
.default(false)
|
|
283
|
+
.default(false)
|
|
284
|
+
.volatile(),
|
|
247
285
|
cacheByPrompt: z
|
|
248
286
|
.boolean()
|
|
249
287
|
.description('If the same prompt was already generated (present in history), return the cached result instead of generating again. Off by default.')
|
|
250
|
-
.default(false)
|
|
288
|
+
.default(false)
|
|
289
|
+
.volatile(),
|
|
251
290
|
qualityGate: z
|
|
252
291
|
.boolean()
|
|
253
292
|
.description('Automatic quality gate with silent re-roll for blank or defective frames. On by default.')
|
|
254
|
-
.default(true)
|
|
293
|
+
.default(true)
|
|
294
|
+
.volatile(),
|
|
255
295
|
dailyBudgetUsd: z
|
|
256
296
|
.number()
|
|
257
297
|
.description('Daily image generation spending budget in USD. 0 (default) disables limit enforcement.')
|
|
258
|
-
.default(0)
|
|
298
|
+
.default(0)
|
|
299
|
+
.volatile(),
|
|
259
300
|
loopGuardLimit: z
|
|
260
301
|
.number()
|
|
261
302
|
.description('Maximum consecutive image generations per session without user interaction. 0 disables protection.')
|
|
262
|
-
.default(3)
|
|
303
|
+
.default(3)
|
|
304
|
+
.volatile(),
|
|
263
305
|
diskCache: z
|
|
264
306
|
.boolean()
|
|
265
307
|
.description('Content-addressed disk caching for identical generations (<50ms retrieval, zero API cost). On by default.')
|
|
266
|
-
.default(true)
|
|
308
|
+
.default(true)
|
|
309
|
+
.volatile(),
|
|
267
310
|
fallbackProviders: z
|
|
268
311
|
.array(z.string())
|
|
269
312
|
.description('Ordered list of fallback providers to cascade to if the primary encounters 429, 5xx, or quota limits.')
|
|
270
|
-
.default([])
|
|
313
|
+
.default([])
|
|
314
|
+
.volatile(),
|
|
271
315
|
})
|
|
272
316
|
|
|
317
|
+
function isVolatileRef(value) {
|
|
318
|
+
return !!value && typeof value === 'object' && !Array.isArray(value) && typeof value.get === 'function'
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function plainConfig(cfg) {
|
|
322
|
+
if (isVolatileRef(cfg)) return plainConfig(cfg.get())
|
|
323
|
+
if (!cfg || typeof cfg !== 'object') return cfg
|
|
324
|
+
const out = {}
|
|
325
|
+
for (const key of Object.keys(cfg)) {
|
|
326
|
+
const value = cfg[key]
|
|
327
|
+
out[key] = isVolatileRef(value) ? value.get() : value
|
|
328
|
+
}
|
|
329
|
+
return out
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function volatileConfig(cfg) {
|
|
333
|
+
const plain = plainConfig(cfg)
|
|
334
|
+
if (!plain || typeof plain !== 'object') return plain
|
|
335
|
+
const out = {}
|
|
336
|
+
for (const [key, field] of Object.entries(Config.dict || {})) {
|
|
337
|
+
if (field?.meta?.volatile && Object.hasOwn(plain, key)) {
|
|
338
|
+
out[key] = plain[key]
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function publicConfig(cfg) {
|
|
345
|
+
return plainConfig(cfg)
|
|
346
|
+
}
|
|
347
|
+
|
|
273
348
|
/** Keep a file stem safe for the filesystem. */
|
|
274
349
|
/** Read source image for editing: filesystem path or attachment id. */
|
|
275
350
|
export async function resolveSource(ctx, exec, ref) {
|
|
@@ -307,43 +382,33 @@ export async function resolveApiKey(ctx, ref) {
|
|
|
307
382
|
try {
|
|
308
383
|
const resolved = await ctx.credentials.resolve(credentialRef(candidate))
|
|
309
384
|
if (resolved && resolved.value) return resolved.value
|
|
310
|
-
} catch {
|
|
311
|
-
//
|
|
385
|
+
} catch (_) {
|
|
386
|
+
// Credential reference not found in service, try next candidate
|
|
312
387
|
}
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
388
|
+
const envVal = process.env[candidate]
|
|
389
|
+
if (envVal) return envVal
|
|
315
390
|
}
|
|
316
|
-
|
|
317
|
-
`API key not configured: set credential/env "${ref}" (Web: Settings → Credentials, or add "${ref}: <key>" to $DSH_HOME/.credentials.yaml)`,
|
|
318
|
-
)
|
|
391
|
+
return ''
|
|
319
392
|
}
|
|
320
393
|
|
|
321
394
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* distinguishable only from user overrides. If settings are already configured
|
|
326
|
-
* under the new namespace, preserve them as newer choices.
|
|
327
|
-
*
|
|
328
|
-
* The legacy block is left untouched in settings to avoid unexpected deletions.
|
|
329
|
-
* and causes no conflict with the active namespace.
|
|
395
|
+
* If the user configured the plugin under its old name (dsh-fal-image-gen), copy
|
|
396
|
+
* those settings forward into dsh-image-gen once so existing keys and choices
|
|
397
|
+
* are preserved. Only touches values that the user actually modified.
|
|
330
398
|
*/
|
|
331
399
|
function migrateLegacySettings(sctx, scope) {
|
|
332
400
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (mine && typeof mine === 'object' && Object.keys(mine).length > 0) return
|
|
401
|
+
if (!scope || typeof scope.update !== 'function') return
|
|
402
|
+
const legacyScope = sctx.settings.get?.(LEGACY_NS)
|
|
403
|
+
if (!legacyScope) return
|
|
404
|
+
const legacy = legacyScope.get?.()
|
|
405
|
+
if (!legacy || typeof legacy !== 'object') return
|
|
339
406
|
scope.update(structuredClone(legacy))
|
|
340
|
-
} catch (
|
|
341
|
-
// Settings migration skipped — fallback to default schema configuration
|
|
342
|
-
// and user may configure settings via UI card. No crash required.
|
|
407
|
+
} catch (_) {
|
|
408
|
+
// Settings migration skipped — fallback to default schema configuration
|
|
343
409
|
}
|
|
344
410
|
}
|
|
345
411
|
|
|
346
|
-
/** History directory: persists across restarts. */
|
|
347
412
|
/** Collect text chunks from llm.stream iterator. */
|
|
348
413
|
export async function collectText(iterable) {
|
|
349
414
|
let out = ''
|
|
@@ -388,7 +453,7 @@ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
|
|
|
388
453
|
const text = await collectText(chunks)
|
|
389
454
|
if (!text) return { prompt, enhanced: false }
|
|
390
455
|
return { prompt: text, enhanced: true }
|
|
391
|
-
} catch (
|
|
456
|
+
} catch (_) {
|
|
392
457
|
return { prompt, enhanced: false }
|
|
393
458
|
}
|
|
394
459
|
}
|
|
@@ -405,29 +470,79 @@ export function apply(ctx, config) {
|
|
|
405
470
|
})
|
|
406
471
|
}
|
|
407
472
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
473
|
+
let settingsApi
|
|
474
|
+
let currentLiveConfig = null
|
|
475
|
+
let getConfig = () => currentLiveConfig || config
|
|
476
|
+
const live = () => Config(structuredClone(plainConfig(getConfig() ?? {}))) ?? config
|
|
477
|
+
|
|
478
|
+
const createSettingsAdapter = (svc) => {
|
|
479
|
+
if (!svc) return undefined
|
|
480
|
+
if (typeof svc.replace === 'function' || typeof svc.update === 'function' || typeof svc.mutate === 'function') {
|
|
481
|
+
const getRevision = () => {
|
|
482
|
+
try {
|
|
483
|
+
return svc.describe?.().find((row) => row.ns === NS)?.revision
|
|
484
|
+
} catch (_) {
|
|
485
|
+
return undefined
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
get: () => live(),
|
|
490
|
+
replace: async (next) => {
|
|
491
|
+
const parsed = Config(structuredClone(plainConfig(next)))
|
|
492
|
+
currentLiveConfig = parsed
|
|
493
|
+
const payload = volatileConfig(parsed)
|
|
494
|
+
if (typeof svc.replace === 'function') {
|
|
495
|
+
await svc.replace(NS, payload, getRevision())
|
|
496
|
+
} else if (typeof svc.update === 'function') {
|
|
497
|
+
await svc.update(NS, payload, getRevision())
|
|
498
|
+
}
|
|
499
|
+
return parsed
|
|
500
|
+
},
|
|
501
|
+
update: async (patch) => {
|
|
502
|
+
const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
|
|
503
|
+
currentLiveConfig = merged
|
|
504
|
+
const payload = volatileConfig(merged)
|
|
505
|
+
if (typeof svc.update === 'function') {
|
|
506
|
+
await svc.update(NS, payload, getRevision())
|
|
507
|
+
} else if (typeof svc.replace === 'function') {
|
|
508
|
+
await svc.replace(NS, payload, getRevision())
|
|
509
|
+
}
|
|
510
|
+
return merged
|
|
511
|
+
},
|
|
512
|
+
watch: (cb) => {
|
|
513
|
+
if (typeof svc.watch === 'function') return svc.watch(cb)
|
|
514
|
+
return () => {}
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return undefined
|
|
519
|
+
}
|
|
415
520
|
|
|
416
521
|
ctx.inject(['settings'], (sctx) => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
getConfig = () => config
|
|
422
|
-
|
|
522
|
+
if (typeof sctx.settings?.register === 'function') {
|
|
523
|
+
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
524
|
+
settingsApi = scope
|
|
525
|
+
migrateLegacySettings(sctx, scope)
|
|
526
|
+
getConfig = () => (scope?.get?.() ?? config) ?? config
|
|
527
|
+
sctx.effect(() => () => {
|
|
528
|
+
getConfig = () => config
|
|
529
|
+
settingsApi = undefined
|
|
530
|
+
})
|
|
531
|
+
} else {
|
|
532
|
+
settingsApi = createSettingsAdapter(sctx.settings)
|
|
533
|
+
if (typeof sctx.settings?.describe === 'function') {
|
|
534
|
+
try {
|
|
535
|
+
sctx.settings.describe(NS, Config)
|
|
536
|
+
} catch (_) { /* bestEffort */ }
|
|
537
|
+
}
|
|
538
|
+
sctx.effect(() => () => {
|
|
539
|
+
getConfig = () => config
|
|
540
|
+
settingsApi = undefined
|
|
541
|
+
})
|
|
542
|
+
}
|
|
423
543
|
})
|
|
424
544
|
|
|
425
|
-
// Serve the stored image so the tool card can show it inline.
|
|
426
|
-
// not render image blocks — only assistant messages do — so the picture a
|
|
427
|
-
// tool produces needs a URL of its own.
|
|
428
|
-
//
|
|
429
|
-
// Ids are content-addressed (`sha256:<hex>`), the store verifies them, and
|
|
430
|
-
// the route is same-origin like every other plugin route.
|
|
545
|
+
// Serve the stored image so the tool card can show it inline.
|
|
431
546
|
const imageHandler = (() => {
|
|
432
547
|
return async (req, res) => {
|
|
433
548
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
@@ -447,47 +562,37 @@ export function apply(ctx, config) {
|
|
|
447
562
|
res.end(JSON.stringify({ error: 'bad attachment id' }))
|
|
448
563
|
return
|
|
449
564
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const rawW = Number(query.get('w'))
|
|
457
|
-
const rawH = Number(query.get('h'))
|
|
458
|
-
const safeBytes = Number.isFinite(rawBytes) && rawBytes >= 0 ? rawBytes : 0
|
|
459
|
-
const safeW = Number.isFinite(rawW) && rawW >= 0 ? rawW : 0
|
|
460
|
-
const safeH = Number.isFinite(rawH) && rawH >= 0 ? rawH : 0
|
|
461
|
-
|
|
462
|
-
const ref = {
|
|
463
|
-
attachmentId: id,
|
|
464
|
-
mediaType: query.get('mt') || 'image/png',
|
|
465
|
-
bytes: safeBytes,
|
|
466
|
-
width: safeW,
|
|
467
|
-
height: safeH,
|
|
468
|
-
}
|
|
565
|
+
|
|
566
|
+
const mediaType = query.get('mediaType') ?? 'image/png'
|
|
567
|
+
const bytes = Number(query.get('bytes') ?? 0)
|
|
568
|
+
const width = Number(query.get('width') ?? 0)
|
|
569
|
+
const height = Number(query.get('height') ?? 0)
|
|
570
|
+
|
|
469
571
|
try {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
572
|
+
const stored = await ctx.attachments.readImage({
|
|
573
|
+
attachmentId: id,
|
|
574
|
+
mediaType,
|
|
575
|
+
bytes,
|
|
576
|
+
width,
|
|
577
|
+
height,
|
|
578
|
+
})
|
|
476
579
|
res.writeHead(200, {
|
|
477
|
-
'Content-Type': stored.ref?.mediaType ||
|
|
478
|
-
|
|
580
|
+
'Content-Type': stored.ref?.mediaType || mediaType,
|
|
581
|
+
'Content-Length': stored.data.byteLength,
|
|
479
582
|
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
480
583
|
})
|
|
584
|
+
if (req.method === 'HEAD') {
|
|
585
|
+
res.end()
|
|
586
|
+
return
|
|
587
|
+
}
|
|
481
588
|
res.end(Buffer.isBuffer(stored.data) ? stored.data : Buffer.from(stored.data))
|
|
482
|
-
} catch (
|
|
589
|
+
} catch (_) {
|
|
483
590
|
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
484
|
-
res.end(JSON.stringify({ error: '
|
|
591
|
+
res.end(JSON.stringify({ error: 'attachment not found' }))
|
|
485
592
|
}
|
|
486
593
|
}
|
|
487
594
|
})()
|
|
488
595
|
|
|
489
|
-
// Two routes, one handler: new route for new messages, legacy route for
|
|
490
|
-
// backward compatibility with conversation history.
|
|
491
596
|
for (const path of ['/dsh-image-gen/image', '/dsh-fal-image-gen/image']) {
|
|
492
597
|
ctx.effect(() => ctx.webServer.register({
|
|
493
598
|
kind: 'exact',
|
|
@@ -496,7 +601,14 @@ export function apply(ctx, config) {
|
|
|
496
601
|
}), `dsh-image-gen: image route ${path}`)
|
|
497
602
|
}
|
|
498
603
|
|
|
499
|
-
|
|
604
|
+
// Settings REST routes (#295)
|
|
605
|
+
registerSettingsRoutes(ctx, {
|
|
606
|
+
live,
|
|
607
|
+
getSettingsApi: () => settingsApi,
|
|
608
|
+
setLiveConfig: (c) => { currentLiveConfig = c },
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// One-click plugin updater route per DSH standard
|
|
500
612
|
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
501
613
|
endpoint: '/api/dsh-image-gen/update',
|
|
502
614
|
packageName: '@goodandready/dsh-image-gen',
|
|
@@ -524,12 +636,13 @@ export function apply(ctx, config) {
|
|
|
524
636
|
const deps = {
|
|
525
637
|
fetchImpl: fetch,
|
|
526
638
|
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
527
|
-
cfg: (typeof getConfig ===
|
|
639
|
+
cfg: (typeof getConfig === 'function') ? getConfig() : config,
|
|
528
640
|
ctx,
|
|
529
641
|
}
|
|
530
642
|
const result = await testProviderConnection(deps, provider)
|
|
643
|
+
const guardState = getLoopGuardState(provider)
|
|
531
644
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
532
|
-
res.end(JSON.stringify({ provider, ...result }))
|
|
645
|
+
res.end(JSON.stringify({ provider, loopGuard: guardState, ...result }))
|
|
533
646
|
} catch (err) {
|
|
534
647
|
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
535
648
|
res.end(JSON.stringify({ ok: false, error: err.message }))
|
|
@@ -566,6 +679,15 @@ export function apply(ctx, config) {
|
|
|
566
679
|
},
|
|
567
680
|
}), 'dsh-image-gen: history route')
|
|
568
681
|
|
|
682
|
+
// Vault route (#159)
|
|
683
|
+
registerVaultRoutes(ctx)
|
|
684
|
+
|
|
685
|
+
// Session and cache teardown cleanup effect
|
|
686
|
+
ctx.effect(() => () => {
|
|
687
|
+
clearAllAnchors()
|
|
688
|
+
resetLoopGuard()
|
|
689
|
+
}, 'dsh-image-gen: session cleanup')
|
|
690
|
+
|
|
569
691
|
// Tool registrations live in register-tools.js; each tool is a labeled ctx.effect (#216).
|
|
570
692
|
registerAllTools(ctx, {
|
|
571
693
|
config,
|
|
@@ -575,5 +697,4 @@ export function apply(ctx, config) {
|
|
|
575
697
|
slugify,
|
|
576
698
|
resolveApiKey,
|
|
577
699
|
})
|
|
578
|
-
|
|
579
700
|
}
|
package/lib/provider-utils.js
CHANGED
|
@@ -53,14 +53,12 @@ export function estimateSharpnessAndVariance(bytes) {
|
|
|
53
53
|
if (!buf || buf.length < 64) {
|
|
54
54
|
return { score: 0, passed: false, isBlank: true, reason: 'Empty or corrupt image buffer' }
|
|
55
55
|
}
|
|
56
|
-
let sum = 0
|
|
57
56
|
let diffSum = 0
|
|
58
57
|
const sampleStep = Math.max(1, Math.floor(buf.length / 4096))
|
|
59
58
|
let count = 0
|
|
60
59
|
for (let i = 8; i < buf.length - sampleStep; i += sampleStep) {
|
|
61
60
|
const v = buf[i]
|
|
62
61
|
const nextV = buf[i + sampleStep]
|
|
63
|
-
sum += v
|
|
64
62
|
diffSum += Math.abs(v - nextV)
|
|
65
63
|
count++
|
|
66
64
|
}
|