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