@goodandready/dsh-image-gen 0.10.14 → 0.10.15
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 +10 -0
- package/README.ru.md +10 -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 +36 -18
- package/lib/loop-guard.js +6 -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,16 @@
|
|
|
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.15
|
|
88
|
+
* **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.
|
|
89
|
+
* **Security Hardening (Token Masking)**: Added automatic pattern masking for Replicate API tokens (`r8_...`) and Google Gemini API keys (`AIza...`) in `sanitizeErrorAndLogs`.
|
|
90
|
+
* **Cache Resilience**: Isolated LRU touch updates in `getCachedGeneration` so that metadata write contention does not discard valid image bytes on cache hits.
|
|
91
|
+
* **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.
|
|
92
|
+
* **Vectorize Image Attachment & Toolview Preview**: `vectorize_image` now registers the SVG in `ctx.attachments` and provides `url`, enabling interactive preview in `tool.call.toolview`.
|
|
93
|
+
* **SVG Markup Validation**: Added strict validation in `optimizeSvgContent` rejecting non-SVG strings with descriptive errors.
|
|
94
|
+
* **Cost Meter Safety**: Hardened `assertBudgetAvailable` to handle string/undefined/NaN values safely.
|
|
95
|
+
* **Expanded Test Suite**: Added `test/batch3-quality.test.mjs` expanding suite to **126 automated tests (100% pass)**.
|
|
96
|
+
|
|
87
97
|
### 🚀 What's New in v0.10.14
|
|
88
98
|
* **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
99
|
* **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,16 @@ graph LR
|
|
|
130
130
|
- **100% интернационализация (#140)**: устранены захардкоженные русские строки, добавлены переводы на русский, английский и китайский языки.
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
### 🚀 Что нового в v0.10.15
|
|
134
|
+
* **Обход Loop Guard при нулевом лимите (#212)**: Исправлена ошибка, когда при значении `loopGuardLimit: 0` (отключение защиты) срабатывал `Math.max(1, 0) = 1` и блокировал вызовы на 2-й итерации. При значении 0 лимит теперь полностью отключается.
|
|
135
|
+
* **Усиление безопасности токенов**: В `sanitizeErrorAndLogs` добавлено маскирование токенов Replicate (`r8_...`) и ключей Google Gemini (`AIza...`).
|
|
136
|
+
* **Устойчивость дискового кэша**: Изолировано обновление метаданных LRU при чтении из кэша — ошибки записи метаданных больше не приводят к сбросу валидных байтов кэша.
|
|
137
|
+
* **Безопасность загрузки файлов**: В `resolveConversationImage` добавлены проверки существования файла, запрет 0-байтных файлов и ограничение 50 МБ для предотвращения переполнения памяти.
|
|
138
|
+
* **Вложения и Toolview для векторизации**: Инструмент `vectorize_image` теперь сохраняет SVG во вложения `ctx.attachments` и возвращает `url`, что активирует карточку интерактивного предпросмотра в чате.
|
|
139
|
+
* **Валидация SVG-разметки**: `optimizeSvgContent` теперь строго проверяет наличие корневого тега `<svg>` и выдает понятную ошибку при передаче не-SVG текста.
|
|
140
|
+
* **Безопасная обработка бюджета**: `assertBudgetAvailable` защищен от `NaN`, нечисловых и неопределенных значений стоимости.
|
|
141
|
+
* **Расширенное тестирование**: Добавлен набор тестов `test/batch3-quality.test.mjs`, общее число тестов выросло до **126 (100% pass)**.
|
|
142
|
+
|
|
133
143
|
### 🚀 Что нового в v0.10.14
|
|
134
144
|
* **Единый дизайн в стиле dsh-clinebot (#210)**: Интерфейс настроек переработан в соответствии со стандартами `dsh-clinebot`. Добавлен верхний дашборд быстрой статистики (`.ig-grid-4`, `.ig-stat-box`), отображающий активного провайдера, формат и размер, статус Quality Gate и лимит Loop Guard, дневной бюджет и состояние кэша.
|
|
135
145
|
* **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
|
@@ -1343,24 +1343,42 @@ export function apply(ctx, config) {
|
|
|
1343
1343
|
},
|
|
1344
1344
|
},
|
|
1345
1345
|
async execute(args, exec) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1346
|
+
try {
|
|
1347
|
+
const source = await resolveSource(ctx, exec, args.image)
|
|
1348
|
+
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
1349
|
+
const cfg = live()
|
|
1350
|
+
const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
1351
|
+
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
1352
|
+
const name = `${stem}.svg`
|
|
1353
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
1354
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
1355
|
+
await mkdir(outDir, { recursive: true })
|
|
1356
|
+
const filePath = path.join(outDir, name)
|
|
1357
|
+
await writeFile(filePath, res.bytes)
|
|
1358
|
+
|
|
1359
|
+
let attachment = null
|
|
1360
|
+
let localUrl = ''
|
|
1361
|
+
if (ctx.attachments?.saveImage) {
|
|
1362
|
+
attachment = await ctx.attachments.saveImage({
|
|
1363
|
+
data: new Uint8Array(res.bytes),
|
|
1364
|
+
mediaType: 'image/svg+xml',
|
|
1365
|
+
name,
|
|
1366
|
+
})
|
|
1367
|
+
localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
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`
|
|
1371
|
+
return toLosslessJson({
|
|
1372
|
+
summary,
|
|
1373
|
+
path: filePath,
|
|
1374
|
+
url: localUrl,
|
|
1375
|
+
svg: res.svg,
|
|
1376
|
+
format: 'svg',
|
|
1377
|
+
attachment,
|
|
1378
|
+
})
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
1381
|
+
}
|
|
1364
1382
|
},
|
|
1365
1383
|
}),
|
|
1366
1384
|
)
|
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/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.15",
|
|
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",
|