@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 +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +15 -0
- package/lib/client.js +161 -33
- package/lib/cost-meter.js +4 -2
- package/lib/generation-cache.js +6 -3
- package/lib/index.js +3 -1
- package/lib/loop-guard.js +2 -0
- package/lib/pattern-helpers.js +87 -0
- package/lib/prompt-polisher.js +0 -7
- package/lib/provider-utils.js +302 -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 +95 -847
- package/lib/register-tools.js +11 -1
- package/lib/responsive-helpers.js +94 -0
- package/lib/spritesheet-helpers.js +65 -0
- package/lib/tools/editing.js +2 -0
- package/lib/tools/pattern.js +125 -0
- package/lib/tools/{processing.js → processing-advanced.js} +11 -270
- package/lib/tools/processing-basic.js +327 -0
- package/lib/tools/responsive.js +267 -0
- package/lib/tools/sketch.js +167 -0
- package/lib/tools/spritesheet.js +151 -0
- package/package.json +3 -2
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:
|
|
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}
|
|
@@ -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:
|
|
103
|
-
.ig-btn-danger:hover:not(:disabled){background:
|
|
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:
|
|
110
|
-
.ig-alert-bad{padding:10px 14px;border-radius:8px;background:
|
|
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
|
-
.
|
|
118
|
-
.
|
|
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: '
|
|
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,
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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:
|
|
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: '
|
|
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: '
|
|
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',
|
|
@@ -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
|
}
|
package/lib/generation-cache.js
CHANGED
|
@@ -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 }
|