@goodandready/dsh-image-gen 0.10.23 → 0.10.25

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,20 @@
41
41
 
42
42
  ---
43
43
 
44
+ ## 🚀 Updates v0.10.25: Theme Color Parity, Responsive Mockups, Patterns & Spritesheets (#173, #177, #178, #238)
45
+ * **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
+ * **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.
47
+ * **Seamless Pattern Generator (`generate_seamless_pattern`, #178)**: Automated tileable background texture generation with periodic boundary condition checks and CSS tiling preview.
48
+ * **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.
49
+ * **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.
50
+
51
+ ## 🚀 Updates v0.10.24: Quality Block — Publication Boundary, Settings Status, Theme Colors & Module Split (#235–#242)
52
+ * **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.
53
+ * **Settings Status Fallback**: Corrected provider/config status handling in the Settings Card so partial/unknown states no longer show a false “unavailable”.
54
+ * **Design-System Alignment**: Renamed residual `fal-` CSS class prefixes to the `ig-` system and bound colors to DSH theme variables via `var(--dsw-alias-*, <fallback>)`.
55
+ * **Sensitive File Hardening**: History, cache meta/data, and spend-meter writes now enforce `0600` permissions after write.
56
+ * **Dead Code Removal & Module Split**: Removed unused `listCuratedStyles`; split oversized server modules — `providers.js` utils extracted to `provider-utils.js`, `processing.js` into `processing-basic.js` + `processing-advanced.js`. Client file split is tracked separately (#244).
57
+
44
58
  ## 🚀 Updates v0.10.23: In-App Auto-Updater, Settings Synchronization, Active Probes & Language Purity (#232)
45
59
  * **One-Click In-App Auto-Updater**: Direct 1-click update support inside the plugin Settings Card (`UpdaterSection`) backed by `/api/dsh-image-gen/update`. Fetches the npm registry for release manifests, verifies semver differences, and securely triggers `dsh plugin add @goodandready/dsh-image-gen@latest` restricted to loopback and private LAN connections.
46
60
  * **1:1 Settings Synchronization**: Fully synchronized host `Config` schema in `lib/index.js` with client UI fields (`lib/client.js`), exposing `autoEnhancePrompt` and `defaultStylePreset` under the `✨ Enhancer` tab.
@@ -110,6 +124,7 @@
110
124
  * **`vectorize_image`**: Convert raster graphics to clean scalable SVG vectors with palette quantization.
111
125
  * **`blend_images`**: Multi-reference composition mixing.
112
126
  * **`generate_image_pack`**: Simultaneous multi-aspect ratio rendering with graceful partial recovery.
127
+ * **`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.
113
128
  * **`compare_images`**: Pixel-level visual difference ratio comparison.
114
129
  * **`inspect_image_quality`**: Automated visual audit, Laplacian sharpness scoring, and defect detection.
115
130
  * **`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,20 @@
34
34
  </div>
35
35
 
36
36
  ---
37
+ ## 🚀 Обновления v0.10.25: Чистота темы DSH, адаптивные мокапы, бесшовные паттерны и спрайтшиты (#173, #177, #178, #238)
38
+ * **100% адаптация под переменные темы DSH (#238)**: Полностью устранены все захардкоженные литералы `rgba(...)` и hex-цветов в клиентской части (`src/client/10-css.js`, `src/client/80-gallery.js`). Полупрозрачные статусные фоны бейджей, алертов и модальных оверлеев переведены на динамическое смешивание токенов темы через `color-mix(...)`.
39
+ * **Генератор адаптивных интерфейсных мокапов (`generate_responsive_mockups`, #173)**: Генерация согласованного набора экранов Mobile (9:16), Tablet (3:4) и Desktop (16:9) по единому промпту с параллельным выполнением через `asyncPool` (concurrency 3) и интерактивным таб-интерфейсом в карточке чата.
40
+ * **Генератор бесшовных текстур и паттернов (`generate_seamless_pattern`, #178)**: Создание тайлящихся фоновых текстур со встроенным контролем швов и готовым CSS-предпросмотром.
41
+ * **Генератор 2D-спрайтшитов (`generate_spritesheet`, #177)**: Формирование горизонтальных раскадровок анимации персонажей и UI с автогенерацией CSS `@keyframes` и SVG-превью полосы кадров.
42
+ * **Модульная сборка клиента (#244)**: Клиентская часть разнесена на 15 структурированных модулей в `src/client/*` со скриптом сборки `scripts/build-client.mjs`. Preflight-проверка `FAIL=0`, 179 из 179 тестов успешно пройдены.
43
+
44
+ ## 🚀 Обновления v0.10.24: Блок качества — публикационная граница, статусы настроек, цвета темы и разбиение модулей (#235–#242)
45
+ * **Публикационная граница**: добавлен `.gitattributes` c `export-ignore` для `AGENTS.md`, `index.md`, `docs/` и `.gitea/` — архивы GitHub source и `git archive` больше не содержат внутренние файлы workflow.
46
+ * **Статусы настроек**: исправлен fallback статуса провайдера/конфигурации в карточке настроек — промежуточные/неизвестные состояния больше не показываются ложным «unavailable».
47
+ * **Выравнивание с дизайн-системой**: остаточные CSS-классы `fal-` переименованы в префикс `ig-`; цвета привязаны к переменным темы DSH через `var(--dsw-alias-*, <fallback>)`.
48
+ * **Защита чувствительных файлов**: запись history, cache meta/data и spend-meter после записи получает права `0600`.
49
+ * **Удаление мёртвого кода и разбиение модулей**: удалён неиспользуемый `listCuratedStyles`; укрупнённые server-модули разнесены — утилиты `providers.js` → `provider-utils.js`, `processing.js` → `processing-basic.js` + `processing-advanced.js`. Разбиение client отложено в #244.
50
+
37
51
  ## 🚀 Обновления v0.10.23: Встроенное автообновление, синхронизация настроек, диагностические пробы и чистота языка (#232)
38
52
  * **Встроенное обновление в 1 клик**: Прямая проверка и обновление плагина прямо из карточки настроек (`UpdaterSection`) через эндпоинт `/api/dsh-image-gen/update`. Опрашивает реестр npm, сравнивает semver-версии и безопасно запускает установку через `dsh plugin add` с защитой по loopback и доверенной локальной сети (RFC 1918).
39
53
  * **Синхронизация настроек 1:1**: Полное выравнивание схемы хоста `Config` в `lib/index.js` и полей UI `FIELDS` в `lib/client.js` — во вкладку «✨ Промпт и стили» добавлены тумблеры `autoEnhancePrompt` и выпадающий список `defaultStylePreset`.
@@ -150,6 +164,7 @@ graph LR
150
164
  | **`vectorize_image`** | Векторизация растра в чистый SVG с квантованием палитры | `image`, `color_mode`, `paletteSize` |
151
165
  | **`blend_images`** | Смешивание нескольких изображений в единую композицию | `images`, `weights`, `prompt` |
152
166
  | **`generate_image_pack`** | Пакетный рендеринг форматов 1:1, 16:9, 9:16 с сохранением частичных результатов | `prompt`, `aspect_ratios` |
167
+ | **`generate_responsive_mockups`** | Пакетная адаптивная генерация UI-mockup под Mobile 9:16, Tablet 3:4 и Desktop 16:9 с единой палитрой/стилем; табы в toolview (#173) | `prompt`, `devices`, `style_preset`, `palette_colors`, `seed` |
153
168
  | **`compare_images`** | Сравнение двух картинок (доля различий пикселей) | `image_a`, `image_b` |
154
169
  | **`inspect_image_quality`** | Аудит качества, расчет резкости и проверка дефектов | `image`, `expected_elements` |
155
170
 
package/README.zh.md CHANGED
@@ -47,6 +47,20 @@
47
47
 
48
48
  ---
49
49
 
50
+ ## 🚀 更新 v0.10.25: DSH 主题颜色适配、响应式多端原型、无缝纹理与精灵图导出 (#173, #177, #178, #238)
51
+ * **100% 主题变量原生适配 (#238)**: 彻底移除客户端代码中的所有写死 `rgba(...)` 和 Hex 颜色(`src/client/10-css.js`、`src/client/80-gallery.js`),徽章、提示栏和画廊遮罩全部通过 `color-mix(...)` 配合 `--dsw-alias-*` 主题变量动态生成,完美兼容深色与浅色主题。
52
+ * **多端响应式原型生成器 (`generate_responsive_mockups`, #173)**: 输入单个产品提示词,并发生成风格统一的移动端 (9:16)、平板端 (3:4) 与桌面端 (16:9) UI 原型,支持聊天卡片中的分栏切换预览。
53
+ * **无缝平铺纹理生成器 (`generate_seamless_pattern`, #178)**: 自动生成四方连续平铺背景图,内置接缝平滑度校验与 CSS 平铺预览参数。
54
+ * **2D 逐帧精灵图生成器 (`generate_spritesheet`, #177)**: 自动化横向动画序列帧渲染,内置 CSS `@keyframes` 动画代码与 SVG 多帧预览条。
55
+ * **客户端源码模块化重构 (#244)**: 将原本庞大的单文件拆分为 `src/client/*` 目录下 15 个独立职责模块,配套 `scripts/build-client.mjs` 构建机制。Preflight 检查 `FAIL=0`,179/179 项自动化测试全绿。
56
+
57
+ ## 🚀 更新 v0.10.24: 质量加固 — 发布边界、设置状态、主题颜色与模块拆分 (#235–#242)
58
+ * **发布边界**: 新增 `.gitattributes`,对 `AGENTS.md`、`index.md`、`docs/`、`.gitea/` 设置 `export-ignore`,GitHub source 归档与 `git archive` 不再包含内部工作流文件。
59
+ * **设置状态回退**: 修正设置卡片中的提供方/配置状态处理,部分或未知状态不再误显示为 “unavailable”。
60
+ * **对齐设计系统**: 将残留的 `fal-` CSS 类前缀改为 `ig-`,并以 `var(--dsw-alias-*, <fallback>)` 绑定 DSH 主题变量。
61
+ * **敏感文件加固**: 历史记录、缓存 meta/data 与花费计量写入后强制 `0600` 权限。
62
+ * **死代码清理与模块拆分**: 删除未使用的 `listCuratedStyles`;拆分过大的服务端模块 — `providers.js` 工具函数抽到 `provider-utils.js`,`processing.js` 拆为 `processing-basic.js` + `processing-advanced.js`。客户端拆分见 #244。
63
+
50
64
  ## 🚀 更新 v0.10.23: 应用内一键更新、设置同步、真实探针与语言规范 (#232)
51
65
  * **应用内一键自动更新**: 设置卡片内新增「插件更新」模块(`UpdaterSection`),配合 `/api/dsh-image-gen/update` 接口。自动查询 npm 官方注册表,对比语义化版本号,并在本地回环或私有局域网安全授权下执行 `dsh plugin add @goodandready/dsh-image-gen@latest`。
52
66
  * **1:1 核心配置与界面设置同步**: 严格对齐 `lib/index.js` 宿主 `Config` 与 `lib/client.js` 界面字段,在「✨ 提示词增强」标签页下完整呈现 `autoEnhancePrompt`(智能自动增强提示词)与 `defaultStylePreset`(默认风格预设)。
@@ -64,6 +78,7 @@
64
78
  * **智能比例无损画布裁切器 (`smart_crop_image`)**: 快速裁切适配各大社交与设计标准比例 (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`),支持自动对焦、三分法构图 (`rule_of_thirds`) 与黑边留白 (`letterbox`)。
65
79
  * **品牌资源套件导出器 (`export_asset_pack`)**: 一键生成生产级网站与 PWA 资源套件:SVG Favicon、PWA 推荐图标套件 (`icon-192.svg`, `icon-512.svg`)、`manifest.webmanifest`、1200×630 社交分享预览卡片 (`og-card.svg`) 与索引 `catalog.json`。
66
80
  * **覆盖 12 大视觉工具的交互 Toolview**: 在对话中直接提供重混、放大、裁切与栅格拼图的可视化操作。
81
+ * **响应式多端 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`,对话内以三标签页预览。
67
82
  * **严谨的中英双语界面**: 完整的 `zh` 和 `en` 词典覆盖。
68
83
 
69
84
  ## 🚀 更新 v0.10.19: 侧边栏内置图库、连接诊断与风格引擎 (#224)
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',
@@ -22,7 +23,7 @@ window.__ModuleLoader__.load({
22
23
  try {
23
24
  runtime = require(id)
24
25
  if (runtime && typeof runtime.createSnapshotStore === 'function') break
25
- } catch (_) {}
26
+ } catch (_) { /* module not available in this build; try next */ }
26
27
  }
27
28
 
28
29
  const NS = '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:rgba(16,185,129,0.08)}
79
- .ig-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
80
- .ig-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
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}
@@ -99,23 +100,23 @@ window.__ModuleLoader__.load({
99
100
  .ig-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
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
- .ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
103
- .ig-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
103
+ .ig-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
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:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px}
110
- .ig-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
111
- .ig-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}
112
- .ig-banner-warning{padding:12px 16px;border-radius:8px;background:rgba(245,158,11,0.12);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}
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
- .fal-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
- .fal-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:6px}
118
- .fal-err{padding:10px;border-radius:6px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:12px}
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}
118
+ .ig-meta{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:6px}
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
  }
@@ -328,7 +329,7 @@ window.__ModuleLoader__.load({
328
329
  }
329
330
  }
330
331
  shell() {
331
- const snapshot = (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()) || { status: 'ready', writable: true }
332
+ const snapshot = (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()) || { status: 'unavailable', writable: false }
332
333
  const plan = this.plan()
333
334
  return {
334
335
  available: snapshot.status === 'ready',
@@ -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, rgba(0,0,0,0.8))',
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: '#fff',
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: 'rgba(0,0,0,0.7)',
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: '#0a0a0c' },
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: '0 8px 32px rgba(0,0,0,0.4)',
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',
@@ -1038,7 +1039,7 @@ window.__ModuleLoader__.load({
1038
1039
 
1039
1040
  const state = (typeof props.useFalSettingsCard === 'function'
1040
1041
  ? props.useFalSettingsCard((s) => s)
1041
- : null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
1042
+ : null) || props.state || { available: false, writable: false, provider: { text: 'fal' } }
1042
1043
 
1043
1044
  const disabled = !state.writable
1044
1045
  const currentProvider = (state.provider && state.provider.text) || 'fal'
@@ -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()
@@ -1299,7 +1371,7 @@ window.__ModuleLoader__.load({
1299
1371
  args = JSON.parse(raw)
1300
1372
  prompt = args.prompt || ''
1301
1373
  }
1302
- } catch (_) {}
1374
+ } catch (_) { /* malformed tool args; use empty prompt */ }
1303
1375
 
1304
1376
  const [copied, setCopied] = react.useState(false)
1305
1377
 
@@ -1310,7 +1382,7 @@ window.__ModuleLoader__.load({
1310
1382
  const binding = current && sessions.binding(current)
1311
1383
  const session = binding && binding.session
1312
1384
  if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
1313
- } catch (_) {}
1385
+ } catch (_) { /* session prompt delivery failed; non-fatal */ }
1314
1386
  }
1315
1387
 
1316
1388
  const onCopyPrompt = () => {
@@ -1344,10 +1416,68 @@ window.__ModuleLoader__.load({
1344
1416
 
1345
1417
  const body = []
1346
1418
  if (prompt) {
1347
- body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
1419
+ body.push(react.createElement('div', { className: 'ig-prompt', key: 'p' }, prompt))
1348
1420
  }
1349
1421
  if (failed) {
1350
- body.push(react.createElement('div', { className: 'fal-err', key: 'e' }, parsed.text || t('card.unknownError')))
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: '/dsh-image-gen/image?' + query,
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',
@@ -2008,7 +2136,7 @@ window.__ModuleLoader__.load({
2008
2136
  console.warn('[dsh-image-gen] native sidebar registration failed', e)
2009
2137
  }
2010
2138
  })
2011
- } catch (e) {}
2139
+ } catch (e) { /* native sidebar unavailable */ }
2012
2140
 
2013
2141
  try {
2014
2142
  ctx.inject(['betterSidebar'], (sctx) => {
@@ -2026,7 +2154,7 @@ window.__ModuleLoader__.load({
2026
2154
  console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
2027
2155
  }
2028
2156
  })
2029
- } catch (e) {}
2157
+ } catch (e) { /* betterSidebar unavailable */ }
2030
2158
  }
2031
2159
 
2032
2160
  // Conversation header utilities chip (quick-access gallery button in chat header)
package/lib/cost-meter.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { homedir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
7
+ import { enforceSecurePermissions } from './security.js'
7
8
 
8
9
  /**
9
10
  * Standard pricing table per provider and resolution (USD per generation).
@@ -87,7 +88,7 @@ export function loadDailySpend() {
87
88
  if (data && data.date === today) {
88
89
  return data
89
90
  }
90
- } catch {}
91
+ } catch { /* spend file unreadable; return zero */ }
91
92
  return { date: today, totalSpendUsd: 0, generations: 0 }
92
93
  }
93
94
 
@@ -107,6 +108,7 @@ export function recordSpend(usdAmount, { ctx, meta = {} } = {}) {
107
108
  try {
108
109
  mkdirSync(join(filePath, '..'), { recursive: true })
109
110
  writeFileSync(filePath, JSON.stringify(current, null, 2), 'utf8')
111
+ enforceSecurePermissions(filePath)
110
112
  } catch (err) {
111
113
  // Non-fatal write failure
112
114
  }
@@ -122,7 +124,7 @@ export function recordSpend(usdAmount, { ctx, meta = {} } = {}) {
122
124
  meta,
123
125
  })
124
126
  }
125
- } catch {}
127
+ } catch { /* cost-meter service unavailable */ }
126
128
 
127
129
  return current
128
130
  }
@@ -4,6 +4,7 @@
4
4
  import { homedir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync, unlinkSync, readdirSync } from 'node:fs'
7
+ import { enforceSecurePermissions } from './security.js'
7
8
 
8
9
  const DEFAULT_CACHE_MAX_BYTES = 500 * 1024 * 1024 // 500 MB
9
10
 
@@ -107,6 +108,8 @@ export function setCachedGeneration(hash, { bytes, mediaType = 'image/png', widt
107
108
  writeFileSync(metaPath, JSON.stringify(cacheMeta, null, 2), 'utf8')
108
109
  const binBuf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
109
110
  writeFileSync(dataPath, binBuf)
111
+ enforceSecurePermissions(metaPath)
112
+ enforceSecurePermissions(dataPath)
110
113
 
111
114
  setToL1(hash, {
112
115
  bytes: binBuf,
@@ -151,7 +154,7 @@ export function pruneCacheToLimit(maxBytes = DEFAULT_CACHE_MAX_BYTES) {
151
154
  size,
152
155
  lastAccessed: meta.lastAccessed || meta.createdAt || 0,
153
156
  })
154
- } catch {}
157
+ } catch { /* entry meta unreadable; skip */ }
155
158
  }
156
159
 
157
160
  if (totalSize <= maxBytes) return
@@ -165,7 +168,7 @@ export function pruneCacheToLimit(maxBytes = DEFAULT_CACHE_MAX_BYTES) {
165
168
  if (existsSync(entry.metaPath)) unlinkSync(entry.metaPath)
166
169
  if (existsSync(entry.dataPath)) unlinkSync(entry.dataPath)
167
170
  totalSize -= entry.size
168
- } catch {}
171
+ } catch { /* entry unlink failed; skip */ }
169
172
  }
170
- } catch {}
173
+ } catch { /* cache dir unreadable; prune skipped */ }
171
174
  }
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@
15
15
  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
+ import { enforceSecurePermissions } from './security.js'
18
19
  import { existsSync, unlinkSync } from 'node:fs'
19
20
  import os from 'node:os'
20
21
  import path from 'node:path'
@@ -122,7 +123,7 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
122
123
  })
123
124
  }
124
125
 
125
- export const name = 'dsh-image-gen'
126
+ export const name = '@goodandready/dsh-image-gen'
126
127
 
127
128
  /** Settings namespace the Web card edits. */
128
129
  const NS = 'dsh-image-gen'
@@ -472,6 +473,7 @@ export async function writeHistory(entries) {
472
473
  try {
473
474
  await mkdir(path.dirname(historyFile()), { recursive: true })
474
475
  await writeFile(historyFile(), JSON.stringify(entries, null, 2))
476
+ enforceSecurePermissions(historyFile())
475
477
  } catch (e) { /* history write failure is non-fatal */ }
476
478
  }
477
479
 
package/lib/loop-guard.js CHANGED
@@ -55,6 +55,7 @@ export function trackAndAssertLoopGuard(sessionId, {
55
55
  /**
56
56
  * Resets loop guard counter for a session.
57
57
  */
58
+ /** @internal — test helper; not part of the plugin runtime API */
58
59
  export function resetLoopGuard(sessionId) {
59
60
  const sid = String(sessionId || 'default_session')
60
61
  sessionStates.delete(sid)
@@ -63,6 +64,7 @@ export function resetLoopGuard(sessionId) {
63
64
  /**
64
65
  * Gets current state for debugging/testing.
65
66
  */
67
+ /** @internal — test helper; not part of the plugin runtime API */
66
68
  export function getLoopGuardState(sessionId) {
67
69
  const sid = String(sessionId || 'default_session')
68
70
  return sessionStates.get(sid) || { count: 0, lastPrompt: '', lastTimestamp: 0 }