@goodandready/dsh-image-gen 0.11.22 → 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 +54 -3
- package/lib/index.js +93 -42
- package/lib/providers/backends/custom.js +15 -4
- package/lib/providers/backends/local.js +8 -1
- package/lib/providers/shared-helpers.js +2 -1
- package/lib/register-tools.js +106 -37
- package/lib/settings-route.js +54 -4
- package/lib/tools/character-sheet.js +87 -50
- package/lib/tools/diagram.js +90 -43
- package/lib/tools/theme-pair.js +62 -40
- package/lib/tools/vision-ocr.js +134 -71
- 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
|
@@ -186,9 +186,9 @@ export const Config = z.object({
|
|
|
186
186
|
.default('GEMINI_API_KEY')
|
|
187
187
|
.volatile(),
|
|
188
188
|
localKind: z
|
|
189
|
-
.union([z.const('
|
|
190
|
-
.description('Local generation backend kind: "
|
|
191
|
-
.default('
|
|
189
|
+
.union([z.const('a1111'), z.const('comfyui'), z.const('automatic1111')])
|
|
190
|
+
.description('Local generation backend kind: "a1111" (WebUI) or "comfyui".')
|
|
191
|
+
.default('a1111')
|
|
192
192
|
.volatile(),
|
|
193
193
|
localBaseURL: z
|
|
194
194
|
.string()
|
|
@@ -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".')
|
|
@@ -351,6 +379,29 @@ export function sanitizeRaw(raw) {
|
|
|
351
379
|
}
|
|
352
380
|
cleaned[k] = v
|
|
353
381
|
}
|
|
382
|
+
if (cleaned.localKind === 'automatic1111') {
|
|
383
|
+
cleaned.localKind = 'a1111'
|
|
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
|
+
}
|
|
354
405
|
return cleaned
|
|
355
406
|
}
|
|
356
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
|
}
|
|
@@ -23,16 +23,27 @@ export function createCustomGenerator(deps, job) {
|
|
|
23
23
|
|
|
24
24
|
// Empty key reference indicates unauthenticated gateway.
|
|
25
25
|
const key = cfg.customKeyEnv ? await resolveKey(cfg.customKeyEnv) : ''
|
|
26
|
-
const
|
|
26
|
+
const isEdit = Boolean(source && source.bytes)
|
|
27
|
+
const headers = {}
|
|
28
|
+
if (!isEdit) {
|
|
29
|
+
headers['Content-Type'] = 'application/json'
|
|
30
|
+
}
|
|
27
31
|
if (key) headers.Authorization = `Bearer ${key}`
|
|
28
32
|
|
|
29
33
|
// Omit response_format; modern models return either base64 or URL.
|
|
30
|
-
const endpoint = buildEndpointUrl(base,
|
|
34
|
+
const endpoint = buildEndpointUrl(base, isEdit ? 'images/edits' : 'images/generations')
|
|
31
35
|
const res = await fetchImpl(endpoint, {
|
|
32
36
|
method: 'POST',
|
|
33
37
|
headers,
|
|
34
|
-
body:
|
|
35
|
-
? buildEditForm({
|
|
38
|
+
body: isEdit
|
|
39
|
+
? buildEditForm({
|
|
40
|
+
model: cfg.customModel,
|
|
41
|
+
source,
|
|
42
|
+
mask,
|
|
43
|
+
prompt: promptArg,
|
|
44
|
+
size: cfg.customSize || (aspectPixels ? aspectPixels.join('x') : SIZE_PIXELS[size]) || size,
|
|
45
|
+
strength,
|
|
46
|
+
})
|
|
36
47
|
: JSON.stringify({
|
|
37
48
|
model: cfg.customModel,
|
|
38
49
|
prompt: promptArg,
|
|
@@ -24,7 +24,14 @@ export function createLocalGenerator(deps, job) {
|
|
|
24
24
|
const base = String(cfg.localBaseURL || '').replace(/\/+$/, '')
|
|
25
25
|
if (!base) throw new Error('Local image provider: server address is not configured (Settings → Image generation)')
|
|
26
26
|
const [width, height] = pxSize(aspectPixels, size)
|
|
27
|
-
const
|
|
27
|
+
const rawKind = cfg.localKind || 'a1111'
|
|
28
|
+
const kind = (rawKind === 'a1111' || rawKind === 'automatic1111')
|
|
29
|
+
? 'a1111'
|
|
30
|
+
: (rawKind === 'comfyui' ? 'comfyui' : null)
|
|
31
|
+
|
|
32
|
+
if (!kind) {
|
|
33
|
+
throw new Error(`Unknown local generation backend kind "${rawKind}". Expected "a1111" or "comfyui".`)
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
if (kind === 'a1111') {
|
|
30
37
|
const isImg2Img = Boolean(source && source.bytes)
|
|
@@ -86,8 +86,9 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export function buildEditForm({ source, mask, prompt, size, strength }) {
|
|
89
|
+
export function buildEditForm({ source, mask, prompt, size, strength, model }) {
|
|
90
90
|
const form = new FormData()
|
|
91
|
+
if (model) form.append('model', model)
|
|
91
92
|
form.append('image', new Blob([source.bytes], { type: source.mediaType || 'image/png' }), 'source.png')
|
|
92
93
|
if (mask) form.append('mask', new Blob([mask.bytes], { type: mask.mediaType || 'image/png' }), 'mask.png')
|
|
93
94
|
form.append('prompt', prompt)
|
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
|
}
|