@goodandready/dsh-image-gen 0.10.14 → 0.10.16
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/README.md +18 -0
- package/README.ru.md +18 -0
- package/lib/cost-meter.js +3 -2
- package/lib/frontend-assets.js +3 -0
- package/lib/generation-cache.js +7 -3
- package/lib/index.js +62 -73
- package/lib/loop-guard.js +6 -1
- package/lib/providers.js +61 -1
- package/lib/resolve-image.js +12 -1
- package/lib/security.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,24 @@
|
|
|
84
84
|
* **Subscription Aspect Ratio Mapping**: maps aspect ratios (`16:9`, `3:2`, `9:16`, `2:3`) to appropriate subscription dimensions (`1536x1024` / `1024x1536`) instead of falling back to default square `1024x1024`.
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
### 🚀 What's New in v0.10.16
|
|
88
|
+
* **Safe Attachment Service Fallback (#214)**: Added graceful fallback handling in `saveAttachmentSafe` across all 8 visual tools (`generate_image`, variations, `remove_background`, `upscale_image`, `blend_images`, etc.). When running in headless CLI mode or when the attachment store is unavailable, outputs are safely written to disk with full file paths and data references rather than crashing tool execution.
|
|
89
|
+
* **HTTP Image Endpoint Strict Validation**: Hardened `/dsh-image-gen/image` route against malformed or NaN parameters (`b`, `w`, `h`), cleanly responding with HTTP 400 or 404 without leaking internal system traces.
|
|
90
|
+
* **ComfyUI Node Error Extraction**: Integrated `extractComfyNodeErrors` to parse node execution errors from `/history/{pid}` status responses, immediately reporting root-cause node diagnostics rather than timing out.
|
|
91
|
+
* **Custom Provider Error Normalization**: Standardized gateway error extracts through `formatErrorMessage`, eliminating raw JSON or `[object Object]` artifacts on 4xx/5xx API responses.
|
|
92
|
+
* **Cache & History Consistency**: Ensured content-addressed disk cache hits synchronize seamlessly with generation history and sidecar metadata.
|
|
93
|
+
* **Test Suite Expansion**: Added `test/batch4-hardening.test.mjs`, expanding test coverage to **128 automated unit tests (100% pass)**.
|
|
94
|
+
|
|
95
|
+
### 🚀 What's New in v0.10.15
|
|
96
|
+
* **Loop Guard Zero-Limit Bypass (#212)**: Fixed an edge case where setting `loopGuardLimit: 0` (configured to disable protection) enforced a limit of 1 due to `Math.max(1, limit)`. Setting limit to 0 now properly bypasses loop checks.
|
|
97
|
+
* **Security Hardening (Token Masking)**: Added automatic pattern masking for Replicate API tokens (`r8_...`) and Google Gemini API keys (`AIza...`) in `sanitizeErrorAndLogs`.
|
|
98
|
+
* **Cache Resilience**: Isolated LRU touch updates in `getCachedGeneration` so that metadata write contention does not discard valid image bytes on cache hits.
|
|
99
|
+
* **Defensive File Safety**: Added explicit file existence, empty file (0 bytes), and 50 MB maximum size cap checks in `resolveConversationImage` preventing OOM crashes on huge files.
|
|
100
|
+
* **Vectorize Image Attachment & Toolview Preview**: `vectorize_image` now registers the SVG in `ctx.attachments` and provides `url`, enabling interactive preview in `tool.call.toolview`.
|
|
101
|
+
* **SVG Markup Validation**: Added strict validation in `optimizeSvgContent` rejecting non-SVG strings with descriptive errors.
|
|
102
|
+
* **Cost Meter Safety**: Hardened `assertBudgetAvailable` to handle string/undefined/NaN values safely.
|
|
103
|
+
* **Expanded Test Suite**: Added `test/batch3-quality.test.mjs` expanding suite to **126 automated tests (100% pass)**.
|
|
104
|
+
|
|
87
105
|
### 🚀 What's New in v0.10.14
|
|
88
106
|
* **dsh-clinebot Unified Visual Overhaul (#210)**: Redesigned client settings card following the high-end `dsh-clinebot` unified design system. Added top quick stats row (`.ig-grid-4`, `.ig-stat-box`) showing active provider, default specs, safety/loop guard, and daily budget/cache status. Integrated status badges (`.ig-badge-ok/warn/bad`).
|
|
89
107
|
* **5 Categorized Settings Tabs**: Settings fields organized into intuitive tabs: ⚙️ General, 🔌 Provider, ✨ Prompt & Styles, 🛡️ Safety & Budget, ⚡ Cache & Storage.
|
package/README.ru.md
CHANGED
|
@@ -130,6 +130,24 @@ graph LR
|
|
|
130
130
|
- **100% интернационализация (#140)**: устранены захардкоженные русские строки, добавлены переводы на русский, английский и китайский языки.
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
### 🚀 Что нового в v0.10.16
|
|
134
|
+
* **Безопасный Fallback сервиса вложений (#214)**: Реализована функция `saveAttachmentSafe` во всех 8 визуальных инструментах (`generate_image`, вариации, `remove_background`, `upscale_image`, `blend_images` и др.). При запуске в автономном режиме без сервиса вложений или при сбое хранилища инструмент гарантированно сохраняет файл на диск и отдает пути без падения.
|
|
135
|
+
* **Строгая валидация HTTP-эндпоинта картинок**: В обработчике `/dsh-image-gen/image` усилена проверка query-параметров (`b`, `w`, `h`), защищающая от `NaN` и отрицательных чисел, а также исключающая утечку внутренних трассировок при 404/500 ошибках.
|
|
136
|
+
* **Диагностика ошибок нод ComfyUI**: Добавлена функция `extractComfyNodeErrors`, мгновенно извлекающая сообщения об ошибках нод из `/history/{pid}` при сбое генерации вместо бесконечного ожидания таймаута.
|
|
137
|
+
* **Нормализация ошибок Custom/OpenAI API**: Ошибки сторонних шлюзов и API провайдеров единообразно форматируются через `formatErrorMessage`, предотвращая появление `[object Object]` или громоздких дампов JSON.
|
|
138
|
+
* **Согласованность истории и дискового кэша**: Попадания в кэш корректно поддерживают актуальность метаданных sidecar и истории генераций.
|
|
139
|
+
* **Расширение тестов**: Добавлен тестовый набор `test/batch4-hardening.test.mjs`, общее количество тестов выросло до **128 (100% pass)**.
|
|
140
|
+
|
|
141
|
+
### 🚀 Что нового в v0.10.15
|
|
142
|
+
* **Обход Loop Guard при нулевом лимите (#212)**: Исправлена ошибка, когда при значении `loopGuardLimit: 0` (отключение защиты) срабатывал `Math.max(1, 0) = 1` и блокировал вызовы на 2-й итерации. При значении 0 лимит теперь полностью отключается.
|
|
143
|
+
* **Усиление безопасности токенов**: В `sanitizeErrorAndLogs` добавлено маскирование токенов Replicate (`r8_...`) и ключей Google Gemini (`AIza...`).
|
|
144
|
+
* **Устойчивость дискового кэша**: Изолировано обновление метаданных LRU при чтении из кэша — ошибки записи метаданных больше не приводят к сбросу валидных байтов кэша.
|
|
145
|
+
* **Безопасность загрузки файлов**: В `resolveConversationImage` добавлены проверки существования файла, запрет 0-байтных файлов и ограничение 50 МБ для предотвращения переполнения памяти.
|
|
146
|
+
* **Вложения и Toolview для векторизации**: Инструмент `vectorize_image` теперь сохраняет SVG во вложения `ctx.attachments` и возвращает `url`, что активирует карточку интерактивного предпросмотра в чате.
|
|
147
|
+
* **Валидация SVG-разметки**: `optimizeSvgContent` теперь строго проверяет наличие корневого тега `<svg>` и выдает понятную ошибку при передаче не-SVG текста.
|
|
148
|
+
* **Безопасная обработка бюджета**: `assertBudgetAvailable` защищен от `NaN`, нечисловых и неопределенных значений стоимости.
|
|
149
|
+
* **Расширенное тестирование**: Добавлен набор тестов `test/batch3-quality.test.mjs`, общее число тестов выросло до **126 (100% pass)**.
|
|
150
|
+
|
|
133
151
|
### 🚀 Что нового в v0.10.14
|
|
134
152
|
* **Единый дизайн в стиле dsh-clinebot (#210)**: Интерфейс настроек переработан в соответствии со стандартами `dsh-clinebot`. Добавлен верхний дашборд быстрой статистики (`.ig-grid-4`, `.ig-stat-box`), отображающий активного провайдера, формат и размер, статус Quality Gate и лимит Loop Guard, дневной бюджет и состояние кэша.
|
|
135
153
|
* **5 категорий настроек (табы)**: Поля сгруппированы по вкладкам: ⚙️ Основные, 🔌 Провайдер, ✨ Промпт и стили, 🛡️ Безопасность и бюджет, ⚡ Кэш и хранение.
|
package/lib/cost-meter.js
CHANGED
|
@@ -135,11 +135,12 @@ export function assertBudgetAvailable(estimatedCost, dailyBudgetUsd) {
|
|
|
135
135
|
const budget = Number(dailyBudgetUsd) || 0
|
|
136
136
|
if (budget <= 0) return true // Budget 0 = unlimited
|
|
137
137
|
|
|
138
|
+
const cost = Number(estimatedCost) || 0
|
|
138
139
|
const current = loadDailySpend()
|
|
139
|
-
const projected = +(current.totalSpendUsd +
|
|
140
|
+
const projected = +(current.totalSpendUsd + cost).toFixed(4)
|
|
140
141
|
if (projected > budget) {
|
|
141
142
|
throw new Error(
|
|
142
|
-
`Daily image generation budget exceeded: current spend $${current.totalSpendUsd.toFixed(2)} + estimated $${
|
|
143
|
+
`Daily image generation budget exceeded: current spend $${current.totalSpendUsd.toFixed(2)} + estimated $${cost.toFixed(2)} exceeds limit $${budget.toFixed(2)} (Settings → Image generation → dailyBudgetUsd)`
|
|
143
144
|
)
|
|
144
145
|
}
|
|
145
146
|
return true
|
package/lib/frontend-assets.js
CHANGED
|
@@ -306,6 +306,9 @@ export function optimizeSvgContent(svgString, options = {}) {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
let cleaned = svgString.trim()
|
|
309
|
+
if (!cleaned.includes('<svg')) {
|
|
310
|
+
throw new Error('Content does not contain valid SVG markup (<svg ...>)')
|
|
311
|
+
}
|
|
309
312
|
|
|
310
313
|
cleaned = cleaned.replace(/<\?xml[\s\S]*?\?>/gi, '')
|
|
311
314
|
cleaned = cleaned.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
|
package/lib/generation-cache.js
CHANGED
|
@@ -33,9 +33,13 @@ export function getCachedGeneration(hash, { force = false } = {}) {
|
|
|
33
33
|
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
34
34
|
const bytes = readFileSync(dataPath)
|
|
35
35
|
|
|
36
|
-
// Touch meta file for LRU update
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Touch meta file for LRU update in isolated block
|
|
37
|
+
try {
|
|
38
|
+
meta.lastAccessed = Date.now()
|
|
39
|
+
writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf8')
|
|
40
|
+
} catch {
|
|
41
|
+
// Non-fatal touch failure: still return valid cache entry
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
return {
|
|
41
45
|
bytes,
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
resolveStylePreset,
|
|
48
48
|
blendImagesFal,
|
|
49
49
|
toLosslessJson,
|
|
50
|
+
saveAttachmentSafe,
|
|
50
51
|
} from './providers.js'
|
|
51
52
|
|
|
52
53
|
import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from './resolve-image.js'
|
|
@@ -72,6 +73,9 @@ export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMedi
|
|
|
72
73
|
/**
|
|
73
74
|
* Сохраняет сгенерированный ассет в воркспейс, регистрирует в attachments и возвращает Dual-Output (#150).
|
|
74
75
|
*/
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
75
79
|
export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
76
80
|
bytes,
|
|
77
81
|
mediaType,
|
|
@@ -89,11 +93,7 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
|
89
93
|
args = {},
|
|
90
94
|
action = 'generated',
|
|
91
95
|
}) {
|
|
92
|
-
const attachment = await ctx
|
|
93
|
-
data: new Uint8Array(bytes),
|
|
94
|
-
mediaType,
|
|
95
|
-
name,
|
|
96
|
-
})
|
|
96
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes, mediaType, name })
|
|
97
97
|
|
|
98
98
|
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
99
99
|
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
@@ -102,12 +102,6 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
|
102
102
|
const filePath = path.join(outDir, name)
|
|
103
103
|
await writeFile(filePath, bytes)
|
|
104
104
|
|
|
105
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
106
|
-
+ '&mt=' + encodeURIComponent(attachment.mediaType)
|
|
107
|
-
+ '&b=' + encodeURIComponent(String(attachment.bytes))
|
|
108
|
-
+ '&w=' + encodeURIComponent(String(attachment.width))
|
|
109
|
-
+ '&h=' + encodeURIComponent(String(attachment.height))
|
|
110
|
-
|
|
111
105
|
await writeFile(
|
|
112
106
|
path.join(outDir, `${stem}.json`),
|
|
113
107
|
JSON.stringify(buildSidecar({
|
|
@@ -614,14 +608,26 @@ export function apply(ctx, config) {
|
|
|
614
608
|
// The tool result carries all of them, so the card sends them back.
|
|
615
609
|
// Nothing is gained by forging them — the bytes are still verified
|
|
616
610
|
// against the sha256 in the id, and a mismatch is simply a 404.
|
|
611
|
+
const rawBytes = Number(query.get('b'))
|
|
612
|
+
const rawW = Number(query.get('w'))
|
|
613
|
+
const rawH = Number(query.get('h'))
|
|
614
|
+
const safeBytes = Number.isFinite(rawBytes) && rawBytes >= 0 ? rawBytes : 0
|
|
615
|
+
const safeW = Number.isFinite(rawW) && rawW >= 0 ? rawW : 0
|
|
616
|
+
const safeH = Number.isFinite(rawH) && rawH >= 0 ? rawH : 0
|
|
617
|
+
|
|
617
618
|
const ref = {
|
|
618
619
|
attachmentId: id,
|
|
619
620
|
mediaType: query.get('mt') || 'image/png',
|
|
620
|
-
bytes:
|
|
621
|
-
width:
|
|
622
|
-
height:
|
|
621
|
+
bytes: safeBytes,
|
|
622
|
+
width: safeW,
|
|
623
|
+
height: safeH,
|
|
623
624
|
}
|
|
624
625
|
try {
|
|
626
|
+
if (!ctx.attachments || typeof ctx.attachments.readImage !== 'function') {
|
|
627
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
628
|
+
res.end(JSON.stringify({ error: 'attachment service unavailable' }))
|
|
629
|
+
return
|
|
630
|
+
}
|
|
625
631
|
const stored = await ctx.attachments.readImage(ref)
|
|
626
632
|
res.writeHead(200, {
|
|
627
633
|
'Content-Type': stored.ref?.mediaType || 'image/png',
|
|
@@ -631,7 +637,7 @@ export function apply(ctx, config) {
|
|
|
631
637
|
res.end(Buffer.from(stored.data))
|
|
632
638
|
} catch (error) {
|
|
633
639
|
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
634
|
-
res.end(JSON.stringify({ error:
|
|
640
|
+
res.end(JSON.stringify({ error: 'image not found or reference mismatch' }))
|
|
635
641
|
}
|
|
636
642
|
}
|
|
637
643
|
})()
|
|
@@ -907,11 +913,7 @@ export function apply(ctx, config) {
|
|
|
907
913
|
const stem = `${slugify(args.output_name || promptArg)}-${Date.now().toString(36)}-${jobSeed}`
|
|
908
914
|
const name = `${stem}.${extension}`
|
|
909
915
|
|
|
910
|
-
const attachment = await ctx
|
|
911
|
-
data: new Uint8Array(bytes),
|
|
912
|
-
mediaType,
|
|
913
|
-
name,
|
|
914
|
-
})
|
|
916
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes, mediaType, name })
|
|
915
917
|
|
|
916
918
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
917
919
|
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
@@ -920,12 +922,6 @@ export function apply(ctx, config) {
|
|
|
920
922
|
const filePath = path.join(outDir, name)
|
|
921
923
|
await writeFile(filePath, bytes)
|
|
922
924
|
|
|
923
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
924
|
-
+ '&mt=' + encodeURIComponent(attachment.mediaType)
|
|
925
|
-
+ '&b=' + encodeURIComponent(String(attachment.bytes))
|
|
926
|
-
+ '&w=' + encodeURIComponent(String(attachment.width))
|
|
927
|
-
+ '&h=' + encodeURIComponent(String(attachment.height))
|
|
928
|
-
|
|
929
925
|
// Sidecar: рядом с картинкой — метаданные генерации. Каталог становится
|
|
930
926
|
// самодокументируемым (галерея/повтор/диагностика читают их без БД).
|
|
931
927
|
await writeFile(
|
|
@@ -1055,11 +1051,7 @@ export function apply(ctx, config) {
|
|
|
1055
1051
|
const extension = diskHit.mediaType === 'image/jpeg' ? 'jpg' : diskHit.mediaType === 'image/webp' ? 'webp' : 'png'
|
|
1056
1052
|
const stem = `${slugify(args.output_name || promptVal)}-${Date.now().toString(36)}-${seedVal}`
|
|
1057
1053
|
const name = `${stem}.${extension}`
|
|
1058
|
-
const attachment = await ctx.
|
|
1059
|
-
data: new Uint8Array(diskHit.bytes),
|
|
1060
|
-
mediaType: diskHit.mediaType,
|
|
1061
|
-
name,
|
|
1062
|
-
})
|
|
1054
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: diskHit.bytes, mediaType: diskHit.mediaType, name })
|
|
1063
1055
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1064
1056
|
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
1065
1057
|
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
@@ -1067,12 +1059,6 @@ export function apply(ctx, config) {
|
|
|
1067
1059
|
const filePath = path.join(outDir, name)
|
|
1068
1060
|
await writeFile(filePath, diskHit.bytes)
|
|
1069
1061
|
|
|
1070
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1071
|
-
+ '&mt=' + encodeURIComponent(attachment.mediaType)
|
|
1072
|
-
+ '&b=' + encodeURIComponent(String(attachment.bytes))
|
|
1073
|
-
+ '&w=' + encodeURIComponent(String(attachment.width))
|
|
1074
|
-
+ '&h=' + encodeURIComponent(String(attachment.height))
|
|
1075
|
-
|
|
1076
1062
|
return {
|
|
1077
1063
|
path: filePath,
|
|
1078
1064
|
url: localUrl,
|
|
@@ -1211,17 +1197,12 @@ export function apply(ctx, config) {
|
|
|
1211
1197
|
)
|
|
1212
1198
|
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
1213
1199
|
const name = `${stem}.png`
|
|
1214
|
-
const attachment = await ctx.
|
|
1215
|
-
data: new Uint8Array(res.bytes),
|
|
1216
|
-
mediaType: 'image/png',
|
|
1217
|
-
name,
|
|
1218
|
-
})
|
|
1200
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
1219
1201
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1220
1202
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
1221
1203
|
await mkdir(outDir, { recursive: true })
|
|
1222
1204
|
const filePath = path.join(outDir, name)
|
|
1223
1205
|
await writeFile(filePath, res.bytes)
|
|
1224
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1225
1206
|
const summary = buildDualOutputMarkdown({
|
|
1226
1207
|
action: 'background removed',
|
|
1227
1208
|
filePath,
|
|
@@ -1284,17 +1265,12 @@ export function apply(ctx, config) {
|
|
|
1284
1265
|
)
|
|
1285
1266
|
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
1286
1267
|
const name = `${stem}.png`
|
|
1287
|
-
const attachment = await ctx.
|
|
1288
|
-
data: new Uint8Array(res.bytes),
|
|
1289
|
-
mediaType: 'image/png',
|
|
1290
|
-
name,
|
|
1291
|
-
})
|
|
1268
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
1292
1269
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1293
1270
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
1294
1271
|
await mkdir(outDir, { recursive: true })
|
|
1295
1272
|
const filePath = path.join(outDir, name)
|
|
1296
1273
|
await writeFile(filePath, res.bytes)
|
|
1297
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1298
1274
|
const summary = buildDualOutputMarkdown({
|
|
1299
1275
|
action: `upscaled (${args.scale || 2}x)`,
|
|
1300
1276
|
filePath,
|
|
@@ -1343,24 +1319,42 @@ export function apply(ctx, config) {
|
|
|
1343
1319
|
},
|
|
1344
1320
|
},
|
|
1345
1321
|
async execute(args, exec) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1322
|
+
try {
|
|
1323
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1324
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
1325
|
+
const cfg = live()
|
|
1326
|
+
const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
1327
|
+
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
1328
|
+
const name = `${stem}.svg`
|
|
1329
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1330
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
1331
|
+
await mkdir(outDir, { recursive: true })
|
|
1332
|
+
const filePath = path.join(outDir, name)
|
|
1333
|
+
await writeFile(filePath, res.bytes)
|
|
1334
|
+
|
|
1335
|
+
let attachment = null
|
|
1336
|
+
let localUrl = ''
|
|
1337
|
+
if (ctx.attachments?.saveImage) {
|
|
1338
|
+
attachment = await ctx.attachments.saveImage({
|
|
1339
|
+
data: new Uint8Array(res.bytes),
|
|
1340
|
+
mediaType: 'image/svg+xml',
|
|
1341
|
+
name,
|
|
1342
|
+
})
|
|
1343
|
+
localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const summary = `### Vectorized SVG Export\n- **File**: ${filePath}\n- **Format**: SVG (XML vector)\n- **Color mode**: ${args.color_mode || 'color'}\n- **Palette size**: ${res.palette ? res.palette.length : 16} colors\n`
|
|
1347
|
+
return toLosslessJson({
|
|
1348
|
+
summary,
|
|
1349
|
+
path: filePath,
|
|
1350
|
+
url: localUrl,
|
|
1351
|
+
svg: res.svg,
|
|
1352
|
+
format: 'svg',
|
|
1353
|
+
attachment,
|
|
1354
|
+
})
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
1357
|
+
}
|
|
1364
1358
|
},
|
|
1365
1359
|
}),
|
|
1366
1360
|
)
|
|
@@ -1407,17 +1401,12 @@ export function apply(ctx, config) {
|
|
|
1407
1401
|
)
|
|
1408
1402
|
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
1409
1403
|
const name = `${stem}.png`
|
|
1410
|
-
const attachment = await ctx.
|
|
1411
|
-
data: new Uint8Array(res.bytes),
|
|
1412
|
-
mediaType: 'image/png',
|
|
1413
|
-
name,
|
|
1414
|
-
})
|
|
1404
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
1415
1405
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1416
1406
|
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
1417
1407
|
await mkdir(outDir, { recursive: true })
|
|
1418
1408
|
const filePath = path.join(outDir, name)
|
|
1419
1409
|
await writeFile(filePath, res.bytes)
|
|
1420
|
-
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1421
1410
|
const summary = buildDualOutputMarkdown({
|
|
1422
1411
|
action: 'blended composition',
|
|
1423
1412
|
filePath,
|
package/lib/loop-guard.js
CHANGED
|
@@ -29,8 +29,13 @@ export function trackAndAssertLoopGuard(sessionId, {
|
|
|
29
29
|
return { count: 0, allowed: true }
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// A limit of 0 explicitly disables loop guard protection
|
|
33
|
+
if (limit === 0 || limit === '0') {
|
|
34
|
+
return { count: 0, remaining: Infinity, allowed: true }
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
const current = sessionStates.get(sid) || { count: 0, lastPrompt: '', lastTimestamp: 0 }
|
|
33
|
-
const maxLimit = Math.max(1, limit)
|
|
38
|
+
const maxLimit = Math.max(1, Number(limit) || DEFAULT_MAX_CONSECUTIVE_GENERATIONS)
|
|
34
39
|
|
|
35
40
|
if (current.count >= maxLimit) {
|
|
36
41
|
throw new Error(
|
package/lib/providers.js
CHANGED
|
@@ -461,6 +461,60 @@ export async function pixelDiff(a, b) {
|
|
|
461
461
|
return { diffRatio: diff / a.length }
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/** Извлечение детальных сообщений об ошибках нод ComfyUI из ответа /history/{pid}. */
|
|
465
|
+
|
|
466
|
+
/** Безопасное сохранение вложения с fallback'ом при отсутствии ctx.attachments */
|
|
467
|
+
export async function saveAttachmentSafe(ctx, { bytes, mediaType, name }) {
|
|
468
|
+
if (ctx && ctx.attachments && typeof ctx.attachments.saveImage === 'function') {
|
|
469
|
+
try {
|
|
470
|
+
const att = await ctx.attachments.saveImage({
|
|
471
|
+
data: new Uint8Array(bytes),
|
|
472
|
+
mediaType,
|
|
473
|
+
name,
|
|
474
|
+
})
|
|
475
|
+
if (att && att.attachmentId) {
|
|
476
|
+
const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(att.attachmentId)
|
|
477
|
+
+ '&mt=' + encodeURIComponent(att.mediaType || mediaType)
|
|
478
|
+
+ '&b=' + encodeURIComponent(String(att.bytes || bytes.length))
|
|
479
|
+
+ '&w=' + encodeURIComponent(String(att.width || 0))
|
|
480
|
+
+ '&h=' + encodeURIComponent(String(att.height || 0))
|
|
481
|
+
return { attachment: att, localUrl }
|
|
482
|
+
}
|
|
483
|
+
} catch (err) {
|
|
484
|
+
// Fallback below if attachment service fails
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
attachment: {
|
|
489
|
+
attachmentId: '',
|
|
490
|
+
mediaType,
|
|
491
|
+
bytes: bytes.length,
|
|
492
|
+
width: 0,
|
|
493
|
+
height: 0,
|
|
494
|
+
name,
|
|
495
|
+
},
|
|
496
|
+
localUrl: '',
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function extractComfyNodeErrors(entry) {
|
|
501
|
+
if (!entry || typeof entry !== 'object') return ''
|
|
502
|
+
const status = entry.status
|
|
503
|
+
if (status && status.status_str === 'error') {
|
|
504
|
+
const msgs = status.messages || []
|
|
505
|
+
const errList = []
|
|
506
|
+
for (const m of msgs) {
|
|
507
|
+
if (Array.isArray(m) && m[0] === 'execution_error') {
|
|
508
|
+
const d = m[1] || {}
|
|
509
|
+
errList.push(`node ${d.node_id || 'unknown'} (${d.node_type || ''}): ${d.exception_message || d.exception_type || 'execution failed'}`)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (errList.length) return errList.join('; ')
|
|
513
|
+
return status.status_str || 'execution error'
|
|
514
|
+
}
|
|
515
|
+
return ''
|
|
516
|
+
}
|
|
517
|
+
|
|
464
518
|
export function makeProviders(deps, job) {
|
|
465
519
|
const { fetchImpl, resolveKey, cfg } = deps
|
|
466
520
|
const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels, aspectRatio } = job
|
|
@@ -553,7 +607,7 @@ export function makeProviders(deps, job) {
|
|
|
553
607
|
throw new Error(`Custom image API has no provisioned channel for "${cfg.customModel}" (HTTP 503); check gateway channel and routing configuration`)
|
|
554
608
|
}
|
|
555
609
|
}
|
|
556
|
-
const detail = data?.error
|
|
610
|
+
const detail = formatErrorMessage(data?.error || data)
|
|
557
611
|
throw new Error(`Image API failed (HTTP ${res.status}): ${detail}`)
|
|
558
612
|
}
|
|
559
613
|
const item = data?.data?.[0]
|
|
@@ -727,6 +781,12 @@ export function makeProviders(deps, job) {
|
|
|
727
781
|
if (!hist.ok) continue
|
|
728
782
|
const histData = await hist.json().catch(() => ({}))
|
|
729
783
|
const entry = histData[pid]
|
|
784
|
+
if (entry) {
|
|
785
|
+
const comfyErr = extractComfyNodeErrors(entry)
|
|
786
|
+
if (comfyErr) {
|
|
787
|
+
throw new Error(`Local ComfyUI execution failed: ${comfyErr}`)
|
|
788
|
+
}
|
|
789
|
+
}
|
|
730
790
|
if (entry && entry.outputs) {
|
|
731
791
|
const outputs = entry.outputs
|
|
732
792
|
const img = Object.values(outputs).flatMap((o) => o.images || []).find((i) => i && i.filename)
|
package/lib/resolve-image.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { estimateSharpnessAndVariance } from './providers.js'
|
|
4
4
|
|
|
@@ -158,6 +158,17 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
|
158
158
|
? effectiveRef
|
|
159
159
|
: path.resolve(sessionCwd, effectiveRef)
|
|
160
160
|
|
|
161
|
+
const fileStat = await stat(absPath).catch(() => null)
|
|
162
|
+
if (!fileStat) {
|
|
163
|
+
throw new Error(`Image file not found: ${effectiveRef}`)
|
|
164
|
+
}
|
|
165
|
+
if (fileStat.size === 0) {
|
|
166
|
+
throw new Error(`Image file is empty (0 bytes): ${effectiveRef}`)
|
|
167
|
+
}
|
|
168
|
+
if (fileStat.size > 50 * 1024 * 1024) {
|
|
169
|
+
throw new Error(`Image file too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
const bytes = await readFile(absPath)
|
|
162
173
|
const mediaType = detectMediaType(absPath, bytes)
|
|
163
174
|
return {
|
package/lib/security.js
CHANGED
|
@@ -42,6 +42,16 @@ export function sanitizeErrorAndLogs(input) {
|
|
|
42
42
|
return maskApiKey(token)
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
+
// Mask Replicate tokens r8_...
|
|
46
|
+
text = text.replace(/(r8_[a-zA-Z0-9_\-]{8,})/gi, (match, token) => {
|
|
47
|
+
return maskApiKey(token)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Mask Google Gemini API keys (starts with AIza and ~35-45 chars)
|
|
51
|
+
text = text.replace(/(AIza[0-9A-Za-z\-_]{30,45})/g, (match, token) => {
|
|
52
|
+
return maskApiKey(token)
|
|
53
|
+
})
|
|
54
|
+
|
|
45
55
|
return text
|
|
46
56
|
}
|
|
47
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.16",
|
|
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",
|