@goodandready/dsh-image-gen 0.11.23 → 0.11.25
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/attachment-helper.js +5 -1
- package/lib/client.js +97 -7
- package/lib/commands.js +9 -3
- package/lib/config-schema.js +50 -2
- package/lib/generation-helpers.js +16 -10
- package/lib/history.js +55 -5
- package/lib/index.js +93 -42
- package/lib/provider-utils.js +2 -0
- package/lib/providers/backends/custom.js +17 -4
- package/lib/providers/backends/gemini.js +45 -4
- package/lib/providers/shared-helpers.js +64 -2
- package/lib/providers.js +21 -29
- package/lib/register-tools.js +106 -37
- package/lib/settings-route.js +54 -4
- package/lib/tools/generation.js +52 -28
- package/lib/vault.js +19 -17
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SIZE_PIXELS,
|
|
5
5
|
formatErrorMessage,
|
|
6
6
|
buildEndpointUrl,
|
|
7
|
+
ensureImageFormat,
|
|
7
8
|
} from '../shared-helpers.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -49,6 +50,7 @@ export function createCustomGenerator(deps, job) {
|
|
|
49
50
|
prompt: promptArg,
|
|
50
51
|
n: 1,
|
|
51
52
|
size: cfg.customSize || (aspectPixels ? aspectPixels.join('x') : SIZE_PIXELS[size]) || size,
|
|
53
|
+
...(format ? { output_format: format } : {}),
|
|
52
54
|
...(negativePrompt !== undefined ? { negative_prompt: negativePrompt } : {}),
|
|
53
55
|
...(guidanceScale !== undefined ? { guidance_scale: guidanceScale } : {}),
|
|
54
56
|
...(quality && quality !== 'auto' ? { quality } : {}),
|
|
@@ -72,9 +74,14 @@ export function createCustomGenerator(deps, job) {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
if (item.b64_json) {
|
|
77
|
+
const rawBytes = Buffer.from(item.b64_json, 'base64')
|
|
78
|
+
const targetFormat = format || data.output_format
|
|
79
|
+
const formatted = await ensureImageFormat(rawBytes, targetFormat, {
|
|
80
|
+
fallbackMime: normalizeMediaType(data.output_format || '', format, rawBytes),
|
|
81
|
+
})
|
|
75
82
|
return {
|
|
76
|
-
bytes:
|
|
77
|
-
mediaType:
|
|
83
|
+
bytes: formatted.bytes,
|
|
84
|
+
mediaType: formatted.mediaType,
|
|
78
85
|
width: 0,
|
|
79
86
|
height: 0,
|
|
80
87
|
seed: seedArg ?? 0,
|
|
@@ -91,9 +98,15 @@ export function createCustomGenerator(deps, job) {
|
|
|
91
98
|
const contentType = download.headers && typeof download.headers.get === 'function'
|
|
92
99
|
? download.headers.get('content-type')
|
|
93
100
|
: ''
|
|
101
|
+
const rawBytes = Buffer.from(await download.arrayBuffer())
|
|
102
|
+
const downloadFormat = contentType ? contentType.replace('image/', '').trim().toLowerCase() : undefined
|
|
103
|
+
const targetFormat = (format && format !== 'png') ? format : (downloadFormat || format)
|
|
104
|
+
const formatted = await ensureImageFormat(rawBytes, targetFormat, {
|
|
105
|
+
fallbackMime: normalizeMediaType(contentType, format),
|
|
106
|
+
})
|
|
94
107
|
return {
|
|
95
|
-
bytes:
|
|
96
|
-
mediaType:
|
|
108
|
+
bytes: formatted.bytes,
|
|
109
|
+
mediaType: formatted.mediaType,
|
|
97
110
|
width: 0,
|
|
98
111
|
height: 0,
|
|
99
112
|
seed: seedArg ?? 0,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeMediaType,
|
|
3
|
+
ensureImageFormat,
|
|
3
4
|
} from '../shared-helpers.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -14,19 +15,49 @@ export function createGeminiGenerator(deps, job) {
|
|
|
14
15
|
|
|
15
16
|
async function gemini(seedArg = seed, promptArg = prompt) {
|
|
16
17
|
const key = await resolveKey(cfg.geminiKeyEnv)
|
|
17
|
-
|
|
18
|
+
let model = cfg.geminiModel || 'gemini-2.0-flash-exp-image-generation'
|
|
19
|
+
// #364: Transparently migrate deprecated Imagen 3/4 IDs to Gemini multimodal model
|
|
20
|
+
if (model.startsWith('imagen-') || model === 'imagen-3.0-generate-002') {
|
|
21
|
+
model = 'gemini-2.0-flash-exp-image-generation'
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const timeoutSignal = typeof AbortSignal !== 'undefined' && AbortSignal.timeout
|
|
19
25
|
? AbortSignal.timeout(cfg.timeoutMs || 120000)
|
|
20
26
|
: undefined
|
|
21
27
|
const effectiveSignal = signal && timeoutSignal && AbortSignal.any
|
|
22
28
|
? AbortSignal.any([signal, timeoutSignal])
|
|
23
29
|
: (signal || timeoutSignal)
|
|
30
|
+
|
|
31
|
+
const parts = []
|
|
32
|
+
if (source && source.bytes) {
|
|
33
|
+
const sourceMime = source.mediaType || 'image/png'
|
|
34
|
+
const sourceBase64 = Buffer.isBuffer(source.bytes) ? source.bytes.toString('base64') : Buffer.from(source.bytes).toString('base64')
|
|
35
|
+
parts.push({
|
|
36
|
+
inlineData: {
|
|
37
|
+
mimeType: sourceMime,
|
|
38
|
+
data: sourceBase64,
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
parts.push({ text: promptArg })
|
|
43
|
+
|
|
44
|
+
// Build unified imageConfig without quality/style overwrite collisions (#364)
|
|
45
|
+
const imageConfig = {}
|
|
46
|
+
if (aspectRatio) imageConfig.aspectRatio = aspectRatio
|
|
47
|
+
if (quality && quality !== 'auto') imageConfig.imageQuality = quality
|
|
48
|
+
if (style && style !== 'none') imageConfig.imageStyle = style
|
|
49
|
+
|
|
50
|
+
const generationConfig = {
|
|
51
|
+
responseModalities: ['IMAGE'],
|
|
52
|
+
...(Object.keys(imageConfig).length > 0 ? { imageConfig } : {}),
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
const res = await fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`, {
|
|
25
56
|
method: 'POST',
|
|
26
57
|
headers: { 'Content-Type': 'application/json' },
|
|
27
58
|
body: JSON.stringify({
|
|
28
|
-
contents: [{ parts
|
|
29
|
-
generationConfig
|
|
59
|
+
contents: [{ parts }],
|
|
60
|
+
generationConfig,
|
|
30
61
|
}),
|
|
31
62
|
signal: effectiveSignal,
|
|
32
63
|
})
|
|
@@ -37,7 +68,17 @@ export function createGeminiGenerator(deps, job) {
|
|
|
37
68
|
}
|
|
38
69
|
const part = data?.candidates?.[0]?.content?.parts?.find((p) => p.inlineData?.data)
|
|
39
70
|
if (!part) throw new Error('Gemini returned no image')
|
|
40
|
-
|
|
71
|
+
|
|
72
|
+
const rawBytes = Buffer.from(part.inlineData.data, 'base64')
|
|
73
|
+
const formatted = await ensureImageFormat(rawBytes, format || part.inlineData.mimeType)
|
|
74
|
+
return {
|
|
75
|
+
bytes: formatted.bytes,
|
|
76
|
+
mediaType: formatted.mediaType,
|
|
77
|
+
width: 0,
|
|
78
|
+
height: 0,
|
|
79
|
+
seed: seedArg ?? 0,
|
|
80
|
+
sourceUrl: '',
|
|
81
|
+
}
|
|
41
82
|
}
|
|
42
83
|
return gemini
|
|
43
84
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sharp from 'sharp'
|
|
1
2
|
/**
|
|
2
3
|
* Shared provider HTTP/auth helpers used by per-backend factories.
|
|
3
4
|
* Extracted from providers.js for #218 modularization.
|
|
@@ -25,16 +26,77 @@ export function falAuthHeader(key) {
|
|
|
25
26
|
: `Key ${trimmed}`
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function
|
|
29
|
+
export function detectImageMediaType(buf) {
|
|
30
|
+
if (!buf || (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array))) return undefined
|
|
31
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf)
|
|
32
|
+
if (b.length < 4) return undefined
|
|
33
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'image/png'
|
|
34
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'image/jpeg'
|
|
35
|
+
if (b.length >= 12 && b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
|
|
36
|
+
b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50) {
|
|
37
|
+
return 'image/webp'
|
|
38
|
+
}
|
|
39
|
+
if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x38) return 'image/gif'
|
|
40
|
+
if (b.length > 5) {
|
|
41
|
+
const head = b.subarray(0, Math.min(256, b.length)).toString('utf8').trimStart()
|
|
42
|
+
if (head.startsWith('<svg') || (head.startsWith('<?xml') && head.includes('<svg'))) {
|
|
43
|
+
return 'image/svg+xml'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeMediaType(contentType, fallbackFormat, bytes) {
|
|
50
|
+
if (bytes) {
|
|
51
|
+
const detected = detectImageMediaType(bytes)
|
|
52
|
+
if (detected) return detected
|
|
53
|
+
}
|
|
29
54
|
const raw = String(contentType ?? '').toLowerCase()
|
|
30
55
|
if (raw.includes('jpeg') || raw.includes('jpg')) return 'image/jpeg'
|
|
31
56
|
if (raw.includes('webp')) return 'image/webp'
|
|
32
57
|
if (raw.includes('png')) return 'image/png'
|
|
33
|
-
if (fallbackFormat === 'jpeg') return 'image/jpeg'
|
|
58
|
+
if (fallbackFormat === 'jpeg' || fallbackFormat === 'jpg') return 'image/jpeg'
|
|
34
59
|
if (fallbackFormat === 'webp') return 'image/webp'
|
|
60
|
+
if (fallbackFormat === 'png') return 'image/png'
|
|
35
61
|
return 'image/png'
|
|
36
62
|
}
|
|
37
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Ensures image bytes match the requested target format ('png', 'jpeg', 'webp').
|
|
66
|
+
* If bytes already match target format, returns them with truthful mediaType.
|
|
67
|
+
* If bytes differ (e.g. provider returned PNG for a webp request), transcodes via Sharp (#386).
|
|
68
|
+
*/
|
|
69
|
+
export async function ensureImageFormat(bytes, targetFormat, { fallbackMime } = {}) {
|
|
70
|
+
if (!bytes) return { bytes: Buffer.alloc(0), mediaType: fallbackMime || 'image/png' }
|
|
71
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
|
|
72
|
+
const actualMime = detectImageMediaType(buf)
|
|
73
|
+
if (!targetFormat && !fallbackMime) return { bytes: buf, mediaType: actualMime || 'image/png' }
|
|
74
|
+
|
|
75
|
+
const rawFormat = String(targetFormat || '').trim().toLowerCase()
|
|
76
|
+
const canonical = rawFormat === 'jpg' ? 'jpeg' : rawFormat
|
|
77
|
+
|
|
78
|
+
if (canonical && !['png', 'jpeg', 'webp'].includes(canonical)) {
|
|
79
|
+
throw new Error(`Unsupported output format "${targetFormat}". Supported formats: png, jpeg, webp`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const targetMime = canonical ? `image/${canonical}` : fallbackMime
|
|
83
|
+
if (actualMime === targetMime) {
|
|
84
|
+
return { bytes: buf, mediaType: targetMime }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (!canonical) return { bytes: buf, mediaType: fallbackMime || actualMime || 'image/png' }
|
|
89
|
+
const transcoded = await sharp(buf).toFormat(canonical).toBuffer()
|
|
90
|
+
const verified = detectImageMediaType(transcoded) || targetMime
|
|
91
|
+
return { bytes: transcoded, mediaType: verified }
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (buf.length < 64) {
|
|
94
|
+
return { bytes: buf, mediaType: fallbackMime || targetMime || actualMime || 'image/png' }
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Failed to encode image to ${canonical}: ${err.message}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
38
100
|
export async function submitJob(fetchImpl, baseURL, model, key, body, signal) {
|
|
39
101
|
const res = await fetchImpl(`${baseURL}/${model}`, {
|
|
40
102
|
method: 'POST',
|
package/lib/providers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CURATED_STYLES, normalizeStyleKey } from './prompt-polisher.js'
|
|
1
2
|
/**
|
|
2
3
|
* Returns candidate API key names considering known aliases (e.g. FAL_API_KEY <-> FAL_KEY).
|
|
3
4
|
*/
|
|
@@ -27,6 +28,8 @@ import { createReplicateGenerator } from './providers/backends/replicate.js'
|
|
|
27
28
|
export {
|
|
28
29
|
falAuthHeader,
|
|
29
30
|
normalizeMediaType,
|
|
31
|
+
detectImageMediaType,
|
|
32
|
+
ensureImageFormat,
|
|
30
33
|
submitJob,
|
|
31
34
|
pollStatus,
|
|
32
35
|
buildEditForm,
|
|
@@ -288,47 +291,36 @@ export function traceToSvg(imageBytes, { colorMode = 'color', paletteSize = 16,
|
|
|
288
291
|
|
|
289
292
|
|
|
290
293
|
export const STYLE_PRESETS = {
|
|
294
|
+
...CURATED_STYLES,
|
|
291
295
|
cinematic: {
|
|
296
|
+
...CURATED_STYLES.cinematic,
|
|
292
297
|
promptSuffix: 'cinematic film still, 35mm photograph, dramatic lighting, highly detailed',
|
|
293
298
|
negativePrompt: 'cartoon, illustration, 3d render, oversaturated, blurry, distorted',
|
|
294
299
|
guidanceScale: 6.5,
|
|
295
300
|
},
|
|
296
|
-
|
|
297
|
-
promptSuffix: 'anime aesthetic, Makoto Shinkai style, vibrant colors, detailed lineart, masterpiece',
|
|
298
|
-
negativePrompt: 'photorealistic, 3d, realistic photo, noisy, deformed, lowres',
|
|
299
|
-
guidanceScale: 7.0,
|
|
300
|
-
},
|
|
301
|
-
isometric: {
|
|
301
|
+
isometric: CURATED_STYLES.isometric_3d || {
|
|
302
302
|
promptSuffix: 'isometric 3D render, clay style, soft studio lighting, clean ambient occlusion',
|
|
303
303
|
negativePrompt: 'flat, 2d, sketch, photograph, noisy background',
|
|
304
304
|
guidanceScale: 7.0,
|
|
305
305
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
promptSuffix: '16-bit retro pixel art, crisp pixels, dithered shading, nostalgic game palette',
|
|
313
|
-
negativePrompt: 'smooth, vector, 3d render, blurry, photorealistic',
|
|
314
|
-
guidanceScale: 8.0,
|
|
315
|
-
},
|
|
316
|
-
oil_painting: {
|
|
317
|
-
promptSuffix: 'classical oil painting, visible canvas texture, rich brushstrokes, fine art',
|
|
318
|
-
negativePrompt: 'photo, digital render, 3d, plastic, anime, flat vector',
|
|
319
|
-
guidanceScale: 7.0,
|
|
320
|
-
},
|
|
321
|
-
minimalist: {
|
|
322
|
-
promptSuffix: 'minimalist graphic design, clean lines, flat vector illustration, elegant geometry',
|
|
323
|
-
negativePrompt: 'busy, cluttered, photorealistic, 3d, noisy texture, gradient overload',
|
|
324
|
-
guidanceScale: 7.0,
|
|
325
|
-
},
|
|
306
|
+
vector: CURATED_STYLES.minimalist_vector,
|
|
307
|
+
minimalist: CURATED_STYLES.minimalist_vector,
|
|
308
|
+
film: CURATED_STYLES.analog_film,
|
|
309
|
+
analog: CURATED_STYLES.analog_film,
|
|
310
|
+
photo: CURATED_STYLES.photorealistic,
|
|
311
|
+
realism: CURATED_STYLES.photorealistic,
|
|
326
312
|
}
|
|
327
313
|
|
|
328
314
|
export function resolveStylePreset(styleKeyOrText, existingNegative, existingGuidance) {
|
|
329
315
|
if (!styleKeyOrText) return { promptSuffix: '', negativePrompt: existingNegative, guidanceScale: existingGuidance }
|
|
330
|
-
const
|
|
331
|
-
|
|
316
|
+
const raw = String(styleKeyOrText).trim()
|
|
317
|
+
if (!raw) return { promptSuffix: '', negativePrompt: existingNegative, guidanceScale: existingGuidance }
|
|
318
|
+
const key = raw.toLowerCase().replace(/[-\s]/g, '_')
|
|
319
|
+
if (key === 'none' || key === 'default' || key === 'off') {
|
|
320
|
+
return { promptSuffix: '', negativePrompt: existingNegative, guidanceScale: existingGuidance }
|
|
321
|
+
}
|
|
322
|
+
const canonicalKey = normalizeStyleKey(key) || key
|
|
323
|
+
const preset = STYLE_PRESETS[canonicalKey] || STYLE_PRESETS[key]
|
|
332
324
|
if (preset && typeof preset === 'object') {
|
|
333
325
|
const combinedNeg = [existingNegative, preset.negativePrompt].filter(Boolean).join(', ')
|
|
334
326
|
return {
|
|
@@ -338,7 +330,7 @@ export function resolveStylePreset(styleKeyOrText, existingNegative, existingGui
|
|
|
338
330
|
}
|
|
339
331
|
}
|
|
340
332
|
return {
|
|
341
|
-
promptSuffix: typeof preset === 'string' ? preset :
|
|
333
|
+
promptSuffix: typeof preset === 'string' ? preset : raw,
|
|
342
334
|
negativePrompt: existingNegative,
|
|
343
335
|
guidanceScale: existingGuidance,
|
|
344
336
|
}
|
package/lib/register-tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// register-tools.js — orchestrates all image-gen tool registrations (#216, #337).
|
|
1
|
+
// register-tools.js — orchestrates all image-gen tool registrations (#216, #337, #384).
|
|
2
2
|
// Supports 'minimal' (default: <= 3 core tools), 'all' (all 30 tools), and 'custom' (toggled toolsets).
|
|
3
|
+
// Reactively updates tool registrations when toolsetProfile or toolsets change.
|
|
3
4
|
|
|
4
5
|
import { registerGenerationTools } from './tools/generation.js'
|
|
5
6
|
import { registerProcessingTools } from './tools/processing-basic.js'
|
|
@@ -20,12 +21,16 @@ import { registerVisionOcrTools } from './tools/vision-ocr.js'
|
|
|
20
21
|
import { registerDiagramTools } from './tools/diagram.js'
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* Registers every image-gen tool on the host tools service according to toolsetProfile (#337).
|
|
24
|
+
* Registers every image-gen tool on the host tools service according to toolsetProfile (#337, #384).
|
|
24
25
|
*
|
|
25
26
|
* @param {object} ctx cordis context
|
|
26
27
|
* @param {object} deps shared runtime from apply()
|
|
28
|
+
* @returns {object} { sync, dispose, getRegisteredNames }
|
|
27
29
|
*/
|
|
28
30
|
export function registerAllTools(ctx, deps) {
|
|
31
|
+
let activeDisposers = []
|
|
32
|
+
const registeredNames = new Set()
|
|
33
|
+
|
|
29
34
|
const originalToolsRegister = ctx?.tools?.register ? ctx.tools.register.bind(ctx.tools) : null
|
|
30
35
|
const wrappedTools = originalToolsRegister ? Object.create(ctx.tools, {
|
|
31
36
|
register: {
|
|
@@ -41,7 +46,23 @@ export function registerAllTools(ctx, deps) {
|
|
|
41
46
|
return originalExecute(args, exec)
|
|
42
47
|
},
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
const res = originalToolsRegister(wrappedDef)
|
|
50
|
+
registeredNames.add(def.name)
|
|
51
|
+
if (typeof res === 'function') {
|
|
52
|
+
activeDisposers.push(res)
|
|
53
|
+
} else {
|
|
54
|
+
activeDisposers.push(() => {
|
|
55
|
+
if (typeof ctx.tools?.unregister === 'function') {
|
|
56
|
+
ctx.tools.unregister(def.name)
|
|
57
|
+
} else if (typeof ctx.tools?.delete === 'function') {
|
|
58
|
+
ctx.tools.delete(def.name)
|
|
59
|
+
} else if (Array.isArray(ctx.tools?.list)) {
|
|
60
|
+
const idx = ctx.tools.list.findIndex((t) => t.name === def.name)
|
|
61
|
+
if (idx !== -1) ctx.tools.list.splice(idx, 1)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return res
|
|
45
66
|
},
|
|
46
67
|
writable: true,
|
|
47
68
|
configurable: true,
|
|
@@ -51,47 +72,95 @@ export function registerAllTools(ctx, deps) {
|
|
|
51
72
|
const targetCtx = originalToolsRegister ? Object.create(ctx, {
|
|
52
73
|
tools: { value: wrappedTools, writable: true, configurable: true }
|
|
53
74
|
}) : ctx
|
|
54
|
-
const cfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
|
|
55
|
-
const profile = cfg.toolsetProfile || 'minimal'
|
|
56
|
-
const isAll = profile === 'all'
|
|
57
|
-
const sets = cfg.toolsets || {}
|
|
58
|
-
const design = isAll || Boolean(sets.design || cfg.toolsetDesign)
|
|
59
|
-
const processing = isAll || Boolean(sets.processing || cfg.toolsetProcessing)
|
|
60
|
-
const frontend = isAll || Boolean(sets.frontend || cfg.toolsetFrontend)
|
|
61
|
-
const creative = isAll || Boolean(sets.creative || cfg.toolsetCreative)
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
function doRegister(cfg) {
|
|
77
|
+
const profile = cfg.toolsetProfile || 'minimal'
|
|
78
|
+
const isAll = profile === 'all'
|
|
79
|
+
const sets = cfg.toolsets || {}
|
|
80
|
+
const design = isAll || Boolean(sets.design || cfg.toolsetDesign)
|
|
81
|
+
const processing = isAll || Boolean(sets.processing || cfg.toolsetProcessing)
|
|
82
|
+
const frontend = isAll || Boolean(sets.frontend || cfg.toolsetFrontend)
|
|
83
|
+
const creative = isAll || Boolean(sets.creative || cfg.toolsetCreative)
|
|
84
|
+
|
|
85
|
+
// 1. Core tools: generate_image, edit_image, inspect_image_quality (<= 3 tools in minimal)
|
|
86
|
+
registerGenerationTools(targetCtx, deps, { coreOnly: !creative })
|
|
87
|
+
registerEditingTools(targetCtx, deps, { coreOnly: !processing })
|
|
88
|
+
registerInspectTools(targetCtx, deps, { coreOnly: !processing })
|
|
89
|
+
|
|
90
|
+
// 2. Processing toolset
|
|
91
|
+
if (processing) {
|
|
92
|
+
registerProcessingTools(targetCtx, deps)
|
|
93
|
+
registerProcessingAdvancedTools(targetCtx, deps)
|
|
94
|
+
registerVisionOcrTools(targetCtx, deps)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Design toolset
|
|
98
|
+
if (design) {
|
|
99
|
+
registerAnchorTools(targetCtx, deps)
|
|
100
|
+
registerUiAssetTools(targetCtx, deps)
|
|
101
|
+
registerStyleMatrixTools(targetCtx, deps)
|
|
102
|
+
registerThemePairTools(targetCtx, deps)
|
|
103
|
+
registerCharacterSheetTools(targetCtx, deps)
|
|
104
|
+
registerDiagramTools(targetCtx, deps)
|
|
105
|
+
}
|
|
67
106
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
// 4. Frontend toolset
|
|
108
|
+
if (frontend) {
|
|
109
|
+
registerFrontendTools(targetCtx, deps)
|
|
110
|
+
registerResponsiveTools(targetCtx, deps)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. Creative toolset
|
|
114
|
+
if (creative) {
|
|
115
|
+
registerSketchTools(targetCtx, deps)
|
|
116
|
+
registerSpritesheetTools(targetCtx, deps)
|
|
117
|
+
registerPatternTools(targetCtx, deps)
|
|
118
|
+
}
|
|
73
119
|
}
|
|
74
120
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
function dispose() {
|
|
122
|
+
for (const d of activeDisposers) {
|
|
123
|
+
if (typeof d === 'function') {
|
|
124
|
+
try { d() } catch (err) {
|
|
125
|
+
// Disposer cleanup errors are safely ignored on tool rebuild
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
activeDisposers = []
|
|
130
|
+
registeredNames.clear()
|
|
83
131
|
}
|
|
84
132
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
function computeKey(cfg) {
|
|
134
|
+
const profile = cfg.toolsetProfile || 'minimal'
|
|
135
|
+
const isAll = profile === 'all'
|
|
136
|
+
const sets = cfg.toolsets || {}
|
|
137
|
+
return JSON.stringify({
|
|
138
|
+
profile,
|
|
139
|
+
design: isAll || Boolean(sets.design || cfg.toolsetDesign),
|
|
140
|
+
processing: isAll || Boolean(sets.processing || cfg.toolsetProcessing),
|
|
141
|
+
frontend: isAll || Boolean(sets.frontend || cfg.toolsetFrontend),
|
|
142
|
+
creative: isAll || Boolean(sets.creative || cfg.toolsetCreative),
|
|
143
|
+
})
|
|
89
144
|
}
|
|
90
145
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
146
|
+
let currentKey = ''
|
|
147
|
+
function sync() {
|
|
148
|
+
const currentCfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
|
|
149
|
+
const key = computeKey(currentCfg)
|
|
150
|
+
if (key === currentKey) return
|
|
151
|
+
currentKey = key
|
|
152
|
+
dispose()
|
|
153
|
+
doRegister(currentCfg)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Initial registration
|
|
157
|
+
const initialCfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
|
|
158
|
+
currentKey = computeKey(initialCfg)
|
|
159
|
+
doRegister(initialCfg)
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
sync,
|
|
163
|
+
dispose,
|
|
164
|
+
getRegisteredNames: () => Array.from(registeredNames),
|
|
96
165
|
}
|
|
97
166
|
}
|
package/lib/settings-route.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateComfyWorkflow } from './comfy-workflow-helpers.js'
|
|
1
2
|
// lib/settings-route.js
|
|
2
3
|
// REST endpoints for plugin settings (/dsh-image-gen/config, /dsh-image-gen/status) (#295, #300).
|
|
3
4
|
// Provides HTTP read/write fallback when DSH core is accessed over network.
|
|
@@ -92,23 +93,72 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, setLiveConfi
|
|
|
92
93
|
return
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
const resetFields = []
|
|
97
|
+
if (Array.isArray(payload?.resetFields)) {
|
|
98
|
+
resetFields.push(...payload.resetFields)
|
|
99
|
+
} else if (Array.isArray(payload?.reset)) {
|
|
100
|
+
resetFields.push(...payload.reset)
|
|
101
|
+
} else if (typeof payload?.resetField === 'string') {
|
|
102
|
+
resetFields.push(payload.resetField)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Coerce fallbackProviders string to array (#369)
|
|
106
|
+
if (typeof raw.fallbackProviders === 'string') {
|
|
107
|
+
raw.fallbackProviders = raw.fallbackProviders
|
|
108
|
+
.split(',')
|
|
109
|
+
.map((s) => s.trim())
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Validate comfyWorkflowJson if non-empty string (#369)
|
|
114
|
+
if (typeof raw.comfyWorkflowJson === 'string' && raw.comfyWorkflowJson.trim() !== '') {
|
|
115
|
+
try {
|
|
116
|
+
validateComfyWorkflow(raw.comfyWorkflowJson)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
119
|
+
res.end(JSON.stringify({ ok: false, error: `Invalid comfyWorkflowJson: ${err.message}` }))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
const allowedKeys = new Set(Object.keys(Config.dict || {}))
|
|
96
|
-
for (const k of
|
|
125
|
+
for (const k of resetFields) {
|
|
126
|
+
if (!allowedKeys.has(k)) {
|
|
127
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
128
|
+
res.end(JSON.stringify({ ok: false, error: `unknown config field to reset: ${k}` }))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
134
|
+
if (k === 'resetFields' || k === 'reset' || k === 'resetField') continue
|
|
97
135
|
if (!allowedKeys.has(k)) {
|
|
98
136
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
99
137
|
res.end(JSON.stringify({ ok: false, error: `unknown config field: ${k}` }))
|
|
100
138
|
return
|
|
101
139
|
}
|
|
140
|
+
if (v === null) {
|
|
141
|
+
resetFields.push(k)
|
|
142
|
+
}
|
|
102
143
|
}
|
|
103
144
|
|
|
104
145
|
try {
|
|
105
|
-
const base = plainConfig(live()) || {}
|
|
106
|
-
|
|
146
|
+
const base = structuredClone(plainConfig(live()) || {})
|
|
147
|
+
for (const k of resetFields) {
|
|
148
|
+
delete base[k]
|
|
149
|
+
}
|
|
150
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
151
|
+
if (k === 'resetFields' || k === 'reset' || k === 'resetField') continue
|
|
152
|
+
if (v !== null && v !== undefined) {
|
|
153
|
+
base[k] = v
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const merged = plainConfig(Config(base))
|
|
107
157
|
const settingsApi = getSettingsApi ? getSettingsApi() : null
|
|
108
158
|
if (settingsApi && typeof settingsApi.replace === 'function') {
|
|
109
159
|
await settingsApi.replace(merged)
|
|
110
160
|
} else if (settingsApi && typeof settingsApi.update === 'function') {
|
|
111
|
-
await settingsApi.update(
|
|
161
|
+
await settingsApi.update(merged)
|
|
112
162
|
} else if (typeof setLiveConfig === 'function') {
|
|
113
163
|
setLiveConfig(merged)
|
|
114
164
|
}
|