@goodandready/dsh-image-gen 0.10.25 → 0.10.27

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 CHANGED
@@ -41,6 +41,12 @@
41
41
 
42
42
  ---
43
43
 
44
+ ## 🚀 Updates v0.10.26: Architecture Decomposition & Repository Sanitization (#235, #238, #239)
45
+ * **Architecture & Modular Decomposition (#239)**: All server modules in `lib/` now strictly meet the <= 600 line threshold: extracted `lib/tools/generation-pack.js` (74 lines, reduces `lib/tools/generation.js` to 547 lines), `lib/history.js` (93 lines), and `lib/attachment-helper.js` (82 lines, reduces `lib/index.js` to 557 lines). Client codebase modularized across 15 sub-modules in `src/client/*` (all <= 337 lines).
46
+ * **Repository Sanitization (#235)**: Internal agent workflow files (`AGENTS.md`, `index.md`) fully untracked from Git and excluded via `.gitignore` and `.gitattributes`.
47
+ * **Theme Parity Completed (#238)**: Full verification and closure of zero-standalone-color rule across all Web UI surfaces with native `--dsw-alias-*` variables and `color-mix(...)` state tints.
48
+ * **Package Integrity**: Added canonical `README.ru.md` and `README.zh.md` to npm package `files` allowlist.
49
+
44
50
  ## 🚀 Updates v0.10.25: Theme Color Parity, Responsive Mockups, Patterns & Spritesheets (#173, #177, #178, #238)
45
51
  * **100% Theme Color Parity (#238)**: Completely replaced all hardcoded `rgba(...)` and hex color literals in the Web UI (`src/client/10-css.js`, `src/client/80-gallery.js`) with native DSH theme variables (`--dsw-alias-*`) and dynamic `color-mix(...)`. Badges, alerts, and modal overlays automatically adapt to light and dark themes with zero hardcoded values.
46
52
  * **Responsive Multi-Device Mockup Generator (`generate_responsive_mockups`, #173)**: Single product prompt generates coherent Mobile (9:16), Tablet (3:4), and Desktop (16:9) viewports in parallel via `asyncPool` concurrency limit (3). Interactive toolview with tabbed preview navigation.
package/README.ru.md CHANGED
@@ -34,6 +34,12 @@
34
34
  </div>
35
35
 
36
36
  ---
37
+ ## 🚀 Обновления v0.10.26: Архитектурная декомпозиция и чистота репозитория (#235, #238, #239)
38
+ * **Архитектурная декомпозиция модулей (#239)**: Все серверные модули в `lib/` приведены к проектному стандарту <= 600 строк: генерация пакетов вынесена в `lib/tools/generation-pack.js` (74 строки, `generation.js` снижен до 547 строк), кэш и история — в `lib/history.js` (93 строки), вложения и сайдкары — в `lib/attachment-helper.js` (82 строки, `index.js` снижен до 557 строк). Клиентский код полностью разбит на 15 модулей в `src/client/*` (каждый <= 337 строк).
39
+ * **Чистота репозитория (#235)**: Служебные файлы агентов (`AGENTS.md`, `index.md`) сняты с git-отслеживания и защищены правилами в `.gitignore` и `.gitattributes`.
40
+ * **Паритет тем оформления (#238)**: Полная верификация и закрытие правила нулевого хардкода цветов для всех поверхностей UI через системные токены `--dsw-alias-*` и `color-mix(...)`.
41
+ * **Целостность пакета**: Манифест `files` в `package.json` расширен языковыми файлами `README.ru.md` и `README.zh.md`.
42
+
37
43
  ## 🚀 Обновления v0.10.25: Чистота темы DSH, адаптивные мокапы, бесшовные паттерны и спрайтшиты (#173, #177, #178, #238)
38
44
  * **100% адаптация под переменные темы DSH (#238)**: Полностью устранены все захардкоженные литералы `rgba(...)` и hex-цветов в клиентской части (`src/client/10-css.js`, `src/client/80-gallery.js`). Полупрозрачные статусные фоны бейджей, алертов и модальных оверлеев переведены на динамическое смешивание токенов темы через `color-mix(...)`.
39
45
  * **Генератор адаптивных интерфейсных мокапов (`generate_responsive_mockups`, #173)**: Генерация согласованного набора экранов Mobile (9:16), Tablet (3:4) и Desktop (16:9) по единому промпту с параллельным выполнением через `asyncPool` (concurrency 3) и интерактивным таб-интерфейсом в карточке чата.
package/README.zh.md CHANGED
@@ -47,6 +47,12 @@
47
47
 
48
48
  ---
49
49
 
50
+ ## 🚀 更新 v0.10.26: 架构模块化拆分与仓库源码净化 (#235, #238, #239)
51
+ * **架构模块化拆分 (#239)**:所有服务端模块均严格符合 <= 600 行规范:提取 `lib/tools/generation-pack.js` (74行,`generation.js` 降至 547行)、`lib/history.js` (93行) 与 `lib/attachment-helper.js` (82行,`index.js` 降至 557行)。客户端源码解耦为 `src/client/*` 15 个子模块(均 <= 337 行)。
52
+ * **仓库源码净化 (#235)**:内部智能体指令文件 (`AGENTS.md`, `index.md`) 已从 Git 索引彻底移除并加入 `.gitignore` 与 `.gitattributes`。
53
+ * **主题规范对齐 (#238)**:全面完成所有 Web UI 界面的零硬编码颜色验证与关闭,深度适配 `--dsw-alias-*` 与 `color-mix(...)`。
54
+ * **包分发完整性**:正式将 `README.ru.md` 与 `README.zh.md` 收录至 npm `files` 发布白名单。
55
+
50
56
  ## 🚀 更新 v0.10.25: DSH 主题颜色适配、响应式多端原型、无缝纹理与精灵图导出 (#173, #177, #178, #238)
51
57
  * **100% 主题变量原生适配 (#238)**: 彻底移除客户端代码中的所有写死 `rgba(...)` 和 Hex 颜色(`src/client/10-css.js`、`src/client/80-gallery.js`),徽章、提示栏和画廊遮罩全部通过 `color-mix(...)` 配合 `--dsw-alias-*` 主题变量动态生成,完美兼容深色与浅色主题。
52
58
  * **多端响应式原型生成器 (`generate_responsive_mockups`, #173)**: 输入单个产品提示词,并发生成风格统一的移动端 (9:16)、平板端 (3:4) 与桌面端 (16:9) UI 原型,支持聊天卡片中的分栏切换预览。
@@ -0,0 +1,82 @@
1
+ import path from 'node:path'
2
+ import { mkdir, writeFile } from 'node:fs/promises'
3
+ import { saveAttachmentSafe, buildSidecar, toLosslessJson } from './providers.js'
4
+ import { buildDualOutputMarkdown } from './resolve-image.js'
5
+
6
+ export async function saveAndAttachResult(ctx, exec, cfg, {
7
+ bytes,
8
+ mediaType,
9
+ name,
10
+ stem,
11
+ prompt,
12
+ size,
13
+ format,
14
+ seed,
15
+ provider,
16
+ model,
17
+ cost,
18
+ sourceUrl,
19
+ deliverAs,
20
+ args = {},
21
+ action = 'generated',
22
+ }) {
23
+ const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes, mediaType, name })
24
+
25
+ const sessionCwd = exec?.agent?.session?.header?.cwd
26
+ const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
27
+ const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
28
+ await mkdir(outDir, { recursive: true })
29
+ const filePath = path.join(outDir, name)
30
+ await writeFile(filePath, bytes)
31
+
32
+ await writeFile(
33
+ path.join(outDir, `${stem}.json`),
34
+ JSON.stringify(buildSidecar({
35
+ prompt,
36
+ size,
37
+ format,
38
+ seed,
39
+ provider,
40
+ deliverAs,
41
+ width: attachment.width,
42
+ height: attachment.height,
43
+ mediaType,
44
+ attachmentId: attachment.attachmentId,
45
+ url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
46
+ cost,
47
+ }), null, 2),
48
+ )
49
+
50
+ const summary = buildDualOutputMarkdown({
51
+ action,
52
+ filePath,
53
+ width: attachment.width,
54
+ height: attachment.height,
55
+ mediaType,
56
+ seed,
57
+ provider,
58
+ model: model || cfg.model || cfg.customModel || 'default',
59
+ cost,
60
+ attachmentId: attachment.attachmentId,
61
+ })
62
+
63
+ return toLosslessJson({
64
+ summary,
65
+ path: filePath,
66
+ url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
67
+ width: attachment.width,
68
+ height: attachment.height,
69
+ seed,
70
+ prompt,
71
+ cost,
72
+ format: mediaType.replace('image/', ''),
73
+ attachment: {
74
+ attachmentId: attachment.attachmentId,
75
+ mediaType: attachment.mediaType,
76
+ bytes: attachment.bytes,
77
+ width: attachment.width,
78
+ height: attachment.height,
79
+ name: attachment.name,
80
+ },
81
+ })
82
+ }
package/lib/history.js ADDED
@@ -0,0 +1,93 @@
1
+ import path from 'node:path'
2
+ import os from 'node:os'
3
+ import { existsSync, unlinkSync } from 'node:fs'
4
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
5
+ import { enforceSecurePermissions } from './security.js'
6
+
7
+ export function historyFile() {
8
+ return path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'dsh-image-gen', 'history.json')
9
+ }
10
+
11
+ /** Read history from disk file (empty array if not found). */
12
+ /** Find history entry matching seed+prompt if file exists. */
13
+ /** Find history entry matching prompt text if file exists. */
14
+ export function findCachedGeneration(entries, hash) {
15
+ if (!hash || !Array.isArray(entries)) return undefined
16
+ return entries.find((e) => e.cacheHash === hash)
17
+ }
18
+
19
+ export async function findCachedByPrompt(entries, prompt) {
20
+ return entries.find((e) => e.prompt === prompt)
21
+ }
22
+
23
+ export async function findCached(entries, seed, prompt) {
24
+ if (seed === undefined) return undefined
25
+ return entries.find((e) => e.seed === seed && e.prompt === prompt)
26
+ }
27
+
28
+ /** Return cached entry if file exists on disk, otherwise undefined. */
29
+ export async function cachedResult(entry) {
30
+ if (!entry || !entry.path) return undefined
31
+ if (!entry.path || !existsSync(entry.path)) return undefined
32
+ return {
33
+ path: entry.path,
34
+ url: entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : '',
35
+ thumbnailUrl: entry.thumbnailUrl || (entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : ''),
36
+ width: entry.width || 1024,
37
+ height: entry.height || 1024,
38
+ seed: entry.seed,
39
+ prompt: entry.prompt,
40
+ cost: 0,
41
+ format: (entry.mediaType || 'image/png').replace('image/', ''),
42
+ cached: true,
43
+ attachment: entry.attachmentId ? {
44
+ attachmentId: entry.attachmentId,
45
+ mediaType: entry.mediaType || 'image/png',
46
+ bytes: entry.bytes || 0,
47
+ width: entry.width || 1024,
48
+ height: entry.height || 1024,
49
+ name: entry.name || '',
50
+ } : undefined,
51
+ }
52
+ }
53
+
54
+ /** Prune files and history records older than pruneDays (including sidecars). */
55
+ export async function pruneHistory(entries, pruneDays) {
56
+ if (!pruneDays || pruneDays <= 0) return entries
57
+ const cutoff = Date.now() - pruneDays * 86400000
58
+ const kept = []
59
+ for (const e of entries) {
60
+ const created = e.createdAt ? Date.parse(e.createdAt) : NaN
61
+ if (Number.isFinite(created) && created < cutoff) {
62
+ try { unlinkSync(e.path) } catch (err) { /* file already deleted */ }
63
+ try { unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
64
+ continue
65
+ }
66
+ kept.push(e)
67
+ }
68
+ return kept
69
+ }
70
+
71
+ export async function readHistory() {
72
+ try {
73
+ const raw = await readFile(historyFile(), 'utf-8')
74
+ const parsed = JSON.parse(raw)
75
+ return Array.isArray(parsed) ? parsed : []
76
+ } catch (e) {
77
+ return []
78
+ }
79
+ }
80
+
81
+ /** Save history to disk file (atomic overwrite). */
82
+ export async function writeHistory(entries) {
83
+ try {
84
+ await mkdir(path.dirname(historyFile()), { recursive: true })
85
+ await writeFile(historyFile(), JSON.stringify(entries, null, 2))
86
+ enforceSecurePermissions(historyFile())
87
+ } catch (e) { /* history write failure is non-fatal */ }
88
+ }
89
+
90
+ /** Filter entries whose files exist on disk; newest first. */
91
+ export function filterHistory(entries, exists) {
92
+ return entries.filter((e) => exists(e.path)).slice(0, 50)
93
+ }
package/lib/index.js CHANGED
@@ -16,6 +16,18 @@ import z from '@deepseek-ai/schemastery'
16
16
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
17
17
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
18
18
  import { enforceSecurePermissions } from './security.js'
19
+ import { saveAndAttachResult } from './attachment-helper.js'
20
+ import {
21
+ historyFile,
22
+ findCachedGeneration,
23
+ findCachedByPrompt,
24
+ findCached,
25
+ cachedResult,
26
+ pruneHistory,
27
+ readHistory,
28
+ writeHistory,
29
+ filterHistory,
30
+ } from './history.js'
19
31
  import { existsSync, unlinkSync } from 'node:fs'
20
32
  import os from 'node:os'
21
33
  import path from 'node:path'
@@ -37,6 +49,18 @@ import { registerPluginUpdater } from './updater.js'
37
49
 
38
50
 
39
51
  export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMediaType, resolveConversationImage, analyzeImageWithVision }
52
+ export { saveAndAttachResult }
53
+ export {
54
+ historyFile,
55
+ findCachedGeneration,
56
+ findCachedByPrompt,
57
+ findCached,
58
+ cachedResult,
59
+ pruneHistory,
60
+ readHistory,
61
+ writeHistory,
62
+ filterHistory,
63
+ }
40
64
 
41
65
 
42
66
  /**
@@ -45,84 +69,6 @@ export { IMAGE_SIZES, OUTPUT_FORMATS, PROVIDER_KEYS, buildSidecar, normalizeMedi
45
69
 
46
70
 
47
71
 
48
- export async function saveAndAttachResult(ctx, exec, cfg, {
49
- bytes,
50
- mediaType,
51
- name,
52
- stem,
53
- prompt,
54
- size,
55
- format,
56
- seed,
57
- provider,
58
- model,
59
- cost,
60
- sourceUrl,
61
- deliverAs,
62
- args = {},
63
- action = 'generated',
64
- }) {
65
- const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes, mediaType, name })
66
-
67
- const sessionCwd = exec?.agent?.session?.header?.cwd
68
- const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
69
- const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
70
- await mkdir(outDir, { recursive: true })
71
- const filePath = path.join(outDir, name)
72
- await writeFile(filePath, bytes)
73
-
74
- await writeFile(
75
- path.join(outDir, `${stem}.json`),
76
- JSON.stringify(buildSidecar({
77
- prompt,
78
- size,
79
- format,
80
- seed,
81
- provider,
82
- deliverAs,
83
- width: attachment.width,
84
- height: attachment.height,
85
- mediaType,
86
- attachmentId: attachment.attachmentId,
87
- url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
88
- cost,
89
- }), null, 2),
90
- )
91
-
92
- const summary = buildDualOutputMarkdown({
93
- action,
94
- filePath,
95
- width: attachment.width,
96
- height: attachment.height,
97
- mediaType,
98
- seed,
99
- provider,
100
- model: model || cfg.model || cfg.customModel || 'default',
101
- cost,
102
- attachmentId: attachment.attachmentId,
103
- })
104
-
105
- return toLosslessJson({
106
- summary,
107
- path: filePath,
108
- url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
109
- width: attachment.width,
110
- height: attachment.height,
111
- seed,
112
- prompt,
113
- cost,
114
- format: mediaType.replace('image/', ''),
115
- attachment: {
116
- attachmentId: attachment.attachmentId,
117
- mediaType: attachment.mediaType,
118
- bytes: attachment.bytes,
119
- width: attachment.width,
120
- height: attachment.height,
121
- name: attachment.name,
122
- },
123
- })
124
- }
125
-
126
72
  export const name = '@goodandready/dsh-image-gen'
127
73
 
128
74
  /** Settings namespace the Web card edits. */
@@ -394,94 +340,6 @@ function migrateLegacySettings(sctx, scope) {
394
340
  }
395
341
 
396
342
  /** History directory: persists across restarts. */
397
- export function historyFile() {
398
- return path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'dsh-image-gen', 'history.json')
399
- }
400
-
401
- /** Read history from disk file (empty array if not found). */
402
- /** Find history entry matching seed+prompt if file exists. */
403
- /** Find history entry matching prompt text if file exists. */
404
- export function findCachedGeneration(entries, hash) {
405
- if (!hash || !Array.isArray(entries)) return undefined
406
- return entries.find((e) => e.cacheHash === hash)
407
- }
408
-
409
- export async function findCachedByPrompt(entries, prompt) {
410
- return entries.find((e) => e.prompt === prompt)
411
- }
412
-
413
- export async function findCached(entries, seed, prompt) {
414
- if (seed === undefined) return undefined
415
- return entries.find((e) => e.seed === seed && e.prompt === prompt)
416
- }
417
-
418
- /** Return cached entry if file exists on disk, otherwise undefined. */
419
- export async function cachedResult(entry) {
420
- if (!entry || !entry.path) return undefined
421
- if (!entry.path || !existsSync(entry.path)) return undefined
422
- return {
423
- path: entry.path,
424
- url: entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : '',
425
- thumbnailUrl: entry.thumbnailUrl || (entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : ''),
426
- width: entry.width || 1024,
427
- height: entry.height || 1024,
428
- seed: entry.seed,
429
- prompt: entry.prompt,
430
- cost: 0,
431
- format: (entry.mediaType || 'image/png').replace('image/', ''),
432
- cached: true,
433
- attachment: entry.attachmentId ? {
434
- attachmentId: entry.attachmentId,
435
- mediaType: entry.mediaType || 'image/png',
436
- bytes: entry.bytes || 0,
437
- width: entry.width || 1024,
438
- height: entry.height || 1024,
439
- name: entry.name || '',
440
- } : undefined,
441
- }
442
- }
443
-
444
- /** Prune files and history records older than pruneDays (including sidecars). */
445
- export async function pruneHistory(entries, pruneDays) {
446
- if (!pruneDays || pruneDays <= 0) return entries
447
- const cutoff = Date.now() - pruneDays * 86400000
448
- const kept = []
449
- for (const e of entries) {
450
- const created = e.createdAt ? Date.parse(e.createdAt) : NaN
451
- if (Number.isFinite(created) && created < cutoff) {
452
- try { unlinkSync(e.path) } catch (err) { /* file already deleted */ }
453
- try { unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
454
- continue
455
- }
456
- kept.push(e)
457
- }
458
- return kept
459
- }
460
-
461
- export async function readHistory() {
462
- try {
463
- const raw = await readFile(historyFile(), 'utf-8')
464
- const parsed = JSON.parse(raw)
465
- return Array.isArray(parsed) ? parsed : []
466
- } catch (e) {
467
- return []
468
- }
469
- }
470
-
471
- /** Save history to disk file (atomic overwrite). */
472
- export async function writeHistory(entries) {
473
- try {
474
- await mkdir(path.dirname(historyFile()), { recursive: true })
475
- await writeFile(historyFile(), JSON.stringify(entries, null, 2))
476
- enforceSecurePermissions(historyFile())
477
- } catch (e) { /* history write failure is non-fatal */ }
478
- }
479
-
480
- /** Filter entries whose files exist on disk; newest first. */
481
- export function filterHistory(entries, exists) {
482
- return entries.filter((e) => exists(e.path)).slice(0, 50)
483
- }
484
-
485
343
  /** Collect text chunks from llm.stream iterator. */
486
344
  export async function collectText(iterable) {
487
345
  let out = ''
@@ -0,0 +1,74 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import { toLosslessJson } from '../providers.js'
3
+ import { slugify } from '../index.js'
4
+
5
+ export function registerGenerationPackTool(ctx, deps) {
6
+ const { live } = deps
7
+
8
+ ctx.effect(() => {
9
+ ctx.tools.register(
10
+ defineTool({
11
+ name: 'generate_image_pack',
12
+ description: 'Generate a multi-aspect ratio pack of the same image (e.g. 1:1, 16:9, 9:16) for different platforms.',
13
+ parameters: {
14
+ prompt: { type: 'string', required: true, description: 'Text prompt for image generation.' },
15
+ aspect_ratios: { type: 'array', items: { type: 'string' }, description: 'Target ratios (default: ["1:1", "16:9", "9:16"]).' },
16
+ output_name: { type: 'string', description: 'Base file name for the pack.' },
17
+ },
18
+ output: {
19
+ schema: {
20
+ type: 'object',
21
+ additionalProperties: true,
22
+ properties: {
23
+ images: { type: 'array', items: { type: 'object', additionalProperties: true } },
24
+ count: { type: 'number' },
25
+ warnings: { type: 'array', items: { type: 'string' } },
26
+ },
27
+ },
28
+ },
29
+ async execute(args, exec) {
30
+ const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
31
+ const cfg = live()
32
+ const generateTool = ctx.tools.get('generate_image')
33
+ const results = []
34
+ const warnings = []
35
+ const seedBase = Math.floor(Math.random() * 100000)
36
+
37
+ const packTasks = ratios.map((ratio) => async () => {
38
+ const sizeName = ratio === '16:9' ? 'landscape_16_9' : ratio === '9:16' ? 'portrait_16_9' : ratio === '4:3' ? 'landscape_4_3' : ratio === '3:4' ? 'portrait_4_3' : 'square_hd'
39
+ const name = `${slugify(args.output_name || 'pack')}-${ratio.replace(':', 'x')}`
40
+ try {
41
+ const genResult = await generateTool.execute({
42
+ prompt: args.prompt,
43
+ image_size: sizeName,
44
+ seed: seedBase,
45
+ output_name: name,
46
+ }, exec)
47
+ return { success: true, ratio, genResult }
48
+ } catch (err) {
49
+ return { success: false, ratio, error: err?.message || String(err) }
50
+ }
51
+ })
52
+ const settledPack = await Promise.all(packTasks.map((t) => t()))
53
+ for (const item of settledPack) {
54
+ if (item.success) {
55
+ results.push({ ratio: item.ratio, ...item.genResult })
56
+ } else {
57
+ warnings.push(`Ratio ${item.ratio} failed: ${item.error}`)
58
+ }
59
+ }
60
+
61
+ if (results.length === 0 && warnings.length > 0) {
62
+ throw new Error(`All aspect ratio generations failed in pack: ${warnings.join('; ')}`)
63
+ }
64
+
65
+ return toLosslessJson({
66
+ images: results,
67
+ count: results.length,
68
+ ...(warnings.length ? { warnings } : {}),
69
+ })
70
+ },
71
+ }),
72
+ )
73
+ }, 'dsh-image-gen: tool generate_image_pack')
74
+ }
@@ -1,4 +1,5 @@
1
1
  import { polishPrompt } from '../prompt-polisher.js'
2
+ import { registerGenerationPackTool } from './generation-pack.js'
2
3
 
3
4
  /** Run an array of async task functions with a concurrency cap */
4
5
  async function asyncPool(tasks, concurrency = 3) {
@@ -542,70 +543,5 @@ export function registerGenerationTools(ctx, deps) {
542
543
  }),
543
544
  )
544
545
  }, 'dsh-image-gen: tool generate_image')
545
- ctx.effect(() => {
546
- ctx.tools.register(
547
- defineTool({
548
- name: 'generate_image_pack',
549
- description: 'Generate a multi-aspect ratio pack of the same image (e.g. 1:1, 16:9, 9:16) for different platforms.',
550
- parameters: {
551
- prompt: { type: 'string', required: true, description: 'Text prompt for image generation.' },
552
- aspect_ratios: { type: 'array', items: { type: 'string' }, description: 'Target ratios (default: ["1:1", "16:9", "9:16"]).' },
553
- output_name: { type: 'string', description: 'Base file name for the pack.' },
554
- },
555
- output: {
556
- schema: {
557
- type: 'object',
558
- additionalProperties: true,
559
- properties: {
560
- images: { type: 'array', items: { type: 'object', additionalProperties: true } },
561
- count: { type: 'number' },
562
- warnings: { type: 'array', items: { type: 'string' } },
563
- },
564
- },
565
- },
566
- async execute(args, exec) {
567
- const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
568
- const cfg = live()
569
- const generateTool = ctx.tools.get('generate_image')
570
- const results = []
571
- const warnings = []
572
- const seedBase = Math.floor(Math.random() * 100000)
573
-
574
- const packTasks = ratios.map((ratio) => async () => {
575
- const sizeName = ratio === '16:9' ? 'landscape_16_9' : ratio === '9:16' ? 'portrait_16_9' : ratio === '4:3' ? 'landscape_4_3' : ratio === '3:4' ? 'portrait_4_3' : 'square_hd'
576
- const name = `${slugify(args.output_name || 'pack')}-${ratio.replace(':', 'x')}`
577
- try {
578
- const genResult = await generateTool.execute({
579
- prompt: args.prompt,
580
- image_size: sizeName,
581
- seed: seedBase,
582
- output_name: name,
583
- }, exec)
584
- return { success: true, ratio, genResult }
585
- } catch (err) {
586
- return { success: false, ratio, error: err?.message || String(err) }
587
- }
588
- })
589
- const settledPack = await Promise.all(packTasks.map((t) => t()))
590
- for (const item of settledPack) {
591
- if (item.success) {
592
- results.push({ ratio: item.ratio, ...item.genResult })
593
- } else {
594
- warnings.push(`Ratio ${item.ratio} failed: ${item.error}`)
595
- }
596
- }
597
-
598
- if (results.length === 0 && warnings.length > 0) {
599
- throw new Error(`All aspect ratio generations failed in pack: ${warnings.join('; ')}`)
600
- }
601
-
602
- return toLosslessJson({
603
- images: results,
604
- count: results.length,
605
- ...(warnings.length ? { warnings } : {}),
606
- })
607
- },
608
- }),
609
- )
610
- }, 'dsh-image-gen: tool generate_image_pack')
546
+ registerGenerationPackTool(ctx, deps)
611
547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.25",
3
+ "version": "0.10.27",
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",
@@ -36,7 +36,9 @@
36
36
  "lib/",
37
37
  "cordis.patch.yml",
38
38
  "README.md",
39
- "LICENSE"
39
+ "LICENSE",
40
+ "README.ru.md",
41
+ "README.zh.md"
40
42
  ],
41
43
  "license": "MIT",
42
44
  "engines": {