@goodandready/dsh-image-gen 0.11.23 → 0.11.24
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 +97 -7
- package/lib/config-schema.js +48 -0
- package/lib/index.js +93 -42
- package/lib/register-tools.js +106 -37
- package/lib/settings-route.js +54 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -264,6 +264,40 @@ window.__ModuleLoader__.load({
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function arrayField(field) {
|
|
268
|
+
return {
|
|
269
|
+
field,
|
|
270
|
+
format: (value) => (Array.isArray(value) ? value.join(', ') : typeof value === 'string' ? value : ''),
|
|
271
|
+
parse: (text) => {
|
|
272
|
+
if (text === '') return { kind: 'clear' }
|
|
273
|
+
const items = text.split(',').map((s) => s.trim()).filter(Boolean)
|
|
274
|
+
return { kind: 'set', value: items }
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function jsonField(field) {
|
|
280
|
+
return {
|
|
281
|
+
field,
|
|
282
|
+
format: (value) => {
|
|
283
|
+
if (typeof value === 'string') return value
|
|
284
|
+
if (value && typeof value === 'object') {
|
|
285
|
+
try { return JSON.stringify(value, null, 2) } catch { return '' }
|
|
286
|
+
}
|
|
287
|
+
return ''
|
|
288
|
+
},
|
|
289
|
+
parse: (text) => {
|
|
290
|
+
if (text === '') return { kind: 'clear' }
|
|
291
|
+
try {
|
|
292
|
+
JSON.parse(text)
|
|
293
|
+
return { kind: 'set', value: text }
|
|
294
|
+
} catch (_) {
|
|
295
|
+
return void 0
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
267
301
|
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
268
302
|
const IMAGE_SIZES = ['landscape_4_3', 'landscape_16_9', 'portrait_4_3', 'portrait_16_9', 'square', 'square_hd']
|
|
269
303
|
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
@@ -328,10 +362,11 @@ window.__ModuleLoader__.load({
|
|
|
328
362
|
// Backward-compat aliases for tests
|
|
329
363
|
{ field: 'localComfyUrl', spec: textField('localComfyUrl'), kind: 'text', labelKey: 'f.localComfyUrl', hintKey: 'f.localComfyUrlHint', placeholderKey: 'p.localComfyUrl', when: ['local'], tab: 'provider' },
|
|
330
364
|
{ field: 'localA1111Url', spec: textField('localA1111Url'), kind: 'text', labelKey: 'f.localA1111Url', hintKey: 'f.localA1111UrlHint', placeholderKey: 'p.localA1111Url', when: ['local'], tab: 'provider' },
|
|
365
|
+
{ field: 'comfyWorkflowJson', spec: jsonField('comfyWorkflowJson'), kind: 'text', labelKey: 'f.comfyWorkflowJson', hintKey: 'f.comfyWorkflowJsonHint', placeholderKey: 'p.comfyWorkflowJson', when: ['local'], tab: 'provider' },
|
|
331
366
|
|
|
332
367
|
// Provider: Codex / Grok
|
|
333
368
|
{ field: 'subscriptionQuality', spec: selectField('subscriptionQuality', ['', 'low', 'medium', 'high']), kind: 'select', options: ['', 'low', 'medium', 'high'], labelKey: 'f.subscriptionQuality', hintKey: 'f.subscriptionQualityHint', when: ['codex', 'grok'], tab: 'provider' },
|
|
334
|
-
{ field: 'fallbackProviders', spec:
|
|
369
|
+
{ field: 'fallbackProviders', spec: arrayField('fallbackProviders'), kind: 'text', labelKey: 'f.fallbackProviders', hintKey: 'f.fallbackProvidersHint', placeholderKey: 'p.fallbackProviders', tab: 'provider' },
|
|
335
370
|
|
|
336
371
|
// Enhancer & Style
|
|
337
372
|
{ field: 'autoEnhancePrompt', spec: booleanField('autoEnhancePrompt'), kind: 'select', options: ['true', 'false'], labelKey: 'f.autoEnhancePrompt', hintKey: 'f.autoEnhancePromptHint', tab: 'enhancer' },
|
|
@@ -2548,6 +2583,9 @@ window.__ModuleLoader__.load({
|
|
|
2548
2583
|
'f.localA1111Url': 'Local A1111 URL (Legacy)',
|
|
2549
2584
|
'f.localA1111UrlHint': 'Direct Automatic1111 endpoint URL.',
|
|
2550
2585
|
'p.localA1111Url': 'http://127.0.0.1:7860',
|
|
2586
|
+
'f.comfyWorkflowJson': 'ComfyUI Workflow JSON',
|
|
2587
|
+
'f.comfyWorkflowJsonHint': 'Custom ComfyUI API workflow graph (JSON) with {{prompt}} placeholders.',
|
|
2588
|
+
'p.comfyWorkflowJson': '{"3": {"class_type": "KSampler", ...}}',
|
|
2551
2589
|
|
|
2552
2590
|
'f.subscriptionQuality': 'Subscription Quality',
|
|
2553
2591
|
'f.subscriptionQualityHint': 'Target quality profile when using Codex or Grok subscriptions.',
|
|
@@ -2824,6 +2862,9 @@ window.__ModuleLoader__.load({
|
|
|
2824
2862
|
'f.localA1111Url': '本地 A1111 地址 (Legacy)',
|
|
2825
2863
|
'f.localA1111UrlHint': '直接指向 Automatic1111 的服务地址。',
|
|
2826
2864
|
'p.localA1111Url': 'http://127.0.0.1:7860',
|
|
2865
|
+
'f.comfyWorkflowJson': 'ComfyUI 工作流 JSON',
|
|
2866
|
+
'f.comfyWorkflowJsonHint': '包含 {{prompt}} 等占位符的自定义 ComfyUI API 工作流 JSON。',
|
|
2867
|
+
'p.comfyWorkflowJson': '{"3": {"class_type": "KSampler", ...}}',
|
|
2827
2868
|
|
|
2828
2869
|
'f.subscriptionQuality': '订阅质量',
|
|
2829
2870
|
'f.subscriptionQualityHint': 'Codex 或 Grok 订阅通道的质量档位。',
|
|
@@ -3009,31 +3050,80 @@ window.__ModuleLoader__.load({
|
|
|
3009
3050
|
const target = activeTarget || resolveActiveScope(ctx)
|
|
3010
3051
|
const snap = target?.getSnapshot ? target.getSnapshot() : null
|
|
3011
3052
|
if (snap?.status === 'ready' && snap.writable && target.set) return target.set(key, val)
|
|
3053
|
+
const prevValue = httpSnapshot.value
|
|
3012
3054
|
httpSnapshot = { ...httpSnapshot, value: { ...httpSnapshot.value, [key]: val } }
|
|
3013
3055
|
notify()
|
|
3014
3056
|
try {
|
|
3015
|
-
await fetch('/dsh-image-gen/config', {
|
|
3057
|
+
const res = await fetch('/dsh-image-gen/config', {
|
|
3016
3058
|
method: 'PUT',
|
|
3017
3059
|
headers: { 'Content-Type': 'application/json' },
|
|
3018
3060
|
body: JSON.stringify({ [key]: val }),
|
|
3019
3061
|
})
|
|
3020
|
-
|
|
3062
|
+
if (!res.ok) {
|
|
3063
|
+
let errText = `Failed to update setting (HTTP ${res.status})`
|
|
3064
|
+
try {
|
|
3065
|
+
const errData = await res.json()
|
|
3066
|
+
if (errData?.error) errText = errData.error
|
|
3067
|
+
} catch { /* ignore */ }
|
|
3068
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3069
|
+
notify()
|
|
3070
|
+
throw new Error(errText)
|
|
3071
|
+
}
|
|
3072
|
+
const data = await res.json()
|
|
3073
|
+
if (!data.ok) {
|
|
3074
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3075
|
+
notify()
|
|
3076
|
+
throw new Error(data.error || 'Failed to update setting')
|
|
3077
|
+
}
|
|
3078
|
+
if (data.config) {
|
|
3079
|
+
httpSnapshot = { status: 'ready', writable: true, value: data.config }
|
|
3080
|
+
notify()
|
|
3081
|
+
}
|
|
3082
|
+
return data
|
|
3083
|
+
} catch (err) {
|
|
3084
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3085
|
+
notify()
|
|
3086
|
+
throw err
|
|
3087
|
+
}
|
|
3021
3088
|
},
|
|
3022
3089
|
async delete(key) {
|
|
3023
3090
|
const target = activeTarget || resolveActiveScope(ctx)
|
|
3024
3091
|
const snap = target?.getSnapshot ? target.getSnapshot() : null
|
|
3025
|
-
if (snap?.status === 'ready' && snap.writable && target.delete
|
|
3092
|
+
if (snap?.status === 'ready' && snap.writable && (target.delete || target.reset)) {
|
|
3093
|
+
if (target.delete) return target.delete(key)
|
|
3094
|
+
if (target.reset) return target.reset(key)
|
|
3095
|
+
}
|
|
3096
|
+
const prevValue = httpSnapshot.value
|
|
3026
3097
|
const nextVal = { ...httpSnapshot.value }
|
|
3027
3098
|
delete nextVal[key]
|
|
3028
3099
|
httpSnapshot = { ...httpSnapshot, value: nextVal }
|
|
3029
3100
|
notify()
|
|
3030
3101
|
try {
|
|
3031
|
-
await fetch('/dsh-image-gen/config', {
|
|
3102
|
+
const res = await fetch('/dsh-image-gen/config', {
|
|
3032
3103
|
method: 'PUT',
|
|
3033
3104
|
headers: { 'Content-Type': 'application/json' },
|
|
3034
|
-
body: JSON.stringify({ [key]
|
|
3105
|
+
body: JSON.stringify({ resetFields: [key] }),
|
|
3035
3106
|
})
|
|
3036
|
-
|
|
3107
|
+
if (!res.ok) {
|
|
3108
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3109
|
+
notify()
|
|
3110
|
+
throw new Error(`Failed to reset setting (HTTP ${res.status})`)
|
|
3111
|
+
}
|
|
3112
|
+
const data = await res.json()
|
|
3113
|
+
if (!data.ok) {
|
|
3114
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3115
|
+
notify()
|
|
3116
|
+
throw new Error(data.error || 'Failed to reset setting')
|
|
3117
|
+
}
|
|
3118
|
+
if (data.config) {
|
|
3119
|
+
httpSnapshot = { status: 'ready', writable: true, value: data.config }
|
|
3120
|
+
notify()
|
|
3121
|
+
}
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
httpSnapshot = { ...httpSnapshot, value: prevValue }
|
|
3124
|
+
notify()
|
|
3125
|
+
throw err
|
|
3126
|
+
}
|
|
3037
3127
|
},
|
|
3038
3128
|
subscribe(fn) {
|
|
3039
3129
|
listeners.add(fn)
|
package/lib/config-schema.js
CHANGED
|
@@ -215,6 +215,34 @@ export const Config = z.object({
|
|
|
215
215
|
.description('CFG scale for local generation.')
|
|
216
216
|
.default(7.0)
|
|
217
217
|
.volatile(),
|
|
218
|
+
comfyWorkflowJson: z
|
|
219
|
+
.string()
|
|
220
|
+
.description('Custom ComfyUI API workflow JSON template with {{prompt}}, {{seed}}, etc. placeholders.')
|
|
221
|
+
.default('')
|
|
222
|
+
.volatile(),
|
|
223
|
+
localComfyUrl: z
|
|
224
|
+
.string()
|
|
225
|
+
.description('Legacy alias for ComfyUI URL.')
|
|
226
|
+
.default('')
|
|
227
|
+
.volatile(),
|
|
228
|
+
localA1111Url: z
|
|
229
|
+
.string()
|
|
230
|
+
.description('Legacy alias for A1111 URL.')
|
|
231
|
+
.default('')
|
|
232
|
+
.volatile(),
|
|
233
|
+
enableLlmEnhancer: z
|
|
234
|
+
.boolean()
|
|
235
|
+
.description('Legacy alias for enhancePrompt.')
|
|
236
|
+
.default(false)
|
|
237
|
+
.volatile(),
|
|
238
|
+
cacheTtlDays: z
|
|
239
|
+
.number()
|
|
240
|
+
.min(0)
|
|
241
|
+
.max(3650)
|
|
242
|
+
.step(1)
|
|
243
|
+
.description('Legacy alias for pruneDays.')
|
|
244
|
+
.default(0)
|
|
245
|
+
.volatile(),
|
|
218
246
|
subscriptionQuality: z
|
|
219
247
|
.string()
|
|
220
248
|
.description('Image quality tier for subscription accounts: "standard" or "hd".')
|
|
@@ -354,6 +382,26 @@ export function sanitizeRaw(raw) {
|
|
|
354
382
|
if (cleaned.localKind === 'automatic1111') {
|
|
355
383
|
cleaned.localKind = 'a1111'
|
|
356
384
|
}
|
|
385
|
+
// Coerce string fallbackProviders to array (#369)
|
|
386
|
+
if (typeof cleaned.fallbackProviders === 'string') {
|
|
387
|
+
cleaned.fallbackProviders = cleaned.fallbackProviders
|
|
388
|
+
.split(',')
|
|
389
|
+
.map((s) => s.trim())
|
|
390
|
+
.filter(Boolean)
|
|
391
|
+
}
|
|
392
|
+
// Migrate legacy fields (#369)
|
|
393
|
+
if (cleaned.localComfyUrl && !cleaned.localBaseURL && cleaned.localKind === 'comfyui') {
|
|
394
|
+
cleaned.localBaseURL = cleaned.localComfyUrl
|
|
395
|
+
}
|
|
396
|
+
if (cleaned.localA1111Url && !cleaned.localBaseURL && cleaned.localKind === 'a1111') {
|
|
397
|
+
cleaned.localBaseURL = cleaned.localA1111Url
|
|
398
|
+
}
|
|
399
|
+
if (typeof cleaned.enableLlmEnhancer === 'boolean' && cleaned.enhancePrompt === undefined) {
|
|
400
|
+
cleaned.enhancePrompt = cleaned.enableLlmEnhancer
|
|
401
|
+
}
|
|
402
|
+
if (typeof cleaned.cacheTtlDays === 'number' && cleaned.cacheTtlDays > 0 && cleaned.pruneDays === undefined) {
|
|
403
|
+
cleaned.pruneDays = cleaned.cacheTtlDays
|
|
404
|
+
}
|
|
357
405
|
return cleaned
|
|
358
406
|
}
|
|
359
407
|
|
package/lib/index.js
CHANGED
|
@@ -114,6 +114,74 @@ export { buildEnhancePromptSystemMessage } from './prompt-enhancer.js'
|
|
|
114
114
|
* Canonical implementation lives in prompt-enhancer.js; re-exported for backwards compat. */
|
|
115
115
|
export { enhancePrompt } from './prompt-enhancer.js'
|
|
116
116
|
|
|
117
|
+
export function createSettingsAdapter(svc, { ns = NS, getLive = () => ({}), setLive = () => {} } = {}) {
|
|
118
|
+
if (!svc) return undefined
|
|
119
|
+
if (typeof svc.replace === 'function' || typeof svc.update === 'function' || typeof svc.mutate === 'function') {
|
|
120
|
+
const getRevision = () => {
|
|
121
|
+
try {
|
|
122
|
+
return svc.describe?.().find((row) => row.ns === ns)?.revision
|
|
123
|
+
} catch (_) {
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
get: () => getLive(),
|
|
129
|
+
replace: async (next) => {
|
|
130
|
+
const parsed = plainConfig(Config(structuredClone(plainConfig(next))))
|
|
131
|
+
const payload = volatileConfig(parsed)
|
|
132
|
+
if (typeof svc.replace === 'function') {
|
|
133
|
+
await svc.replace(ns, payload, getRevision())
|
|
134
|
+
} else if (typeof svc.update === 'function') {
|
|
135
|
+
await svc.update(ns, payload, getRevision())
|
|
136
|
+
} else if (typeof svc.mutate === 'function') {
|
|
137
|
+
await svc.mutate(ns, payload, getRevision())
|
|
138
|
+
} else {
|
|
139
|
+
throw new Error('Settings service has no replace, update, or mutate method')
|
|
140
|
+
}
|
|
141
|
+
setLive(parsed)
|
|
142
|
+
return parsed
|
|
143
|
+
},
|
|
144
|
+
update: async (patch) => {
|
|
145
|
+
const merged = plainConfig(Config({ ...publicConfig(getLive()), ...plainConfig(patch) }))
|
|
146
|
+
const payload = volatileConfig(merged)
|
|
147
|
+
if (typeof svc.update === 'function') {
|
|
148
|
+
await svc.update(ns, payload, getRevision())
|
|
149
|
+
} else if (typeof svc.replace === 'function') {
|
|
150
|
+
await svc.replace(ns, payload, getRevision())
|
|
151
|
+
} else if (typeof svc.mutate === 'function') {
|
|
152
|
+
await svc.mutate(ns, payload, getRevision())
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error('Settings service has no update, replace, or mutate method')
|
|
155
|
+
}
|
|
156
|
+
setLive(merged)
|
|
157
|
+
return merged
|
|
158
|
+
},
|
|
159
|
+
delete: async (key) => {
|
|
160
|
+
const base = structuredClone(plainConfig(getLive()) || {})
|
|
161
|
+
delete base[key]
|
|
162
|
+
const merged = plainConfig(Config(base))
|
|
163
|
+
const payload = volatileConfig(merged)
|
|
164
|
+
if (typeof svc.replace === 'function') {
|
|
165
|
+
await svc.replace(ns, payload, getRevision())
|
|
166
|
+
} else if (typeof svc.update === 'function') {
|
|
167
|
+
await svc.update(ns, payload, getRevision())
|
|
168
|
+
} else if (typeof svc.mutate === 'function') {
|
|
169
|
+
await svc.mutate(ns, payload, getRevision())
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error('Settings service has no replace, update, or mutate method')
|
|
172
|
+
}
|
|
173
|
+
setLive(merged)
|
|
174
|
+
return merged
|
|
175
|
+
},
|
|
176
|
+
watch: (cb) => {
|
|
177
|
+
if (typeof svc.watch === 'function') return svc.watch(cb)
|
|
178
|
+
return () => {}
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
117
185
|
export function apply(ctx, rawConfig) {
|
|
118
186
|
const config = ensureConfig(rawConfig)
|
|
119
187
|
const baseConfig = plainConfig(config)
|
|
@@ -130,50 +198,18 @@ export function apply(ctx, rawConfig) {
|
|
|
130
198
|
|
|
131
199
|
let settingsApi
|
|
132
200
|
let currentLiveConfig = null
|
|
201
|
+
let toolController = null
|
|
133
202
|
let getConfig = () => currentLiveConfig || config
|
|
134
203
|
const live = () => plainConfig(getConfig() ?? {})
|
|
135
204
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
} catch (_) {
|
|
143
|
-
return undefined
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
get: () => live(),
|
|
148
|
-
replace: async (next) => {
|
|
149
|
-
const parsed = plainConfig(Config(structuredClone(plainConfig(next))))
|
|
150
|
-
currentLiveConfig = parsed
|
|
151
|
-
const payload = volatileConfig(parsed)
|
|
152
|
-
if (typeof svc.replace === 'function') {
|
|
153
|
-
await svc.replace(NS, payload, getRevision())
|
|
154
|
-
} else if (typeof svc.update === 'function') {
|
|
155
|
-
await svc.update(NS, payload, getRevision())
|
|
156
|
-
}
|
|
157
|
-
return parsed
|
|
158
|
-
},
|
|
159
|
-
update: async (patch) => {
|
|
160
|
-
const merged = plainConfig(Config({ ...publicConfig(live()), ...plainConfig(patch) }))
|
|
161
|
-
currentLiveConfig = merged
|
|
162
|
-
const payload = volatileConfig(merged)
|
|
163
|
-
if (typeof svc.update === 'function') {
|
|
164
|
-
await svc.update(NS, payload, getRevision())
|
|
165
|
-
} else if (typeof svc.replace === 'function') {
|
|
166
|
-
await svc.replace(NS, payload, getRevision())
|
|
167
|
-
}
|
|
168
|
-
return merged
|
|
169
|
-
},
|
|
170
|
-
watch: (cb) => {
|
|
171
|
-
if (typeof svc.watch === 'function') return svc.watch(cb)
|
|
172
|
-
return () => {}
|
|
173
|
-
},
|
|
205
|
+
const onConfigUpdated = () => {
|
|
206
|
+
try {
|
|
207
|
+
toolController?.sync?.()
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (typeof ctx.logger?.warn === 'function') {
|
|
210
|
+
ctx.logger.warn(`[dsh-image-gen] reactive tool sync failed: ${err?.message || err}`)
|
|
174
211
|
}
|
|
175
212
|
}
|
|
176
|
-
return undefined
|
|
177
213
|
}
|
|
178
214
|
|
|
179
215
|
let settingsSvc = null
|
|
@@ -194,12 +230,20 @@ export function apply(ctx, rawConfig) {
|
|
|
194
230
|
if (!next) return false
|
|
195
231
|
currentLiveConfig = next
|
|
196
232
|
getConfig = () => currentLiveConfig || config
|
|
233
|
+
onConfigUpdated()
|
|
197
234
|
return true
|
|
198
235
|
}
|
|
199
236
|
|
|
200
237
|
ctx.inject(['settings'], (sctx) => {
|
|
201
238
|
settingsSvc = sctx.settings
|
|
202
|
-
settingsApi = createSettingsAdapter(sctx.settings
|
|
239
|
+
settingsApi = createSettingsAdapter(sctx.settings, {
|
|
240
|
+
ns: NS,
|
|
241
|
+
getLive: live,
|
|
242
|
+
setLive: (c) => {
|
|
243
|
+
currentLiveConfig = c
|
|
244
|
+
onConfigUpdated()
|
|
245
|
+
},
|
|
246
|
+
})
|
|
203
247
|
adoptServed()
|
|
204
248
|
sctx.effect(() => () => {
|
|
205
249
|
getConfig = () => config
|
|
@@ -291,7 +335,10 @@ export function apply(ctx, rawConfig) {
|
|
|
291
335
|
registerSettingsRoutes(ctx, {
|
|
292
336
|
live,
|
|
293
337
|
getSettingsApi: () => settingsApi,
|
|
294
|
-
setLiveConfig: (c) => {
|
|
338
|
+
setLiveConfig: (c) => {
|
|
339
|
+
currentLiveConfig = c
|
|
340
|
+
onConfigUpdated()
|
|
341
|
+
},
|
|
295
342
|
})
|
|
296
343
|
|
|
297
344
|
// One-click plugin updater route per DSH standard
|
|
@@ -398,7 +445,7 @@ export function apply(ctx, rawConfig) {
|
|
|
398
445
|
resolveApiKey,
|
|
399
446
|
})
|
|
400
447
|
|
|
401
|
-
registerAllTools(ctx, {
|
|
448
|
+
toolController = registerAllTools(ctx, {
|
|
402
449
|
config: baseConfig,
|
|
403
450
|
live,
|
|
404
451
|
saveAndAttachResult,
|
|
@@ -406,4 +453,8 @@ export function apply(ctx, rawConfig) {
|
|
|
406
453
|
slugify,
|
|
407
454
|
resolveApiKey,
|
|
408
455
|
})
|
|
456
|
+
|
|
457
|
+
ctx.effect(() => () => {
|
|
458
|
+
toolController?.dispose?.()
|
|
459
|
+
}, 'dsh-image-gen: tool registration cleanup')
|
|
409
460
|
}
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.24",
|
|
4
4
|
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|