@goodandready/dsh-image-gen 0.10.24 → 0.10.26
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 +14 -0
- package/README.ru.md +14 -0
- package/README.zh.md +14 -0
- package/lib/attachment-helper.js +82 -0
- package/lib/client.js +149 -21
- package/lib/history.js +93 -0
- package/lib/index.js +25 -167
- package/lib/pattern-helpers.js +87 -0
- package/lib/providers/backends/custom.js +93 -0
- package/lib/providers/backends/fal.js +67 -0
- package/lib/providers/backends/gemini.js +37 -0
- package/lib/providers/backends/local.js +136 -0
- package/lib/providers/backends/replicate.js +83 -0
- package/lib/providers/backends/seedream.js +49 -0
- package/lib/providers/backends/subscription.js +58 -0
- package/lib/providers/shared-helpers.js +164 -0
- package/lib/providers.js +46 -553
- package/lib/register-tools.js +8 -0
- package/lib/responsive-helpers.js +94 -0
- package/lib/spritesheet-helpers.js +65 -0
- package/lib/tools/editing.js +2 -0
- package/lib/tools/generation-pack.js +74 -0
- package/lib/tools/generation.js +2 -66
- package/lib/tools/pattern.js +125 -0
- package/lib/tools/processing-advanced.js +7 -1
- package/lib/tools/responsive.js +267 -0
- package/lib/tools/sketch.js +167 -0
- package/lib/tools/spritesheet.js +151 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -41,6 +41,19 @@
|
|
|
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
|
+
|
|
50
|
+
## 🚀 Updates v0.10.25: Theme Color Parity, Responsive Mockups, Patterns & Spritesheets (#173, #177, #178, #238)
|
|
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.
|
|
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.
|
|
53
|
+
* **Seamless Pattern Generator (`generate_seamless_pattern`, #178)**: Automated tileable background texture generation with periodic boundary condition checks and CSS tiling preview.
|
|
54
|
+
* **2D Spritesheet Generator (`generate_spritesheet`, #177)**: Produces horizontal animation strips with CSS keyframe generator and multi-frame SVG previews for 2D game dev and web UI animations.
|
|
55
|
+
* **Client Modular Source Build (#244)**: Decomposed monolithic `lib/client.js` into 15 structured fragments under `src/client/*` with automated build runner `scripts/build-client.mjs`. Preflight check `FAIL=0`, 179/179 automated tests passing.
|
|
56
|
+
|
|
44
57
|
## 🚀 Updates v0.10.24: Quality Block — Publication Boundary, Settings Status, Theme Colors & Module Split (#235–#242)
|
|
45
58
|
* **Publication Boundary**: Added `.gitattributes` with `export-ignore` for `AGENTS.md`, `index.md`, `docs/`, and `.gitea/` so GitHub source archives and `git archive` no longer ship internal workflow files.
|
|
46
59
|
* **Settings Status Fallback**: Corrected provider/config status handling in the Settings Card so partial/unknown states no longer show a false “unavailable”.
|
|
@@ -117,6 +130,7 @@
|
|
|
117
130
|
* **`vectorize_image`**: Convert raster graphics to clean scalable SVG vectors with palette quantization.
|
|
118
131
|
* **`blend_images`**: Multi-reference composition mixing.
|
|
119
132
|
* **`generate_image_pack`**: Simultaneous multi-aspect ratio rendering with graceful partial recovery.
|
|
133
|
+
* **`generate_responsive_mockups` (#173)**: One shared product prompt rendered as Mobile (9:16), Tablet (3:4) and Desktop (16:9) UI mockups in parallel. Keeps style/palette coherent across viewports, saves `mockup-mobile.png` / `mockup-tablet.png` / `mockup-desktop.png`, and shows a tabbed viewport preview in chat.
|
|
120
134
|
* **`compare_images`**: Pixel-level visual difference ratio comparison.
|
|
121
135
|
* **`inspect_image_quality`**: Automated visual audit, Laplacian sharpness scoring, and defect detection.
|
|
122
136
|
* **`extract_design_tokens`**: Extract CSS Variables, Tailwind color palettes, and W3C Design Tokens from concept art (#172).
|
package/README.ru.md
CHANGED
|
@@ -34,6 +34,19 @@
|
|
|
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
|
+
|
|
43
|
+
## 🚀 Обновления v0.10.25: Чистота темы DSH, адаптивные мокапы, бесшовные паттерны и спрайтшиты (#173, #177, #178, #238)
|
|
44
|
+
* **100% адаптация под переменные темы DSH (#238)**: Полностью устранены все захардкоженные литералы `rgba(...)` и hex-цветов в клиентской части (`src/client/10-css.js`, `src/client/80-gallery.js`). Полупрозрачные статусные фоны бейджей, алертов и модальных оверлеев переведены на динамическое смешивание токенов темы через `color-mix(...)`.
|
|
45
|
+
* **Генератор адаптивных интерфейсных мокапов (`generate_responsive_mockups`, #173)**: Генерация согласованного набора экранов Mobile (9:16), Tablet (3:4) и Desktop (16:9) по единому промпту с параллельным выполнением через `asyncPool` (concurrency 3) и интерактивным таб-интерфейсом в карточке чата.
|
|
46
|
+
* **Генератор бесшовных текстур и паттернов (`generate_seamless_pattern`, #178)**: Создание тайлящихся фоновых текстур со встроенным контролем швов и готовым CSS-предпросмотром.
|
|
47
|
+
* **Генератор 2D-спрайтшитов (`generate_spritesheet`, #177)**: Формирование горизонтальных раскадровок анимации персонажей и UI с автогенерацией CSS `@keyframes` и SVG-превью полосы кадров.
|
|
48
|
+
* **Модульная сборка клиента (#244)**: Клиентская часть разнесена на 15 структурированных модулей в `src/client/*` со скриптом сборки `scripts/build-client.mjs`. Preflight-проверка `FAIL=0`, 179 из 179 тестов успешно пройдены.
|
|
49
|
+
|
|
37
50
|
## 🚀 Обновления v0.10.24: Блок качества — публикационная граница, статусы настроек, цвета темы и разбиение модулей (#235–#242)
|
|
38
51
|
* **Публикационная граница**: добавлен `.gitattributes` c `export-ignore` для `AGENTS.md`, `index.md`, `docs/` и `.gitea/` — архивы GitHub source и `git archive` больше не содержат внутренние файлы workflow.
|
|
39
52
|
* **Статусы настроек**: исправлен fallback статуса провайдера/конфигурации в карточке настроек — промежуточные/неизвестные состояния больше не показываются ложным «unavailable».
|
|
@@ -157,6 +170,7 @@ graph LR
|
|
|
157
170
|
| **`vectorize_image`** | Векторизация растра в чистый SVG с квантованием палитры | `image`, `color_mode`, `paletteSize` |
|
|
158
171
|
| **`blend_images`** | Смешивание нескольких изображений в единую композицию | `images`, `weights`, `prompt` |
|
|
159
172
|
| **`generate_image_pack`** | Пакетный рендеринг форматов 1:1, 16:9, 9:16 с сохранением частичных результатов | `prompt`, `aspect_ratios` |
|
|
173
|
+
| **`generate_responsive_mockups`** | Пакетная адаптивная генерация UI-mockup под Mobile 9:16, Tablet 3:4 и Desktop 16:9 с единой палитрой/стилем; табы в toolview (#173) | `prompt`, `devices`, `style_preset`, `palette_colors`, `seed` |
|
|
160
174
|
| **`compare_images`** | Сравнение двух картинок (доля различий пикселей) | `image_a`, `image_b` |
|
|
161
175
|
| **`inspect_image_quality`** | Аудит качества, расчет резкости и проверка дефектов | `image`, `expected_elements` |
|
|
162
176
|
|
package/README.zh.md
CHANGED
|
@@ -47,6 +47,19 @@
|
|
|
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
|
+
|
|
56
|
+
## 🚀 更新 v0.10.25: DSH 主题颜色适配、响应式多端原型、无缝纹理与精灵图导出 (#173, #177, #178, #238)
|
|
57
|
+
* **100% 主题变量原生适配 (#238)**: 彻底移除客户端代码中的所有写死 `rgba(...)` 和 Hex 颜色(`src/client/10-css.js`、`src/client/80-gallery.js`),徽章、提示栏和画廊遮罩全部通过 `color-mix(...)` 配合 `--dsw-alias-*` 主题变量动态生成,完美兼容深色与浅色主题。
|
|
58
|
+
* **多端响应式原型生成器 (`generate_responsive_mockups`, #173)**: 输入单个产品提示词,并发生成风格统一的移动端 (9:16)、平板端 (3:4) 与桌面端 (16:9) UI 原型,支持聊天卡片中的分栏切换预览。
|
|
59
|
+
* **无缝平铺纹理生成器 (`generate_seamless_pattern`, #178)**: 自动生成四方连续平铺背景图,内置接缝平滑度校验与 CSS 平铺预览参数。
|
|
60
|
+
* **2D 逐帧精灵图生成器 (`generate_spritesheet`, #177)**: 自动化横向动画序列帧渲染,内置 CSS `@keyframes` 动画代码与 SVG 多帧预览条。
|
|
61
|
+
* **客户端源码模块化重构 (#244)**: 将原本庞大的单文件拆分为 `src/client/*` 目录下 15 个独立职责模块,配套 `scripts/build-client.mjs` 构建机制。Preflight 检查 `FAIL=0`,179/179 项自动化测试全绿。
|
|
62
|
+
|
|
50
63
|
## 🚀 更新 v0.10.24: 质量加固 — 发布边界、设置状态、主题颜色与模块拆分 (#235–#242)
|
|
51
64
|
* **发布边界**: 新增 `.gitattributes`,对 `AGENTS.md`、`index.md`、`docs/`、`.gitea/` 设置 `export-ignore`,GitHub source 归档与 `git archive` 不再包含内部工作流文件。
|
|
52
65
|
* **设置状态回退**: 修正设置卡片中的提供方/配置状态处理,部分或未知状态不再误显示为 “unavailable”。
|
|
@@ -71,6 +84,7 @@
|
|
|
71
84
|
* **智能比例无损画布裁切器 (`smart_crop_image`)**: 快速裁切适配各大社交与设计标准比例 (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`),支持自动对焦、三分法构图 (`rule_of_thirds`) 与黑边留白 (`letterbox`)。
|
|
72
85
|
* **品牌资源套件导出器 (`export_asset_pack`)**: 一键生成生产级网站与 PWA 资源套件:SVG Favicon、PWA 推荐图标套件 (`icon-192.svg`, `icon-512.svg`)、`manifest.webmanifest`、1200×630 社交分享预览卡片 (`og-card.svg`) 与索引 `catalog.json`。
|
|
73
86
|
* **覆盖 12 大视觉工具的交互 Toolview**: 在对话中直接提供重混、放大、裁切与栅格拼图的可视化操作。
|
|
87
|
+
* **响应式多端 Mockup 套件 (`generate_responsive_mockups`, #173)**: 同一产品提示词并行生成 Mobile 9:16、Tablet 3:4、Desktop 16:9 三端 UI mockup,统一风格与品牌色,保存为 `mockup-mobile.png` / `mockup-tablet.png` / `mockup-desktop.png`,对话内以三标签页预览。
|
|
74
88
|
* **严谨的中英双语界面**: 完整的 `zh` 和 `en` 词典覆盖。
|
|
75
89
|
|
|
76
90
|
## 🚀 更新 v0.10.19: 侧边栏内置图库、连接诊断与风格引擎 (#224)
|
|
@@ -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/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GENERATED by scripts/build-client.mjs — do not edit. Sources: src/client/*
|
|
1
2
|
// Plugin Settings Card (settings.plugin.item) and Toolviews for @goodandready/dsh-image-gen
|
|
2
3
|
window.__ModuleLoader__.load({
|
|
3
4
|
id: '@goodandready/dsh-image-gen',
|
|
@@ -75,9 +76,9 @@ window.__ModuleLoader__.load({
|
|
|
75
76
|
.ig-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
|
|
76
77
|
|
|
77
78
|
.ig-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
|
|
78
|
-
.ig-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:
|
|
79
|
-
.ig-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
80
|
-
.ig-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
79
|
+
.ig-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}
|
|
80
|
+
.ig-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}
|
|
81
|
+
.ig-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent)}
|
|
81
82
|
|
|
82
83
|
.ig-tabs{display:flex;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:8px;margin-bottom:6px;overflow-x:auto}
|
|
83
84
|
.ig-tab-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:6px;padding:6px 12px;font-size:13px;background:transparent;color:var(--dsw-alias-label-secondary);font-weight:500;display:inline-flex;align-items:center;gap:6px;transition:all .15s ease}
|
|
@@ -100,22 +101,22 @@ window.__ModuleLoader__.load({
|
|
|
100
101
|
.ig-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
101
102
|
.ig-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
102
103
|
.ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
|
|
103
|
-
.ig-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg,
|
|
104
|
+
.ig-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent));border-color:var(--dsw-alias-state-error-primary)}
|
|
104
105
|
.ig-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
105
106
|
|
|
106
107
|
.ig-reset{font-size:11px;color:var(--dsw-alias-state-brand-primary);background:none;border:none;cursor:pointer;padding:0;text-decoration:underline}
|
|
107
108
|
.ig-reset:hover{opacity:0.8}
|
|
108
109
|
|
|
109
|
-
.ig-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg,
|
|
110
|
-
.ig-alert-bad{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg,
|
|
111
|
-
.ig-alert-err{padding:10px 14px;border-radius:8px;background:
|
|
112
|
-
.ig-banner-warning{padding:12px 16px;border-radius:8px;background:
|
|
110
|
+
.ig-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg, color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent));color:var(--dsw-alias-state-success-primary);font-size:13px}
|
|
111
|
+
.ig-alert-bad{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent));color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
112
|
+
.ig-alert-err{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent));color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
113
|
+
.ig-banner-warning{padding:12px 16px;border-radius:8px;background:var(--dsw-alias-state-warning-bg, color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent));border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);font-size:13px;display:flex;align-items:center;gap:10px;font-weight:500}
|
|
113
114
|
|
|
114
115
|
/* Backward-compatibility alias styles for toolview */
|
|
115
116
|
.fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
|
|
116
117
|
.ig-prompt{font-size:13px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border-radius:6px;border:1px solid var(--dsw-alias-border-l2);margin-bottom:8px;word-break:break-word}
|
|
117
118
|
.ig-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:6px}
|
|
118
|
-
.ig-err{padding:10px;border-radius:6px;background:var(--dsw-alias-state-error-bg,
|
|
119
|
+
.ig-err{padding:10px;border-radius:6px;background:var(--dsw-alias-state-error-bg, color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent));color:var(--dsw-alias-state-error-primary);font-size:12px}
|
|
119
120
|
`
|
|
120
121
|
document.head.appendChild(style)
|
|
121
122
|
}
|
|
@@ -826,9 +827,9 @@ window.__ModuleLoader__.load({
|
|
|
826
827
|
left: 0,
|
|
827
828
|
right: 0,
|
|
828
829
|
padding: '4px 6px',
|
|
829
|
-
background: 'linear-gradient(transparent, var(--dsw-alias-overlay-scrim,
|
|
830
|
+
background: 'linear-gradient(transparent, var(--dsw-alias-overlay-scrim, color-mix(in srgb, var(--dsw-alias-bg-layer-0, black) 80%, transparent)))',
|
|
830
831
|
fontSize: '10px',
|
|
831
|
-
color: 'var(--dsw-alias-label-on-overlay
|
|
832
|
+
color: 'var(--dsw-alias-label-on-overlay, var(--dsw-alias-label-primary, white))',
|
|
832
833
|
whiteSpace: 'nowrap',
|
|
833
834
|
overflow: 'hidden',
|
|
834
835
|
textOverflow: 'ellipsis',
|
|
@@ -846,7 +847,7 @@ window.__ModuleLoader__.load({
|
|
|
846
847
|
style: {
|
|
847
848
|
position: 'fixed',
|
|
848
849
|
inset: 0,
|
|
849
|
-
background: 'var(--dsw-alias-overlay-backdrop,
|
|
850
|
+
background: 'var(--dsw-alias-overlay-backdrop, color-mix(in srgb, var(--dsw-alias-bg-layer-0, black) 70%, transparent))',
|
|
850
851
|
zIndex: 10000,
|
|
851
852
|
display: 'flex',
|
|
852
853
|
alignItems: 'center',
|
|
@@ -889,7 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
889
890
|
),
|
|
890
891
|
react.createElement('img', {
|
|
891
892
|
src: selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : ''),
|
|
892
|
-
style: { width: '100%', borderRadius: '8px', maxHeight: '340px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-1
|
|
893
|
+
style: { width: '100%', borderRadius: '8px', maxHeight: '340px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-layer-2, transparent))' },
|
|
893
894
|
}),
|
|
894
895
|
react.createElement(
|
|
895
896
|
'div',
|
|
@@ -1002,7 +1003,7 @@ window.__ModuleLoader__.load({
|
|
|
1002
1003
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1003
1004
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1004
1005
|
borderRadius: '12px',
|
|
1005
|
-
boxShadow: 'var(--dsw-alias-shadow-overlay,0 8px 32px
|
|
1006
|
+
boxShadow: 'var(--dsw-alias-shadow-overlay, 0 8px 32px color-mix(in srgb, var(--dsw-alias-bg-layer-0, black) 40%, transparent))',
|
|
1006
1007
|
zIndex: 9999,
|
|
1007
1008
|
padding: '16px',
|
|
1008
1009
|
overflowY: 'auto',
|
|
@@ -1276,16 +1277,87 @@ window.__ModuleLoader__.load({
|
|
|
1276
1277
|
}
|
|
1277
1278
|
}
|
|
1278
1279
|
|
|
1280
|
+
function tryParseJsonObject(text) {
|
|
1281
|
+
if (typeof text !== 'string') return null
|
|
1282
|
+
const start = text.indexOf('{')
|
|
1283
|
+
const end = text.lastIndexOf('}')
|
|
1284
|
+
if (start < 0 || end <= start) return null
|
|
1285
|
+
try {
|
|
1286
|
+
return JSON.parse(text.slice(start, end + 1))
|
|
1287
|
+
} catch (_) {
|
|
1288
|
+
return null
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function attachmentImageUrl(a) {
|
|
1293
|
+
if (!a) return ''
|
|
1294
|
+
if (a.url && typeof a.url === 'string') return a.url
|
|
1295
|
+
const query = 'id=' + encodeURIComponent(a.attachmentId ?? a.id ?? '')
|
|
1296
|
+
+ '&mt=' + encodeURIComponent(a.mediaType || 'image/png')
|
|
1297
|
+
+ '&b=' + encodeURIComponent(String(a.bytes ?? 0))
|
|
1298
|
+
+ '&w=' + encodeURIComponent(String(a.width ?? 0))
|
|
1299
|
+
+ '&h=' + encodeURIComponent(String(a.height ?? 0))
|
|
1300
|
+
return '/dsh-image-gen/image?' + query
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function readResponsiveImages(block) {
|
|
1304
|
+
if (!block) return []
|
|
1305
|
+
const text = String(block.output || block.text || '')
|
|
1306
|
+
const tagMatch = text.match(/<dsh-image-gen-responsive>([\s\S]*?)<\/dsh-image-gen-responsive>/)
|
|
1307
|
+
const bag = [
|
|
1308
|
+
block.images,
|
|
1309
|
+
block.result && block.result.images,
|
|
1310
|
+
block.data && block.data.images,
|
|
1311
|
+
tagMatch && tryParseJsonObject(tagMatch[1])?.images,
|
|
1312
|
+
tryParseJsonObject(text)?.images,
|
|
1313
|
+
tryParseJsonObject(block.result)?.images,
|
|
1314
|
+
]
|
|
1315
|
+
for (const list of bag) {
|
|
1316
|
+
if (!Array.isArray(list)) continue
|
|
1317
|
+
const images = list
|
|
1318
|
+
.map((item, index) => {
|
|
1319
|
+
if (!item || typeof item !== 'object') return null
|
|
1320
|
+
const attachment = item.attachment || null
|
|
1321
|
+
const url = attachmentImageUrl(attachment) || item.url || ''
|
|
1322
|
+
const rawDevice = String(item.device || '').toLowerCase()
|
|
1323
|
+
const device = ['mobile', 'tablet', 'desktop'].includes(rawDevice)
|
|
1324
|
+
? rawDevice
|
|
1325
|
+
: `device-${index + 1}`
|
|
1326
|
+
const label = item.label
|
|
1327
|
+
|| (device === 'mobile' ? 'Mobile'
|
|
1328
|
+
: device === 'tablet' ? 'Tablet'
|
|
1329
|
+
: device === 'desktop' ? 'Desktop'
|
|
1330
|
+
: `#${index + 1}`)
|
|
1331
|
+
return {
|
|
1332
|
+
device,
|
|
1333
|
+
label,
|
|
1334
|
+
aspect: item.aspect || '',
|
|
1335
|
+
width: item.width || (attachment && attachment.width) || 0,
|
|
1336
|
+
height: item.height || (attachment && attachment.height) || 0,
|
|
1337
|
+
url,
|
|
1338
|
+
attachment,
|
|
1339
|
+
path: item.path || '',
|
|
1340
|
+
seed: item.seed,
|
|
1341
|
+
}
|
|
1342
|
+
})
|
|
1343
|
+
.filter(Boolean)
|
|
1344
|
+
if (images.length >= 2) return images
|
|
1345
|
+
}
|
|
1346
|
+
return []
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1279
1349
|
function FalImageCard(props) {
|
|
1280
1350
|
const block = props.block
|
|
1281
1351
|
const running = !('kind' in (block || {}))
|
|
1282
1352
|
const failed = block && (block.isError || block.error !== undefined)
|
|
1283
1353
|
const parsed = readResult(block)
|
|
1354
|
+
const responsiveImages = !failed && !running ? readResponsiveImages(block) : []
|
|
1284
1355
|
const t = props.t || ((k) => k)
|
|
1285
1356
|
const [remixOpen, setRemixOpen] = react.useState(false)
|
|
1286
1357
|
const [remixStrength, setRemixStrength] = react.useState(0.45)
|
|
1287
1358
|
const [remixPrompt, setRemixPrompt] = react.useState('')
|
|
1288
1359
|
const [copiedRemix, setCopiedRemix] = react.useState(false)
|
|
1360
|
+
const [responsiveTab, setResponsiveTab] = react.useState(0)
|
|
1289
1361
|
|
|
1290
1362
|
react.useEffect(() => {
|
|
1291
1363
|
ensureCss()
|
|
@@ -1348,6 +1420,64 @@ window.__ModuleLoader__.load({
|
|
|
1348
1420
|
}
|
|
1349
1421
|
if (failed) {
|
|
1350
1422
|
body.push(react.createElement('div', { className: 'ig-err', key: 'e' }, parsed.text || t('card.unknownError')))
|
|
1423
|
+
} else if (responsiveImages.length) {
|
|
1424
|
+
const activeIdx = Math.min(responsiveTab, responsiveImages.length - 1)
|
|
1425
|
+
const active = responsiveImages[activeIdx]
|
|
1426
|
+
body.push(
|
|
1427
|
+
react.createElement(
|
|
1428
|
+
'div',
|
|
1429
|
+
{
|
|
1430
|
+
className: 'ig-tabs',
|
|
1431
|
+
role: 'tablist',
|
|
1432
|
+
'aria-label': t('card.responsiveTabs') || 'Responsive viewports',
|
|
1433
|
+
key: 'rtabs',
|
|
1434
|
+
},
|
|
1435
|
+
responsiveImages.map((img, idx) =>
|
|
1436
|
+
react.createElement(
|
|
1437
|
+
'button',
|
|
1438
|
+
{
|
|
1439
|
+
key: img.device,
|
|
1440
|
+
type: 'button',
|
|
1441
|
+
role: 'tab',
|
|
1442
|
+
'aria-selected': idx === activeIdx,
|
|
1443
|
+
className: idx === activeIdx ? 'ig-tab-btn ig-tab-btn-active' : 'ig-tab-btn',
|
|
1444
|
+
onClick: () => setResponsiveTab(idx),
|
|
1445
|
+
},
|
|
1446
|
+
(img.label || img.device) + (img.aspect ? ' · ' + img.aspect : '')
|
|
1447
|
+
)
|
|
1448
|
+
)
|
|
1449
|
+
),
|
|
1450
|
+
active && active.url
|
|
1451
|
+
? react.createElement('img', {
|
|
1452
|
+
key: 'ri',
|
|
1453
|
+
src: active.url,
|
|
1454
|
+
alt: prompt || (active.label + ' responsive mockup'),
|
|
1455
|
+
loading: 'lazy',
|
|
1456
|
+
style: {
|
|
1457
|
+
maxWidth: '100%',
|
|
1458
|
+
borderRadius: '8px',
|
|
1459
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1460
|
+
},
|
|
1461
|
+
})
|
|
1462
|
+
: null,
|
|
1463
|
+
active
|
|
1464
|
+
? react.createElement(
|
|
1465
|
+
'div',
|
|
1466
|
+
{
|
|
1467
|
+
key: 'rmeta',
|
|
1468
|
+
style: {
|
|
1469
|
+
marginTop: '6px',
|
|
1470
|
+
fontSize: '11px',
|
|
1471
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
[
|
|
1475
|
+
active.width && active.height ? `${active.width}×${active.height}` : null,
|
|
1476
|
+
active.path ? String(active.path) : null,
|
|
1477
|
+
].filter(Boolean).join(' · ')
|
|
1478
|
+
)
|
|
1479
|
+
: null
|
|
1480
|
+
)
|
|
1351
1481
|
} else if (!parsed.attachment && parsed.url) {
|
|
1352
1482
|
body.push(react.createElement('img', {
|
|
1353
1483
|
key: 'i',
|
|
@@ -1358,14 +1488,9 @@ window.__ModuleLoader__.load({
|
|
|
1358
1488
|
}))
|
|
1359
1489
|
} else if (parsed.attachment) {
|
|
1360
1490
|
const a = parsed.attachment
|
|
1361
|
-
const query = 'id=' + encodeURIComponent(a.attachmentId ?? a.id ?? '')
|
|
1362
|
-
+ '&mt=' + encodeURIComponent(a.mediaType || 'image/png')
|
|
1363
|
-
+ '&b=' + encodeURIComponent(String(a.bytes ?? 0))
|
|
1364
|
-
+ '&w=' + encodeURIComponent(String(a.width ?? 0))
|
|
1365
|
-
+ '&h=' + encodeURIComponent(String(a.height ?? 0))
|
|
1366
1491
|
body.push(react.createElement('img', {
|
|
1367
1492
|
key: 'i',
|
|
1368
|
-
src:
|
|
1493
|
+
src: attachmentImageUrl(a),
|
|
1369
1494
|
alt: prompt || 'generated image',
|
|
1370
1495
|
loading: 'lazy',
|
|
1371
1496
|
style: { maxWidth: '100%', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)' },
|
|
@@ -1373,7 +1498,7 @@ window.__ModuleLoader__.load({
|
|
|
1373
1498
|
}
|
|
1374
1499
|
|
|
1375
1500
|
const actions = []
|
|
1376
|
-
if (!running && !failed && (parsed.url || parsed.attachment)) {
|
|
1501
|
+
if (!running && !failed && (parsed.url || parsed.attachment || responsiveImages.length)) {
|
|
1377
1502
|
actions.push(
|
|
1378
1503
|
react.createElement('button', { key: 'remix', type: 'button', className: 'ig-btn', onClick: () => setRemixOpen(!remixOpen) }, '⚡ ' + (t('card.remix') || 'Remix')),
|
|
1379
1504
|
react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
|
|
@@ -1519,6 +1644,7 @@ window.__ModuleLoader__.load({
|
|
|
1519
1644
|
'card.copyPrompt': 'Copy Prompt',
|
|
1520
1645
|
'card.origPrompt': 'Original Prompt',
|
|
1521
1646
|
'card.remix': 'Remix',
|
|
1647
|
+
'card.responsiveTabs': 'Responsive viewports',
|
|
1522
1648
|
'remix.drawer_title': 'Variations & Style Remix Workbench',
|
|
1523
1649
|
'remix.creativity_label': 'Creativity / Denoising Strength',
|
|
1524
1650
|
'remix.prompt_placeholder': 'Describe stylistic adjustments, new background, or variations...',
|
|
@@ -1722,6 +1848,7 @@ window.__ModuleLoader__.load({
|
|
|
1722
1848
|
'card.copyPrompt': '复制提示词',
|
|
1723
1849
|
'card.origPrompt': '原始提示词',
|
|
1724
1850
|
'card.remix': '重混变体',
|
|
1851
|
+
'card.responsiveTabs': '响应式视口',
|
|
1725
1852
|
'remix.drawer_title': '变体与风格重混工作台',
|
|
1726
1853
|
'remix.creativity_label': '创意度 / 去噪强度',
|
|
1727
1854
|
'remix.prompt_placeholder': '描述风格调整、新背景或变体细节...',
|
|
@@ -1932,6 +2059,7 @@ window.__ModuleLoader__.load({
|
|
|
1932
2059
|
'remix_image',
|
|
1933
2060
|
'blend_images',
|
|
1934
2061
|
'generate_image_pack',
|
|
2062
|
+
'generate_responsive_mockups',
|
|
1935
2063
|
'remove_background',
|
|
1936
2064
|
'upscale_image',
|
|
1937
2065
|
'vectorize_image',
|
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
|
+
}
|