@goodandready/dsh-image-gen 0.10.23 → 0.10.24
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 +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +21 -21
- package/lib/cost-meter.js +4 -2
- package/lib/generation-cache.js +6 -3
- package/lib/index.js +2 -0
- package/lib/loop-guard.js +2 -0
- package/lib/prompt-polisher.js +0 -7
- package/lib/provider-utils.js +302 -0
- package/lib/providers.js +49 -294
- package/lib/register-tools.js +3 -1
- package/lib/tools/{processing.js → processing-advanced.js} +4 -269
- package/lib/tools/processing-basic.js +327 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -41,6 +41,13 @@
|
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## 🚀 Updates v0.10.24: Quality Block — Publication Boundary, Settings Status, Theme Colors & Module Split (#235–#242)
|
|
45
|
+
* **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
|
+
* **Settings Status Fallback**: Corrected provider/config status handling in the Settings Card so partial/unknown states no longer show a false “unavailable”.
|
|
47
|
+
* **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>)`.
|
|
48
|
+
* **Sensitive File Hardening**: History, cache meta/data, and spend-meter writes now enforce `0600` permissions after write.
|
|
49
|
+
* **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).
|
|
50
|
+
|
|
44
51
|
## 🚀 Updates v0.10.23: In-App Auto-Updater, Settings Synchronization, Active Probes & Language Purity (#232)
|
|
45
52
|
* **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
53
|
* **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.
|
package/README.ru.md
CHANGED
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
---
|
|
37
|
+
## 🚀 Обновления v0.10.24: Блок качества — публикационная граница, статусы настроек, цвета темы и разбиение модулей (#235–#242)
|
|
38
|
+
* **Публикационная граница**: добавлен `.gitattributes` c `export-ignore` для `AGENTS.md`, `index.md`, `docs/` и `.gitea/` — архивы GitHub source и `git archive` больше не содержат внутренние файлы workflow.
|
|
39
|
+
* **Статусы настроек**: исправлен fallback статуса провайдера/конфигурации в карточке настроек — промежуточные/неизвестные состояния больше не показываются ложным «unavailable».
|
|
40
|
+
* **Выравнивание с дизайн-системой**: остаточные CSS-классы `fal-` переименованы в префикс `ig-`; цвета привязаны к переменным темы DSH через `var(--dsw-alias-*, <fallback>)`.
|
|
41
|
+
* **Защита чувствительных файлов**: запись history, cache meta/data и spend-meter после записи получает права `0600`.
|
|
42
|
+
* **Удаление мёртвого кода и разбиение модулей**: удалён неиспользуемый `listCuratedStyles`; укрупнённые server-модули разнесены — утилиты `providers.js` → `provider-utils.js`, `processing.js` → `processing-basic.js` + `processing-advanced.js`. Разбиение client отложено в #244.
|
|
43
|
+
|
|
37
44
|
## 🚀 Обновления v0.10.23: Встроенное автообновление, синхронизация настроек, диагностические пробы и чистота языка (#232)
|
|
38
45
|
* **Встроенное обновление в 1 клик**: Прямая проверка и обновление плагина прямо из карточки настроек (`UpdaterSection`) через эндпоинт `/api/dsh-image-gen/update`. Опрашивает реестр npm, сравнивает semver-версии и безопасно запускает установку через `dsh plugin add` с защитой по loopback и доверенной локальной сети (RFC 1918).
|
|
39
46
|
* **Синхронизация настроек 1:1**: Полное выравнивание схемы хоста `Config` в `lib/index.js` и полей UI `FIELDS` в `lib/client.js` — во вкладку «✨ Промпт и стили» добавлены тумблеры `autoEnhancePrompt` и выпадающий список `defaultStylePreset`.
|
package/README.zh.md
CHANGED
|
@@ -47,6 +47,13 @@
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 🚀 更新 v0.10.24: 质量加固 — 发布边界、设置状态、主题颜色与模块拆分 (#235–#242)
|
|
51
|
+
* **发布边界**: 新增 `.gitattributes`,对 `AGENTS.md`、`index.md`、`docs/`、`.gitea/` 设置 `export-ignore`,GitHub source 归档与 `git archive` 不再包含内部工作流文件。
|
|
52
|
+
* **设置状态回退**: 修正设置卡片中的提供方/配置状态处理,部分或未知状态不再误显示为 “unavailable”。
|
|
53
|
+
* **对齐设计系统**: 将残留的 `fal-` CSS 类前缀改为 `ig-`,并以 `var(--dsw-alias-*, <fallback>)` 绑定 DSH 主题变量。
|
|
54
|
+
* **敏感文件加固**: 历史记录、缓存 meta/data 与花费计量写入后强制 `0600` 权限。
|
|
55
|
+
* **死代码清理与模块拆分**: 删除未使用的 `listCuratedStyles`;拆分过大的服务端模块 — `providers.js` 工具函数抽到 `provider-utils.js`,`processing.js` 拆为 `processing-basic.js` + `processing-advanced.js`。客户端拆分见 #244。
|
|
56
|
+
|
|
50
57
|
## 🚀 更新 v0.10.23: 应用内一键更新、设置同步、真实探针与语言规范 (#232)
|
|
51
58
|
* **应用内一键自动更新**: 设置卡片内新增「插件更新」模块(`UpdaterSection`),配合 `/api/dsh-image-gen/update` 接口。自动查询 npm 官方注册表,对比语义化版本号,并在本地回环或私有局域网安全授权下执行 `dsh plugin add @goodandready/dsh-image-gen@latest`。
|
|
52
59
|
* **1:1 核心配置与界面设置同步**: 严格对齐 `lib/index.js` 宿主 `Config` 与 `lib/client.js` 界面字段,在「✨ 提示词增强」标签页下完整呈现 `autoEnhancePrompt`(智能自动增强提示词)与 `defaultStylePreset`(默认风格预设)。
|
package/lib/client.js
CHANGED
|
@@ -22,7 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
try {
|
|
23
23
|
runtime = require(id)
|
|
24
24
|
if (runtime && typeof runtime.createSnapshotStore === 'function') break
|
|
25
|
-
} catch (_) {}
|
|
25
|
+
} catch (_) { /* module not available in this build; try next */ }
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const NS = 'dsh-image-gen'
|
|
@@ -99,23 +99,23 @@ window.__ModuleLoader__.load({
|
|
|
99
99
|
.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
100
|
.ig-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
101
101
|
.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:rgba(239,68,68,0.12);border-color:
|
|
102
|
+
.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,rgba(239,68,68,0.12));border-color:var(--dsw-alias-state-error-primary)}
|
|
104
104
|
.ig-btn-disabled{opacity:0.5;cursor:not-allowed}
|
|
105
105
|
|
|
106
106
|
.ig-reset{font-size:11px;color:var(--dsw-alias-state-brand-primary);background:none;border:none;cursor:pointer;padding:0;text-decoration:underline}
|
|
107
107
|
.ig-reset:hover{opacity:0.8}
|
|
108
108
|
|
|
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}
|
|
109
|
+
.ig-alert-ok{padding:10px 14px;border-radius:8px;background:var(--dsw-alias-state-success-bg,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:var(--dsw-alias-state-error-bg,rgba(239,68,68,0.1));color:var(--dsw-alias-state-error-primary);font-size:13px}
|
|
111
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
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}
|
|
113
113
|
|
|
114
114
|
/* Backward-compatibility alias styles for toolview */
|
|
115
115
|
.fal_head{font-size:13px;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-label-primary)}
|
|
116
|
-
.
|
|
117
|
-
.
|
|
118
|
-
.
|
|
116
|
+
.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
|
+
.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,rgba(239,68,68,0.1));color:var(--dsw-alias-state-error-primary);font-size:12px}
|
|
119
119
|
`
|
|
120
120
|
document.head.appendChild(style)
|
|
121
121
|
}
|
|
@@ -328,7 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
shell() {
|
|
331
|
-
const snapshot = (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()) || { status: '
|
|
331
|
+
const snapshot = (this.scope && this.scope.getSnapshot && this.scope.getSnapshot()) || { status: 'unavailable', writable: false }
|
|
332
332
|
const plan = this.plan()
|
|
333
333
|
return {
|
|
334
334
|
available: snapshot.status === 'ready',
|
|
@@ -826,9 +826,9 @@ window.__ModuleLoader__.load({
|
|
|
826
826
|
left: 0,
|
|
827
827
|
right: 0,
|
|
828
828
|
padding: '4px 6px',
|
|
829
|
-
background: 'linear-gradient(transparent, rgba(0,0,0,0.8))',
|
|
829
|
+
background: 'linear-gradient(transparent, var(--dsw-alias-overlay-scrim,rgba(0,0,0,0.8)))',
|
|
830
830
|
fontSize: '10px',
|
|
831
|
-
color: '
|
|
831
|
+
color: 'var(--dsw-alias-label-on-overlay,#fff)',
|
|
832
832
|
whiteSpace: 'nowrap',
|
|
833
833
|
overflow: 'hidden',
|
|
834
834
|
textOverflow: 'ellipsis',
|
|
@@ -846,7 +846,7 @@ window.__ModuleLoader__.load({
|
|
|
846
846
|
style: {
|
|
847
847
|
position: 'fixed',
|
|
848
848
|
inset: 0,
|
|
849
|
-
background: 'rgba(0,0,0,0.7)',
|
|
849
|
+
background: 'var(--dsw-alias-overlay-backdrop,rgba(0,0,0,0.7))',
|
|
850
850
|
zIndex: 10000,
|
|
851
851
|
display: 'flex',
|
|
852
852
|
alignItems: 'center',
|
|
@@ -889,7 +889,7 @@ window.__ModuleLoader__.load({
|
|
|
889
889
|
),
|
|
890
890
|
react.createElement('img', {
|
|
891
891
|
src: selected.thumbnailUrl || (selected.attachmentId ? '/dsh-image-gen/image?id=' + encodeURIComponent(selected.attachmentId) : ''),
|
|
892
|
-
style: { width: '100%', borderRadius: '8px', maxHeight: '340px', objectFit: 'contain', background: '
|
|
892
|
+
style: { width: '100%', borderRadius: '8px', maxHeight: '340px', objectFit: 'contain', background: 'var(--dsw-alias-bg-layer-1,#0a0a0c)' },
|
|
893
893
|
}),
|
|
894
894
|
react.createElement(
|
|
895
895
|
'div',
|
|
@@ -1002,7 +1002,7 @@ window.__ModuleLoader__.load({
|
|
|
1002
1002
|
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1003
1003
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1004
1004
|
borderRadius: '12px',
|
|
1005
|
-
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
|
1005
|
+
boxShadow: 'var(--dsw-alias-shadow-overlay,0 8px 32px rgba(0,0,0,0.4))',
|
|
1006
1006
|
zIndex: 9999,
|
|
1007
1007
|
padding: '16px',
|
|
1008
1008
|
overflowY: 'auto',
|
|
@@ -1038,7 +1038,7 @@ window.__ModuleLoader__.load({
|
|
|
1038
1038
|
|
|
1039
1039
|
const state = (typeof props.useFalSettingsCard === 'function'
|
|
1040
1040
|
? props.useFalSettingsCard((s) => s)
|
|
1041
|
-
: null) || props.state || { available:
|
|
1041
|
+
: null) || props.state || { available: false, writable: false, provider: { text: 'fal' } }
|
|
1042
1042
|
|
|
1043
1043
|
const disabled = !state.writable
|
|
1044
1044
|
const currentProvider = (state.provider && state.provider.text) || 'fal'
|
|
@@ -1299,7 +1299,7 @@ window.__ModuleLoader__.load({
|
|
|
1299
1299
|
args = JSON.parse(raw)
|
|
1300
1300
|
prompt = args.prompt || ''
|
|
1301
1301
|
}
|
|
1302
|
-
} catch (_) {}
|
|
1302
|
+
} catch (_) { /* malformed tool args; use empty prompt */ }
|
|
1303
1303
|
|
|
1304
1304
|
const [copied, setCopied] = react.useState(false)
|
|
1305
1305
|
|
|
@@ -1310,7 +1310,7 @@ window.__ModuleLoader__.load({
|
|
|
1310
1310
|
const binding = current && sessions.binding(current)
|
|
1311
1311
|
const session = binding && binding.session
|
|
1312
1312
|
if (session && session.prompt) await session.prompt([{ type: 'text', text }], 'queue')
|
|
1313
|
-
} catch (_) {}
|
|
1313
|
+
} catch (_) { /* session prompt delivery failed; non-fatal */ }
|
|
1314
1314
|
}
|
|
1315
1315
|
|
|
1316
1316
|
const onCopyPrompt = () => {
|
|
@@ -1344,10 +1344,10 @@ window.__ModuleLoader__.load({
|
|
|
1344
1344
|
|
|
1345
1345
|
const body = []
|
|
1346
1346
|
if (prompt) {
|
|
1347
|
-
body.push(react.createElement('div', { className: '
|
|
1347
|
+
body.push(react.createElement('div', { className: 'ig-prompt', key: 'p' }, prompt))
|
|
1348
1348
|
}
|
|
1349
1349
|
if (failed) {
|
|
1350
|
-
body.push(react.createElement('div', { className: '
|
|
1350
|
+
body.push(react.createElement('div', { className: 'ig-err', key: 'e' }, parsed.text || t('card.unknownError')))
|
|
1351
1351
|
} else if (!parsed.attachment && parsed.url) {
|
|
1352
1352
|
body.push(react.createElement('img', {
|
|
1353
1353
|
key: 'i',
|
|
@@ -2008,7 +2008,7 @@ window.__ModuleLoader__.load({
|
|
|
2008
2008
|
console.warn('[dsh-image-gen] native sidebar registration failed', e)
|
|
2009
2009
|
}
|
|
2010
2010
|
})
|
|
2011
|
-
} catch (e) {}
|
|
2011
|
+
} catch (e) { /* native sidebar unavailable */ }
|
|
2012
2012
|
|
|
2013
2013
|
try {
|
|
2014
2014
|
ctx.inject(['betterSidebar'], (sctx) => {
|
|
@@ -2026,7 +2026,7 @@ window.__ModuleLoader__.load({
|
|
|
2026
2026
|
console.warn('[dsh-image-gen] betterSidebar registerTab failed', e)
|
|
2027
2027
|
}
|
|
2028
2028
|
})
|
|
2029
|
-
} catch (e) {}
|
|
2029
|
+
} catch (e) { /* betterSidebar unavailable */ }
|
|
2030
2030
|
}
|
|
2031
2031
|
|
|
2032
2032
|
// 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'
|
|
@@ -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 }
|
package/lib/prompt-polisher.js
CHANGED
|
@@ -185,10 +185,3 @@ export function polishPrompt(basePrompt, stylePreset = null, {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
export function listCuratedStyles() {
|
|
189
|
-
return Object.values(CURATED_STYLES).map(s => ({
|
|
190
|
-
id: s.id,
|
|
191
|
-
label: s.label,
|
|
192
|
-
guidanceScale: s.guidanceScale,
|
|
193
|
-
}))
|
|
194
|
-
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// lib/provider-utils.js
|
|
2
|
+
// Shared provider utilities and size/format constants extracted from providers.js (#239)
|
|
3
|
+
|
|
4
|
+
export function resolveApiKeyCandidates(ref) {
|
|
5
|
+
if (!ref) return []
|
|
6
|
+
const candidates = [ref]
|
|
7
|
+
if (ref === 'FAL_API_KEY') candidates.push('FAL_KEY')
|
|
8
|
+
else if (ref === 'FAL_KEY') candidates.push('FAL_API_KEY')
|
|
9
|
+
return candidates
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export const PROVIDER_MAX_COUNTS = {
|
|
14
|
+
fal: 4,
|
|
15
|
+
replicate: 4,
|
|
16
|
+
custom: 10,
|
|
17
|
+
seedream: 10,
|
|
18
|
+
gemini: 4,
|
|
19
|
+
codex: 1,
|
|
20
|
+
grok: 1,
|
|
21
|
+
local: 4,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeCount(count, max = 4) {
|
|
25
|
+
const n = Math.floor(Number(count))
|
|
26
|
+
if (!Number.isFinite(n)) return 1
|
|
27
|
+
return Math.max(1, Math.min(max, n))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @internal — test helper for validating provider count limits */
|
|
31
|
+
export function clampProviderCount(provider, count) {
|
|
32
|
+
const max = PROVIDER_MAX_COUNTS[provider] || 4
|
|
33
|
+
return normalizeCount(count, max)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildEndpointUrl(baseURL, pathSuffix) {
|
|
37
|
+
const base = String(baseURL || '').trim().replace(/\/+$/, '')
|
|
38
|
+
const suffix = String(pathSuffix || '').trim().replace(/^\/+/, '')
|
|
39
|
+
return `${base}/${suffix}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @internal — test helper for abort signal simulation */
|
|
43
|
+
export function createAbortError(message = 'Image generation cancelled') {
|
|
44
|
+
const err = new Error(message)
|
|
45
|
+
err.name = 'AbortError'
|
|
46
|
+
return err
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/** Fast Laplacian-like sharpness and entropy estimator across image scanlines. */
|
|
51
|
+
export function estimateSharpnessAndVariance(bytes) {
|
|
52
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || [])
|
|
53
|
+
if (!buf || buf.length < 64) {
|
|
54
|
+
return { score: 0, passed: false, isBlank: true, reason: 'Empty or corrupt image buffer' }
|
|
55
|
+
}
|
|
56
|
+
let sum = 0
|
|
57
|
+
let diffSum = 0
|
|
58
|
+
const sampleStep = Math.max(1, Math.floor(buf.length / 4096))
|
|
59
|
+
let count = 0
|
|
60
|
+
for (let i = 8; i < buf.length - sampleStep; i += sampleStep) {
|
|
61
|
+
const v = buf[i]
|
|
62
|
+
const nextV = buf[i + sampleStep]
|
|
63
|
+
sum += v
|
|
64
|
+
diffSum += Math.abs(v - nextV)
|
|
65
|
+
count++
|
|
66
|
+
}
|
|
67
|
+
const avgDiff = count > 0 ? diffSum / count : 0
|
|
68
|
+
if (avgDiff < 2) {
|
|
69
|
+
return { score: 0.1, passed: false, isBlank: true, reason: 'Image appears blank or solid monochrome' }
|
|
70
|
+
}
|
|
71
|
+
const score = Math.min(0.98, Math.max(0.3, +(0.5 + (avgDiff / 255) * 0.5).toFixed(2)))
|
|
72
|
+
return { score, passed: score >= 0.5, isBlank: false, avgDiff: +avgDiff.toFixed(2) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Adaptive color palette quantizer for SVG vectorization. */
|
|
76
|
+
export function quantizePalette(colorMode = 'color', paletteSize = 16) {
|
|
77
|
+
if (colorMode === 'binary') return ['#000000', '#ffffff']
|
|
78
|
+
if (colorMode === 'grayscale') return ['#000000', '#444444', '#888888', '#cccccc', '#ffffff']
|
|
79
|
+
// Standard vibrant UI vector palette
|
|
80
|
+
return [
|
|
81
|
+
'#000000', '#ffffff', '#e11d48', '#2563eb',
|
|
82
|
+
'#16a34a', '#ca8a04', '#9333ea', '#0891b2',
|
|
83
|
+
'#475569', '#64748b', '#94a3b8', '#cbd5e1'
|
|
84
|
+
].slice(0, Math.max(2, paletteSize))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/** Format parameters in standard Automatic1111 / ComfyUI text format for drag-and-drop support. */
|
|
89
|
+
export function formatA1111Parameters(metadata = {}) {
|
|
90
|
+
if (typeof metadata === 'string') return metadata
|
|
91
|
+
const prompt = metadata.prompt || ''
|
|
92
|
+
const neg = metadata.negative_prompt || metadata.negativePrompt || ''
|
|
93
|
+
const seed = metadata.seed ?? ''
|
|
94
|
+
const size = metadata.size || (metadata.width && metadata.height ? `${metadata.width}x${metadata.height}` : '1024x1024')
|
|
95
|
+
const model = metadata.model || metadata.provider || ''
|
|
96
|
+
const steps = metadata.steps || 20
|
|
97
|
+
const cfg = metadata.cfg_scale || metadata.guidance_scale || 7
|
|
98
|
+
|
|
99
|
+
let out = prompt
|
|
100
|
+
if (neg) out += `\nNegative prompt: ${neg}`
|
|
101
|
+
out += `\nSteps: ${steps}, Sampler: Euler, CFG scale: ${cfg}, Seed: ${seed}, Size: ${size}, Model: ${model}`
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
import { createHash } from 'node:crypto'
|
|
107
|
+
|
|
108
|
+
/** Deterministic sha256 hash for image generation caching. */
|
|
109
|
+
export function computeGenerationHash({ provider, model, prompt, seed, size, style }) {
|
|
110
|
+
const norm = [
|
|
111
|
+
String(provider || '').trim().toLowerCase(),
|
|
112
|
+
String(model || '').trim().toLowerCase(),
|
|
113
|
+
String(prompt || '').trim(),
|
|
114
|
+
String(seed ?? ''),
|
|
115
|
+
String(size || '').trim().toLowerCase(),
|
|
116
|
+
String(style || '').trim().toLowerCase(),
|
|
117
|
+
].join('|')
|
|
118
|
+
return createHash('sha256').update(norm).digest('hex')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Image generation provider implementations.
|
|
122
|
+
//
|
|
123
|
+
// A provider receives a generation job and returns finished image bytes. Everything
|
|
124
|
+
// that follows (attachments, workspace files, links, card rendering) is shared
|
|
125
|
+
// across all providers and lives in index.js.
|
|
126
|
+
//
|
|
127
|
+
// Network is injected via fetchImpl and keys via resolveKey, enabling fully isolated unit tests.
|
|
128
|
+
|
|
129
|
+
export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
130
|
+
|
|
131
|
+
/** Clamp count from tool arguments to range 1..4. */
|
|
132
|
+
/** Provider fallback ordering: primary provider first, followed by PROVIDER_KEYS. */
|
|
133
|
+
export function fallbackOrder(primary) {
|
|
134
|
+
return [primary, ...PROVIDER_KEYS.filter((k) => k !== primary)]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Iterates through candidate generators, returning the first successful result.
|
|
139
|
+
* Throws an aggregate error if all candidates fail.
|
|
140
|
+
* @param generators - array of (key, seed) => Promise<generated> functions.
|
|
141
|
+
* @param order - candidate key evaluation order.
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
/** Calculate exponential backoff delay with jitter. */
|
|
145
|
+
export function calculateBackoff(attempt, baseInterval = 500, maxInterval = 5000, jitterFactor = 0.2) {
|
|
146
|
+
const exp = Math.min(maxInterval, baseInterval * Math.pow(1.3, attempt))
|
|
147
|
+
const jitter = exp * jitterFactor * (Math.random() * 2 - 1)
|
|
148
|
+
return Math.max(100, Math.floor(exp + jitter))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Check if an error is a fatal client error that should NOT be cascaded to other providers. */
|
|
152
|
+
export function isFatalClientError(error) {
|
|
153
|
+
const msg = (error?.message || String(error || '')).toLowerCase()
|
|
154
|
+
return (
|
|
155
|
+
msg.includes('content policy') ||
|
|
156
|
+
msg.includes('safety system') ||
|
|
157
|
+
msg.includes('nsfw') ||
|
|
158
|
+
msg.includes('moderation') ||
|
|
159
|
+
msg.includes('bad request (http 400') ||
|
|
160
|
+
msg.includes('invalid_prompt') ||
|
|
161
|
+
msg.includes('prompt is required') ||
|
|
162
|
+
msg.includes('unsupported image format') ||
|
|
163
|
+
msg.includes('402 payment required') ||
|
|
164
|
+
msg.includes('insufficient_quota') ||
|
|
165
|
+
msg.includes('insufficient credits') ||
|
|
166
|
+
msg.includes('exceeded your current quota') ||
|
|
167
|
+
msg.includes('balance is insufficient')
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Safely extract a human-readable message from any error, object, or response.
|
|
173
|
+
* Completely eliminates "[object Object]" and prevents duplicate provider prefixes (e.g. "codex: codex: ...").
|
|
174
|
+
*/
|
|
175
|
+
export function formatErrorMessage(e, providerKey = '') {
|
|
176
|
+
if (e === null || e === undefined) {
|
|
177
|
+
return providerKey ? `${providerKey}: unknown error` : 'unknown error'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let msg = ''
|
|
181
|
+
if (typeof e === 'string') {
|
|
182
|
+
msg = e
|
|
183
|
+
} else if (typeof e === 'object') {
|
|
184
|
+
if (typeof e.message === 'string' && e.message && e.message !== '[object Object]') {
|
|
185
|
+
msg = e.message
|
|
186
|
+
} else if (typeof e.detail === 'string' && e.detail) {
|
|
187
|
+
msg = e.detail
|
|
188
|
+
} else if (e.error) {
|
|
189
|
+
if (typeof e.error === 'string') msg = e.error
|
|
190
|
+
else if (typeof e.error.message === 'string') msg = e.error.message
|
|
191
|
+
else if (typeof e.error.detail === 'string') msg = e.error.detail
|
|
192
|
+
else {
|
|
193
|
+
try { msg = JSON.stringify(e.error) } catch { msg = String(e.error) }
|
|
194
|
+
}
|
|
195
|
+
} else if (typeof e.statusText === 'string' && e.statusText) {
|
|
196
|
+
msg = `HTTP ${e.status || ''} ${e.statusText}`.trim()
|
|
197
|
+
} else if (typeof e.cause === 'string') {
|
|
198
|
+
msg = e.cause
|
|
199
|
+
} else if (e.cause && typeof e.cause.message === 'string') {
|
|
200
|
+
msg = e.cause.message
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!msg || msg === '[object Object]') {
|
|
204
|
+
try {
|
|
205
|
+
const json = JSON.stringify(e)
|
|
206
|
+
if (json && json !== '{}') {
|
|
207
|
+
msg = json.slice(0, 500)
|
|
208
|
+
}
|
|
209
|
+
} catch { /* response body not JSON; use raw text */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!msg || msg === '[object Object]') {
|
|
213
|
+
const keys = Object.getOwnPropertyNames(e)
|
|
214
|
+
if (keys.length) {
|
|
215
|
+
msg = `error [${keys.join(', ')}]`
|
|
216
|
+
} else {
|
|
217
|
+
msg = String(e)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
msg = String(e)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
msg = String(msg || 'unknown error').trim()
|
|
225
|
+
|
|
226
|
+
if (providerKey) {
|
|
227
|
+
const prefixRegex = new RegExp(`^${providerKey}\\s*:\\s*`, 'i')
|
|
228
|
+
while (prefixRegex.test(msg)) {
|
|
229
|
+
msg = msg.replace(prefixRegex, '').trim()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!msg || msg === '[object Object]') {
|
|
234
|
+
msg = 'unknown error object'
|
|
235
|
+
} else if (msg.includes('[object Object]')) {
|
|
236
|
+
msg = msg.replace(/\[object Object\]/g, 'unknown error object').trim()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (providerKey) {
|
|
240
|
+
return `${providerKey}: ${msg}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return msg
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function resolveSubscriptionSize(size, aspectPixels, aspectRatio) {
|
|
247
|
+
if (size && SUBSCRIPTION_SIZES[size]) {
|
|
248
|
+
return SUBSCRIPTION_SIZES[size]
|
|
249
|
+
}
|
|
250
|
+
const ratio = String(aspectRatio || '').trim()
|
|
251
|
+
if (ratio === '16:9' || ratio === '3:2' || ratio === '4:3') {
|
|
252
|
+
return '1536x1024'
|
|
253
|
+
}
|
|
254
|
+
if (ratio === '9:16' || ratio === '2:3' || ratio === '3:4') {
|
|
255
|
+
return '1024x1536'
|
|
256
|
+
}
|
|
257
|
+
if (ratio === '1:1') {
|
|
258
|
+
return '1024x1024'
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(aspectPixels) && aspectPixels.length === 2) {
|
|
261
|
+
const [w, h] = aspectPixels
|
|
262
|
+
if (w > h) return '1536x1024'
|
|
263
|
+
if (h > w) return '1024x1536'
|
|
264
|
+
return '1024x1024'
|
|
265
|
+
}
|
|
266
|
+
return '1024x1024'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const SUBSCRIPTION_SIZES = {
|
|
270
|
+
square_hd: '1024x1024',
|
|
271
|
+
square: '1024x1024',
|
|
272
|
+
portrait_4_3: '1024x1536',
|
|
273
|
+
portrait_16_9: '1024x1536',
|
|
274
|
+
landscape_4_3: '1536x1024',
|
|
275
|
+
landscape_16_9: '1536x1024',
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Image sizes accepted by fal-ai/flux-2/klein (and most FAL flux models). */
|
|
279
|
+
export const IMAGE_SIZES = [
|
|
280
|
+
'square_hd',
|
|
281
|
+
'square',
|
|
282
|
+
'portrait_4_3',
|
|
283
|
+
'portrait_16_9',
|
|
284
|
+
'landscape_4_3',
|
|
285
|
+
'landscape_16_9',
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
/** Output formats accepted by the model. */
|
|
289
|
+
export const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
290
|
+
|
|
291
|
+
// Named sizes are a unified abstraction across all providers.
|
|
292
|
+
// FAL accepts named identifiers; OpenAI-compatible gateways require WxH resolution.
|
|
293
|
+
export const SIZE_PIXELS = {
|
|
294
|
+
square_hd: '1024x1024',
|
|
295
|
+
square: '512x512',
|
|
296
|
+
portrait_4_3: '768x1024',
|
|
297
|
+
portrait_16_9: '576x1024',
|
|
298
|
+
landscape_4_3: '1024x768',
|
|
299
|
+
landscape_16_9: '1024x576',
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Normalize a raw key into a FAL `Authorization: Key <key>` value. */
|