@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/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
|
}
|
|
@@ -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,10 +7,12 @@ 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'
|
|
16
18
|
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
@@ -24,7 +26,7 @@ import {
|
|
|
24
26
|
export function registerCharacterSheetTools(ctx, deps) {
|
|
25
27
|
const {
|
|
26
28
|
live,
|
|
27
|
-
slugify,
|
|
29
|
+
slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
28
30
|
resolveApiKey,
|
|
29
31
|
} = deps
|
|
30
32
|
|
|
@@ -78,6 +80,8 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
78
80
|
},
|
|
79
81
|
seed: { type: 'integer' },
|
|
80
82
|
provider: { type: 'string' },
|
|
83
|
+
path: { type: 'string' },
|
|
84
|
+
url: { type: 'string' },
|
|
81
85
|
attachment: {
|
|
82
86
|
type: 'object',
|
|
83
87
|
additionalProperties: true,
|
|
@@ -89,13 +93,24 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
89
93
|
byteSize: { type: 'integer' },
|
|
90
94
|
},
|
|
91
95
|
},
|
|
96
|
+
_fallback: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
additionalProperties: true,
|
|
99
|
+
},
|
|
92
100
|
},
|
|
93
101
|
},
|
|
94
102
|
render: (_args, value) => renderToolOutput(value),
|
|
95
103
|
},
|
|
96
104
|
async execute(args, execCtx) {
|
|
97
|
-
const cfg = live
|
|
98
|
-
|
|
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
|
+
|
|
99
114
|
const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
|
|
100
115
|
|
|
101
116
|
const sheetPlan = buildCharacterSheetPrompt({
|
|
@@ -105,73 +120,98 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
105
120
|
negativePrompt: args.negativePrompt,
|
|
106
121
|
})
|
|
107
122
|
|
|
108
|
-
const
|
|
109
|
-
fetchImpl: fetch,
|
|
110
|
-
resolveKey: (id) => resolveApiKey(cfg, id),
|
|
111
|
-
cfg,
|
|
112
|
-
})
|
|
123
|
+
const aspectPixels = ASPECT_RATIOS[sheetPlan.aspectRatio] || ASPECT_RATIOS['16:9']
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
let subscriptionImages
|
|
126
|
+
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
127
|
+
|
|
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
|
+
)
|
|
115
142
|
|
|
116
|
-
const
|
|
143
|
+
const fallbackChain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
117
144
|
|
|
118
145
|
const estimatedCost = calculateGenerationCost({
|
|
119
|
-
provider
|
|
146
|
+
provider,
|
|
120
147
|
model: cfg.model || cfg.customModel || 'default',
|
|
121
148
|
size: cfg.defaultSize || '1024x1024',
|
|
122
149
|
count: 1,
|
|
123
150
|
})
|
|
124
151
|
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
125
152
|
|
|
126
|
-
const
|
|
127
|
-
prompt: sheetPlan.prompt,
|
|
128
|
-
negativePrompt: sheetPlan.negativePrompt,
|
|
129
|
-
aspectRatio: sheetPlan.aspectRatio,
|
|
130
|
-
aspectPixels,
|
|
131
|
-
format: 'png',
|
|
132
|
-
seed,
|
|
133
|
-
quality: 'high',
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const { result: genResult, provider: usedProvider } = await executeWithFallback(
|
|
137
|
-
fallbackChain,
|
|
153
|
+
const gen = await executeWithFallback(
|
|
138
154
|
providers,
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
fallbackChain,
|
|
156
|
+
seed,
|
|
157
|
+
sheetPlan.prompt,
|
|
158
|
+
{ logger: ctx.logger },
|
|
141
159
|
)
|
|
142
160
|
|
|
161
|
+
const usedProvider = gen._fallback?.providerUsed || provider
|
|
162
|
+
const finalBytes = gen.bytes
|
|
143
163
|
const timestamp = Date.now()
|
|
144
164
|
const safeSlug = slugify(args.prompt.slice(0, 30)) || 'character-sheet'
|
|
145
165
|
const filename = `character-sheet-${sheetPlan.layout}-${safeSlug}-${timestamp}.png`
|
|
146
166
|
|
|
147
167
|
let attachment = null
|
|
148
|
-
|
|
149
|
-
|
|
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 || ''
|
|
178
|
+
|
|
179
|
+
const sessionCwd = execCtx?.agent?.session?.header?.cwd
|
|
180
|
+
const targetDir = cfg.outputDir || 'generated/images'
|
|
181
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
150
182
|
await mkdir(outDir, { recursive: true })
|
|
151
|
-
|
|
152
|
-
await writeFile(diskPath, Buffer.from(
|
|
153
|
-
|
|
154
|
-
attachment = await saveAttachmentSafe(
|
|
155
|
-
ctx,
|
|
156
|
-
Buffer.from(genResult.bytes),
|
|
157
|
-
'image/png',
|
|
158
|
-
filename,
|
|
159
|
-
diskPath
|
|
160
|
-
)
|
|
161
|
-
} else if (genResult.url) {
|
|
183
|
+
diskPath = path.join(outDir, filename)
|
|
184
|
+
await writeFile(diskPath, Buffer.from(finalBytes))
|
|
185
|
+
} else if (gen.url) {
|
|
162
186
|
attachment = {
|
|
163
|
-
url:
|
|
187
|
+
url: gen.url,
|
|
164
188
|
mediaType: 'image/png',
|
|
165
189
|
filename,
|
|
166
190
|
}
|
|
191
|
+
localUrl = gen.url
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
|
|
194
|
+
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
195
|
+
recordSpend(effectiveCost, {
|
|
170
196
|
ctx,
|
|
171
197
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: sheetPlan.prompt, seed },
|
|
172
198
|
})
|
|
173
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
|
+
|
|
174
213
|
const response = {
|
|
214
|
+
summary: dualOutput,
|
|
175
215
|
prompt: sheetPlan.prompt,
|
|
176
216
|
characterDescription: args.prompt,
|
|
177
217
|
layout: sheetPlan.layout,
|
|
@@ -181,16 +221,13 @@ export function registerCharacterSheetTools(ctx, deps) {
|
|
|
181
221
|
seed,
|
|
182
222
|
provider: usedProvider,
|
|
183
223
|
attachment,
|
|
224
|
+
path: diskPath,
|
|
225
|
+
url: localUrl,
|
|
226
|
+
attachmentId: attachment?.attachmentId,
|
|
227
|
+
_fallback: gen._fallback,
|
|
184
228
|
}
|
|
185
229
|
|
|
186
|
-
|
|
187
|
-
response.attachmentId = attachment.attachmentId
|
|
188
|
-
}
|
|
189
|
-
if (attachment?.url) {
|
|
190
|
-
response.url = attachment.url
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return response
|
|
230
|
+
return toLosslessJson(response)
|
|
194
231
|
},
|
|
195
232
|
})
|
|
196
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,9 +6,12 @@ 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'
|
|
14
16
|
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
15
17
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
export function registerDiagramTools(ctx, deps) {
|
|
22
24
|
const {
|
|
23
25
|
live,
|
|
24
|
-
slugify,
|
|
26
|
+
slugify = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-'),
|
|
25
27
|
resolveApiKey,
|
|
26
28
|
} = deps
|
|
27
29
|
|
|
@@ -75,6 +77,8 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
75
77
|
prompt: { type: 'string' },
|
|
76
78
|
seed: { type: 'integer' },
|
|
77
79
|
provider: { type: 'string' },
|
|
80
|
+
path: { type: 'string' },
|
|
81
|
+
url: { type: 'string' },
|
|
78
82
|
attachment: {
|
|
79
83
|
type: 'object',
|
|
80
84
|
additionalProperties: true,
|
|
@@ -85,13 +89,24 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
85
89
|
filename: { type: 'string' },
|
|
86
90
|
},
|
|
87
91
|
},
|
|
92
|
+
_fallback: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
additionalProperties: true,
|
|
95
|
+
},
|
|
88
96
|
},
|
|
89
97
|
},
|
|
90
98
|
render: (_args, value) => renderToolOutput(value),
|
|
91
99
|
},
|
|
92
100
|
async execute(args, execCtx) {
|
|
93
|
-
const cfg =
|
|
94
|
-
|
|
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
|
+
|
|
95
110
|
const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
|
|
96
111
|
|
|
97
112
|
const plan = buildDiagramIllustrationPrompt({
|
|
@@ -101,70 +116,98 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
101
116
|
aspectRatio: args.aspect_ratio || '16:9',
|
|
102
117
|
})
|
|
103
118
|
|
|
104
|
-
const providers = makeProviders({
|
|
105
|
-
fetchImpl: fetch,
|
|
106
|
-
resolveKey: (id) => resolveApiKey(cfg, id),
|
|
107
|
-
cfg,
|
|
108
|
-
})
|
|
109
|
-
const fallbackChain = resolveFallbackChain(activeProvider, cfg)
|
|
110
119
|
const aspectPixels = ASPECT_RATIOS[plan.aspectRatio] || ASPECT_RATIOS['16:9']
|
|
111
120
|
|
|
121
|
+
let subscriptionImages
|
|
122
|
+
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
123
|
+
|
|
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
|
+
|
|
112
141
|
const estimatedCost = calculateGenerationCost({
|
|
113
|
-
provider
|
|
142
|
+
provider,
|
|
114
143
|
model: cfg.model || cfg.customModel || 'default',
|
|
115
144
|
size: cfg.defaultSize || '1024x1024',
|
|
116
145
|
count: 1,
|
|
117
146
|
})
|
|
118
147
|
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
119
148
|
|
|
120
|
-
const
|
|
121
|
-
prompt: plan.prompt,
|
|
122
|
-
negativePrompt: plan.negativePrompt,
|
|
123
|
-
aspectRatio: plan.aspectRatio,
|
|
124
|
-
aspectPixels,
|
|
125
|
-
format: 'png',
|
|
126
|
-
seed,
|
|
127
|
-
quality: 'high',
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const { result: genResult, provider: usedProvider } = await executeWithFallback(
|
|
131
|
-
fallbackChain,
|
|
149
|
+
const gen = await executeWithFallback(
|
|
132
150
|
providers,
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
fallbackChain,
|
|
152
|
+
seed,
|
|
153
|
+
plan.prompt,
|
|
154
|
+
{ logger: ctx.logger },
|
|
135
155
|
)
|
|
136
156
|
|
|
137
|
-
const
|
|
138
|
-
|
|
157
|
+
const usedProvider = gen._fallback?.providerUsed || provider
|
|
158
|
+
const finalBytes = gen.bytes
|
|
139
159
|
const timestamp = Date.now()
|
|
140
160
|
const safeSlug = slugify(args.title || 'architecture-diagram') || 'diagram'
|
|
141
161
|
const filename = `diagram-3d-${safeSlug}-${timestamp}.png`
|
|
142
162
|
|
|
143
163
|
let attachment = null
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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) {
|
|
155
182
|
attachment = {
|
|
156
|
-
url:
|
|
183
|
+
url: gen.url,
|
|
157
184
|
mediaType: 'image/png',
|
|
158
185
|
filename,
|
|
159
186
|
}
|
|
187
|
+
localUrl = gen.url
|
|
160
188
|
}
|
|
161
189
|
|
|
162
|
-
|
|
190
|
+
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
191
|
+
recordSpend(effectiveCost, {
|
|
163
192
|
ctx,
|
|
164
193
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: plan.prompt, seed },
|
|
165
194
|
})
|
|
166
195
|
|
|
167
|
-
|
|
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,
|
|
168
211
|
title: args.title || 'System Architecture',
|
|
169
212
|
style: plan.style,
|
|
170
213
|
aspectRatio: plan.aspectRatio,
|
|
@@ -173,9 +216,13 @@ export function registerDiagramTools(ctx, deps) {
|
|
|
173
216
|
seed,
|
|
174
217
|
provider: usedProvider,
|
|
175
218
|
attachment,
|
|
176
|
-
|
|
219
|
+
path: diskPath,
|
|
220
|
+
url: localUrl,
|
|
177
221
|
attachmentId: attachment?.attachmentId,
|
|
222
|
+
_fallback: gen._fallback,
|
|
178
223
|
}
|
|
224
|
+
|
|
225
|
+
return toLosslessJson(response)
|
|
179
226
|
},
|
|
180
227
|
})
|
|
181
228
|
)
|