@goodandready/dsh-image-gen 0.11.21 → 0.11.23
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 +14 -0
- package/lib/commands.js +16 -8
- package/lib/config-schema.js +32 -4
- package/lib/generation-helpers.js +266 -250
- package/lib/index.js +12 -2
- package/lib/loop-guard.js +32 -8
- package/lib/provider-utils.js +40 -1
- 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 +42 -17
- package/lib/tools/character-sheet.js +98 -47
- package/lib/tools/diagram.js +101 -40
- package/lib/tools/editing.js +9 -0
- package/lib/tools/generation.js +505 -496
- package/lib/tools/processing-basic.js +6 -1
- package/lib/tools/style-matrix.js +16 -0
- package/lib/tools/theme-pair.js +80 -40
- package/lib/tools/ui-asset.js +15 -0
- package/lib/tools/vision-ocr.js +134 -71
- package/package.json +1 -1
package/lib/provider-utils.js
CHANGED
|
@@ -105,14 +105,53 @@ export function formatA1111Parameters(metadata = {}) {
|
|
|
105
105
|
import { createHash } from 'node:crypto'
|
|
106
106
|
|
|
107
107
|
/** Deterministic sha256 hash for image generation caching. */
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Deterministic sha256 hash for image generation caching (#170, #357).
|
|
110
|
+
* Incorporates full provider job identity: provider, model, prompt, seed, size, style,
|
|
111
|
+
* format, aspect ratio, negative prompt, quality, strength, and source/mask image content hashes.
|
|
112
|
+
*/
|
|
113
|
+
export function computeGenerationHash({
|
|
114
|
+
provider,
|
|
115
|
+
model,
|
|
116
|
+
prompt,
|
|
117
|
+
seed,
|
|
118
|
+
size,
|
|
119
|
+
style,
|
|
120
|
+
format,
|
|
121
|
+
aspectRatio,
|
|
122
|
+
negativePrompt,
|
|
123
|
+
quality,
|
|
124
|
+
strength,
|
|
125
|
+
sourceImage,
|
|
126
|
+
maskImage,
|
|
127
|
+
reference,
|
|
128
|
+
} = {}) {
|
|
129
|
+
const hashContent = (item) => {
|
|
130
|
+
if (!item) return ''
|
|
131
|
+
if (Buffer.isBuffer(item)) return createHash('sha256').update(item).digest('hex').slice(0, 16)
|
|
132
|
+
if (typeof item === 'object' && item.bytes) {
|
|
133
|
+
const b = Buffer.isBuffer(item.bytes) ? item.bytes : Buffer.from(item.bytes)
|
|
134
|
+
return createHash('sha256').update(b).digest('hex').slice(0, 16)
|
|
135
|
+
}
|
|
136
|
+
return String(item).trim()
|
|
137
|
+
}
|
|
138
|
+
|
|
109
139
|
const norm = [
|
|
140
|
+
'v2', // namespace versioning (#357)
|
|
110
141
|
String(provider || '').trim().toLowerCase(),
|
|
111
142
|
String(model || '').trim().toLowerCase(),
|
|
112
143
|
String(prompt || '').trim(),
|
|
113
144
|
String(seed ?? ''),
|
|
114
145
|
String(size || '').trim().toLowerCase(),
|
|
115
146
|
String(style || '').trim().toLowerCase(),
|
|
147
|
+
String(format || '').trim().toLowerCase(),
|
|
148
|
+
String(aspectRatio || '').trim().toLowerCase(),
|
|
149
|
+
String(negativePrompt || '').trim(),
|
|
150
|
+
String(quality || '').trim().toLowerCase(),
|
|
151
|
+
strength !== undefined && strength !== null ? String(strength) : '',
|
|
152
|
+
hashContent(sourceImage),
|
|
153
|
+
hashContent(maskImage),
|
|
154
|
+
hashContent(reference),
|
|
116
155
|
].join('|')
|
|
117
156
|
return createHash('sha256').update(norm).digest('hex')
|
|
118
157
|
}
|
|
@@ -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
|
@@ -26,6 +26,31 @@ import { registerDiagramTools } from './tools/diagram.js'
|
|
|
26
26
|
* @param {object} deps shared runtime from apply()
|
|
27
27
|
*/
|
|
28
28
|
export function registerAllTools(ctx, deps) {
|
|
29
|
+
const originalToolsRegister = ctx?.tools?.register ? ctx.tools.register.bind(ctx.tools) : null
|
|
30
|
+
const wrappedTools = originalToolsRegister ? Object.create(ctx.tools, {
|
|
31
|
+
register: {
|
|
32
|
+
value: (def) => {
|
|
33
|
+
const originalExecute = def.execute
|
|
34
|
+
const wrappedDef = {
|
|
35
|
+
...def,
|
|
36
|
+
execute: async (args, exec) => {
|
|
37
|
+
const currentCfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
|
|
38
|
+
if (currentCfg && currentCfg.enabled === false) {
|
|
39
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
40
|
+
}
|
|
41
|
+
return originalExecute(args, exec)
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
return originalToolsRegister(wrappedDef)
|
|
45
|
+
},
|
|
46
|
+
writable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
},
|
|
49
|
+
}) : ctx?.tools
|
|
50
|
+
|
|
51
|
+
const targetCtx = originalToolsRegister ? Object.create(ctx, {
|
|
52
|
+
tools: { value: wrappedTools, writable: true, configurable: true }
|
|
53
|
+
}) : ctx
|
|
29
54
|
const cfg = typeof deps.live === 'function' ? deps.live() : (deps.config || {})
|
|
30
55
|
const profile = cfg.toolsetProfile || 'minimal'
|
|
31
56
|
const isAll = profile === 'all'
|
|
@@ -36,37 +61,37 @@ export function registerAllTools(ctx, deps) {
|
|
|
36
61
|
const creative = isAll || Boolean(sets.creative || cfg.toolsetCreative)
|
|
37
62
|
|
|
38
63
|
// 1. Core tools: generate_image, edit_image, inspect_image_quality (<= 3 tools in minimal)
|
|
39
|
-
registerGenerationTools(
|
|
40
|
-
registerEditingTools(
|
|
41
|
-
registerInspectTools(
|
|
64
|
+
registerGenerationTools(targetCtx, deps, { coreOnly: !creative })
|
|
65
|
+
registerEditingTools(targetCtx, deps, { coreOnly: !processing })
|
|
66
|
+
registerInspectTools(targetCtx, deps, { coreOnly: !processing })
|
|
42
67
|
|
|
43
68
|
// 2. Processing toolset
|
|
44
69
|
if (processing) {
|
|
45
|
-
registerProcessingTools(
|
|
46
|
-
registerProcessingAdvancedTools(
|
|
47
|
-
registerVisionOcrTools(
|
|
70
|
+
registerProcessingTools(targetCtx, deps)
|
|
71
|
+
registerProcessingAdvancedTools(targetCtx, deps)
|
|
72
|
+
registerVisionOcrTools(targetCtx, deps)
|
|
48
73
|
}
|
|
49
74
|
|
|
50
75
|
// 3. Design toolset
|
|
51
76
|
if (design) {
|
|
52
|
-
registerAnchorTools(
|
|
53
|
-
registerUiAssetTools(
|
|
54
|
-
registerStyleMatrixTools(
|
|
55
|
-
registerThemePairTools(
|
|
56
|
-
registerCharacterSheetTools(
|
|
57
|
-
registerDiagramTools(
|
|
77
|
+
registerAnchorTools(targetCtx, deps)
|
|
78
|
+
registerUiAssetTools(targetCtx, deps)
|
|
79
|
+
registerStyleMatrixTools(targetCtx, deps)
|
|
80
|
+
registerThemePairTools(targetCtx, deps)
|
|
81
|
+
registerCharacterSheetTools(targetCtx, deps)
|
|
82
|
+
registerDiagramTools(targetCtx, deps)
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
// 4. Frontend toolset
|
|
61
86
|
if (frontend) {
|
|
62
|
-
registerFrontendTools(
|
|
63
|
-
registerResponsiveTools(
|
|
87
|
+
registerFrontendTools(targetCtx, deps)
|
|
88
|
+
registerResponsiveTools(targetCtx, deps)
|
|
64
89
|
}
|
|
65
90
|
|
|
66
91
|
// 5. Creative toolset
|
|
67
92
|
if (creative) {
|
|
68
|
-
registerSketchTools(
|
|
69
|
-
registerSpritesheetTools(
|
|
70
|
-
registerPatternTools(
|
|
93
|
+
registerSketchTools(targetCtx, deps)
|
|
94
|
+
registerSpritesheetTools(targetCtx, deps)
|
|
95
|
+
registerPatternTools(targetCtx, deps)
|
|
71
96
|
}
|
|
72
97
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// character-sheet.js — generate_character_sheet tool (#181).
|
|
1
|
+
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
2
|
+
// character-sheet.js — generate_character_sheet tool (#181, #358).
|
|
3
3
|
// Generates multi-angle character model sheets and turnarounds with visual consistency.
|
|
4
4
|
|
|
5
5
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
@@ -7,12 +7,15 @@ import path from 'node:path'
|
|
|
7
7
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
8
8
|
import {
|
|
9
9
|
ASPECT_RATIOS,
|
|
10
|
+
PROVIDER_KEYS,
|
|
10
11
|
buildSidecar,
|
|
11
12
|
makeProviders,
|
|
12
13
|
toLosslessJson,
|
|
13
14
|
saveAttachmentSafe,
|
|
15
|
+
|
|
14
16
|
} from '../providers.js'
|
|
15
17
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
18
|
+
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
16
19
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
17
20
|
import {
|
|
18
21
|
CHARACTER_SHEET_LAYOUTS,
|
|
@@ -23,7 +26,7 @@ import {
|
|
|
23
26
|
export function registerCharacterSheetTools(ctx, deps) {
|
|
24
27
|
const {
|
|
25
28
|
live,
|
|
26
|
-
slugify,
|
|
29
|
+
slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
27
30
|
resolveApiKey,
|
|
28
31
|
} = deps
|
|
29
32
|
|
|
@@ -77,6 +80,8 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
77
80
|
},
|
|
78
81
|
seed: { type: 'integer' },
|
|
79
82
|
provider: { type: 'string' },
|
|
83
|
+
path: { type: 'string' },
|
|
84
|
+
url: { type: 'string' },
|
|
80
85
|
attachment: {
|
|
81
86
|
type: 'object',
|
|
82
87
|
additionalProperties: true,
|
|
@@ -88,13 +93,24 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
88
93
|
byteSize: { type: 'integer' },
|
|
89
94
|
},
|
|
90
95
|
},
|
|
96
|
+
_fallback: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
additionalProperties: true,
|
|
99
|
+
},
|
|
91
100
|
},
|
|
92
101
|
},
|
|
93
102
|
render: (_args, value) => renderToolOutput(value),
|
|
94
103
|
},
|
|
95
104
|
async execute(args, execCtx) {
|
|
96
|
-
const cfg = live
|
|
97
|
-
|
|
105
|
+
const cfg = typeof live === 'function' ? live() : (live?.value || {})
|
|
106
|
+
if (cfg.enabled === false) {
|
|
107
|
+
throw new Error('Image generation is disabled in settings.')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
|
|
111
|
+
? (args.provider || cfg.provider)
|
|
112
|
+
: (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
|
|
113
|
+
|
|
98
114
|
const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
|
|
99
115
|
|
|
100
116
|
const sheetPlan = buildCharacterSheetPrompt({
|
|
@@ -104,60 +120,98 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
104
120
|
negativePrompt: args.negativePrompt,
|
|
105
121
|
})
|
|
106
122
|
|
|
107
|
-
const
|
|
108
|
-
fetchImpl: fetch,
|
|
109
|
-
resolveKey: (id) => resolveApiKey(cfg, id),
|
|
110
|
-
cfg,
|
|
111
|
-
})
|
|
123
|
+
const aspectPixels = ASPECT_RATIOS[sheetPlan.aspectRatio] || ASPECT_RATIOS['16:9']
|
|
112
124
|
|
|
113
|
-
|
|
125
|
+
let subscriptionImages
|
|
126
|
+
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
114
127
|
|
|
115
|
-
const
|
|
128
|
+
const providers = makeProviders(
|
|
129
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
130
|
+
{
|
|
131
|
+
prompt: sheetPlan.prompt,
|
|
132
|
+
negativePrompt: sheetPlan.negativePrompt,
|
|
133
|
+
size: 'custom',
|
|
134
|
+
format: 'png',
|
|
135
|
+
seed,
|
|
136
|
+
signal: execCtx?.signal,
|
|
137
|
+
aspectPixels,
|
|
138
|
+
aspectRatio: sheetPlan.aspectRatio,
|
|
139
|
+
quality: 'high',
|
|
140
|
+
},
|
|
141
|
+
)
|
|
116
142
|
|
|
117
|
-
const
|
|
118
|
-
prompt: sheetPlan.prompt,
|
|
119
|
-
negativePrompt: sheetPlan.negativePrompt,
|
|
120
|
-
aspectRatio: sheetPlan.aspectRatio,
|
|
121
|
-
aspectPixels,
|
|
122
|
-
format: 'png',
|
|
123
|
-
seed,
|
|
124
|
-
quality: 'high',
|
|
125
|
-
}
|
|
143
|
+
const fallbackChain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
126
144
|
|
|
127
|
-
const
|
|
128
|
-
|
|
145
|
+
const estimatedCost = calculateGenerationCost({
|
|
146
|
+
provider,
|
|
147
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
148
|
+
size: cfg.defaultSize || '1024x1024',
|
|
149
|
+
count: 1,
|
|
150
|
+
})
|
|
151
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
152
|
+
|
|
153
|
+
const gen = await executeWithFallback(
|
|
129
154
|
providers,
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
fallbackChain,
|
|
156
|
+
seed,
|
|
157
|
+
sheetPlan.prompt,
|
|
158
|
+
{ logger: ctx.logger },
|
|
132
159
|
)
|
|
133
160
|
|
|
161
|
+
const usedProvider = gen._fallback?.providerUsed || provider
|
|
162
|
+
const finalBytes = gen.bytes
|
|
134
163
|
const timestamp = Date.now()
|
|
135
164
|
const safeSlug = slugify(args.prompt.slice(0, 30)) || 'character-sheet'
|
|
136
165
|
const filename = `character-sheet-${sheetPlan.layout}-${safeSlug}-${timestamp}.png`
|
|
137
166
|
|
|
138
167
|
let attachment = null
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
168
|
+
let localUrl = ''
|
|
169
|
+
let diskPath = ''
|
|
170
|
+
if (finalBytes) {
|
|
171
|
+
const saveRes = await saveAttachmentSafe(ctx, {
|
|
172
|
+
bytes: finalBytes,
|
|
173
|
+
mediaType: 'image/png',
|
|
174
|
+
name: filename,
|
|
175
|
+
})
|
|
176
|
+
attachment = saveRes?.attachment || null
|
|
177
|
+
localUrl = saveRes?.localUrl || ''
|
|
144
178
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} else if (genResult.url) {
|
|
179
|
+
const sessionCwd = execCtx?.agent?.session?.header?.cwd
|
|
180
|
+
const targetDir = cfg.outputDir || 'generated/images'
|
|
181
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
182
|
+
await mkdir(outDir, { recursive: true })
|
|
183
|
+
diskPath = path.join(outDir, filename)
|
|
184
|
+
await writeFile(diskPath, Buffer.from(finalBytes))
|
|
185
|
+
} else if (gen.url) {
|
|
153
186
|
attachment = {
|
|
154
|
-
url:
|
|
187
|
+
url: gen.url,
|
|
155
188
|
mediaType: 'image/png',
|
|
156
189
|
filename,
|
|
157
190
|
}
|
|
191
|
+
localUrl = gen.url
|
|
158
192
|
}
|
|
159
193
|
|
|
194
|
+
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
195
|
+
recordSpend(effectiveCost, {
|
|
196
|
+
ctx,
|
|
197
|
+
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: sheetPlan.prompt, seed },
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const dualOutput = buildDualOutputMarkdown({
|
|
201
|
+
action: 'generated',
|
|
202
|
+
filePath: diskPath || filename,
|
|
203
|
+
width: aspectPixels[0],
|
|
204
|
+
height: aspectPixels[1],
|
|
205
|
+
mediaType: 'image/png',
|
|
206
|
+
seed,
|
|
207
|
+
provider: usedProvider,
|
|
208
|
+
model: cfg.model || cfg.customModel || 'character-sheet',
|
|
209
|
+
cost: gen.cost,
|
|
210
|
+
attachmentId: attachment?.attachmentId,
|
|
211
|
+
})
|
|
212
|
+
|
|
160
213
|
const response = {
|
|
214
|
+
summary: dualOutput,
|
|
161
215
|
prompt: sheetPlan.prompt,
|
|
162
216
|
characterDescription: args.prompt,
|
|
163
217
|
layout: sheetPlan.layout,
|
|
@@ -167,16 +221,13 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
167
221
|
seed,
|
|
168
222
|
provider: usedProvider,
|
|
169
223
|
attachment,
|
|
224
|
+
path: diskPath,
|
|
225
|
+
url: localUrl,
|
|
226
|
+
attachmentId: attachment?.attachmentId,
|
|
227
|
+
_fallback: gen._fallback,
|
|
170
228
|
}
|
|
171
229
|
|
|
172
|
-
|
|
173
|
-
response.attachmentId = attachment.attachmentId
|
|
174
|
-
}
|
|
175
|
-
if (attachment?.url) {
|
|
176
|
-
response.url = attachment.url
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return response
|
|
230
|
+
return toLosslessJson(response)
|
|
180
231
|
},
|
|
181
232
|
})
|
|
182
233
|
)
|
package/lib/tools/diagram.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// diagram.js — beautify_diagram tool (#185).
|
|
1
|
+
// diagram.js — beautify_diagram tool (#185, #359).
|
|
3
2
|
// Transforms raw Mermaid / Graphviz diagrams into high-end 3D isometric architectural illustrations.
|
|
4
3
|
|
|
5
4
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
@@ -7,10 +6,14 @@ import path from 'node:path'
|
|
|
7
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
8
7
|
import {
|
|
9
8
|
ASPECT_RATIOS,
|
|
9
|
+
PROVIDER_KEYS,
|
|
10
10
|
makeProviders,
|
|
11
|
+
toLosslessJson,
|
|
11
12
|
saveAttachmentSafe,
|
|
12
13
|
} from '../providers.js'
|
|
14
|
+
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
13
15
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
16
|
+
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
14
17
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
15
18
|
import {
|
|
16
19
|
DIAGRAM_3D_STYLES,
|
|
@@ -20,7 +23,7 @@ import {
|
|
|
20
23
|
export function registerDiagramTools(ctx, deps) {
|
|
21
24
|
const {
|
|
22
25
|
live,
|
|
23
|
-
slugify,
|
|
26
|
+
slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
24
27
|
resolveApiKey,
|
|
25
28
|
} = deps
|
|
26
29
|
|
|
@@ -74,6 +77,8 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
74
77
|
prompt: { type: 'string' },
|
|
75
78
|
seed: { type: 'integer' },
|
|
76
79
|
provider: { type: 'string' },
|
|
80
|
+
path: { type: 'string' },
|
|
81
|
+
url: { type: 'string' },
|
|
77
82
|
attachment: {
|
|
78
83
|
type: 'object',
|
|
79
84
|
additionalProperties: true,
|
|
@@ -84,13 +89,24 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
84
89
|
filename: { type: 'string' },
|
|
85
90
|
},
|
|
86
91
|
},
|
|
92
|
+
_fallback: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
additionalProperties: true,
|
|
95
|
+
},
|
|
87
96
|
},
|
|
88
97
|
},
|
|
89
98
|
render: (_args, value) => renderToolOutput(value),
|
|
90
99
|
},
|
|
91
100
|
async execute(args, execCtx) {
|
|
92
|
-
const cfg =
|
|
93
|
-
|
|
101
|
+
const cfg = typeof live === 'function' ? live() : (live?.value || {})
|
|
102
|
+
if (cfg.enabled === false) {
|
|
103
|
+
throw new Error('Image generation is disabled in settings.')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const provider = PROVIDER_KEYS.includes(args.provider || cfg.provider)
|
|
107
|
+
? (args.provider || cfg.provider)
|
|
108
|
+
: (PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal')
|
|
109
|
+
|
|
94
110
|
const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
|
|
95
111
|
|
|
96
112
|
const plan = buildDiagramIllustrationPrompt({
|
|
@@ -100,57 +116,98 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
100
116
|
aspectRatio: args.aspect_ratio || '16:9',
|
|
101
117
|
})
|
|
102
118
|
|
|
103
|
-
const providers = makeProviders({
|
|
104
|
-
fetchImpl: fetch,
|
|
105
|
-
resolveKey: (id) => resolveApiKey(cfg, id),
|
|
106
|
-
cfg,
|
|
107
|
-
})
|
|
108
|
-
const fallbackChain = resolveFallbackChain(activeProvider, cfg)
|
|
109
119
|
const aspectPixels = ASPECT_RATIOS[plan.aspectRatio] || ASPECT_RATIOS['16:9']
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
negativePrompt: plan.negativePrompt,
|
|
114
|
-
aspectRatio: plan.aspectRatio,
|
|
115
|
-
aspectPixels,
|
|
116
|
-
format: 'png',
|
|
117
|
-
seed,
|
|
118
|
-
quality: 'high',
|
|
119
|
-
}
|
|
121
|
+
let subscriptionImages
|
|
122
|
+
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
120
123
|
|
|
121
|
-
const
|
|
122
|
-
|
|
124
|
+
const providers = makeProviders(
|
|
125
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
126
|
+
{
|
|
127
|
+
prompt: plan.prompt,
|
|
128
|
+
negativePrompt: plan.negativePrompt,
|
|
129
|
+
size: 'custom',
|
|
130
|
+
format: 'png',
|
|
131
|
+
seed,
|
|
132
|
+
signal: execCtx?.signal,
|
|
133
|
+
aspectPixels,
|
|
134
|
+
aspectRatio: plan.aspectRatio,
|
|
135
|
+
quality: 'high',
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const fallbackChain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
140
|
+
|
|
141
|
+
const estimatedCost = calculateGenerationCost({
|
|
142
|
+
provider,
|
|
143
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
144
|
+
size: cfg.defaultSize || '1024x1024',
|
|
145
|
+
count: 1,
|
|
146
|
+
})
|
|
147
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
148
|
+
|
|
149
|
+
const gen = await executeWithFallback(
|
|
123
150
|
providers,
|
|
124
|
-
|
|
125
|
-
|
|
151
|
+
fallbackChain,
|
|
152
|
+
seed,
|
|
153
|
+
plan.prompt,
|
|
154
|
+
{ logger: ctx.logger },
|
|
126
155
|
)
|
|
127
156
|
|
|
128
|
-
const
|
|
129
|
-
|
|
157
|
+
const usedProvider = gen._fallback?.providerUsed || provider
|
|
158
|
+
const finalBytes = gen.bytes
|
|
130
159
|
const timestamp = Date.now()
|
|
131
160
|
const safeSlug = slugify(args.title || 'architecture-diagram') || 'diagram'
|
|
132
161
|
const filename = `diagram-3d-${safeSlug}-${timestamp}.png`
|
|
133
162
|
|
|
134
163
|
let attachment = null
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
let localUrl = ''
|
|
165
|
+
let diskPath = ''
|
|
166
|
+
if (finalBytes) {
|
|
167
|
+
const saveRes = await saveAttachmentSafe(ctx, {
|
|
168
|
+
bytes: finalBytes,
|
|
169
|
+
mediaType: 'image/png',
|
|
170
|
+
name: filename,
|
|
171
|
+
})
|
|
172
|
+
attachment = saveRes?.attachment || null
|
|
173
|
+
localUrl = saveRes?.localUrl || ''
|
|
174
|
+
|
|
175
|
+
const sessionCwd = execCtx?.agent?.session?.header?.cwd
|
|
176
|
+
const targetDir = cfg.outputDir || 'generated/images'
|
|
177
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
178
|
+
await mkdir(outDir, { recursive: true })
|
|
179
|
+
diskPath = path.join(outDir, filename)
|
|
180
|
+
await writeFile(diskPath, Buffer.from(finalBytes))
|
|
181
|
+
} else if (gen.url) {
|
|
146
182
|
attachment = {
|
|
147
|
-
url:
|
|
183
|
+
url: gen.url,
|
|
148
184
|
mediaType: 'image/png',
|
|
149
185
|
filename,
|
|
150
186
|
}
|
|
187
|
+
localUrl = gen.url
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
|
|
190
|
+
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
191
|
+
recordSpend(effectiveCost, {
|
|
192
|
+
ctx,
|
|
193
|
+
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: plan.prompt, seed },
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const dualOutput = buildDualOutputMarkdown({
|
|
197
|
+
action: 'generated',
|
|
198
|
+
filePath: diskPath || filename,
|
|
199
|
+
width: aspectPixels[0],
|
|
200
|
+
height: aspectPixels[1],
|
|
201
|
+
mediaType: 'image/png',
|
|
202
|
+
seed,
|
|
203
|
+
provider: usedProvider,
|
|
204
|
+
model: cfg.model || cfg.customModel || 'beautify-diagram',
|
|
205
|
+
cost: gen.cost,
|
|
206
|
+
attachmentId: attachment?.attachmentId,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const response = {
|
|
210
|
+
summary: dualOutput,
|
|
154
211
|
title: args.title || 'System Architecture',
|
|
155
212
|
style: plan.style,
|
|
156
213
|
aspectRatio: plan.aspectRatio,
|
|
@@ -159,9 +216,13 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
159
216
|
seed,
|
|
160
217
|
provider: usedProvider,
|
|
161
218
|
attachment,
|
|
162
|
-
|
|
219
|
+
path: diskPath,
|
|
220
|
+
url: localUrl,
|
|
163
221
|
attachmentId: attachment?.attachmentId,
|
|
222
|
+
_fallback: gen._fallback,
|
|
164
223
|
}
|
|
224
|
+
|
|
225
|
+
return toLosslessJson(response)
|
|
165
226
|
},
|
|
166
227
|
})
|
|
167
228
|
)
|
package/lib/tools/editing.js
CHANGED
|
@@ -84,6 +84,9 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
84
84
|
async execute(args, exec) {
|
|
85
85
|
try {
|
|
86
86
|
const cfg = live()
|
|
87
|
+
if (cfg && cfg.enabled === false) {
|
|
88
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
89
|
+
}
|
|
87
90
|
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
88
91
|
const estimatedCostEdit = calculateGenerationCost({
|
|
89
92
|
provider: cfg.provider || 'fal',
|
|
@@ -219,6 +222,9 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
219
222
|
async execute(args, exec) {
|
|
220
223
|
try {
|
|
221
224
|
const cfg = live()
|
|
225
|
+
if (cfg && cfg.enabled === false) {
|
|
226
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
227
|
+
}
|
|
222
228
|
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
223
229
|
const count = normalizeCount(args.count || 1)
|
|
224
230
|
const estimatedCostVary = calculateGenerationCost({
|
|
@@ -357,6 +363,9 @@ export function registerEditingTools(ctx, deps, options = {}) {
|
|
|
357
363
|
async execute(args, exec) {
|
|
358
364
|
try {
|
|
359
365
|
const cfg = live()
|
|
366
|
+
if (cfg && cfg.enabled === false) {
|
|
367
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
368
|
+
}
|
|
360
369
|
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
361
370
|
const count = normalizeCount(args.count || 1)
|
|
362
371
|
const estimatedCost = calculateGenerationCost({
|