@goodandready/dsh-image-gen 0.10.7 → 0.10.10

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
@@ -1,3 +1,9 @@
1
+
2
+ ## 🚀 Updates v0.10.8: Lossless JSON Unification, Dual-Output & Strict Validation (#199)
3
+ - **Lossless JSON & Dual-Output Unification**: `upscale_image`, `remove_background`, `blend_images`, and `vectorize_image` now consistently return `toLosslessJson` and formatted markdown `summary` for text-only LLMs.
4
+ - **Strict Input Image Validation**: `extract_design_tokens`, `image_to_css_gradient`, and `check_image_contrast` explicitly validate source image readability instead of silent fallback.
5
+ - **Unit Testing Suite**: Added `test/tool-consistency.test.mjs` covering tokenization, gradient generator, and PWA suite.
6
+
1
7
  # 📦 @goodandready/dsh-image-gen
2
8
 
3
9
  <div align="center">
@@ -77,6 +83,13 @@
77
83
  * **FAL Credential Aliasing**: seamless fallback between `FAL_API_KEY` and `FAL_KEY` in credentials and environment.
78
84
  * **Subscription Aspect Ratio Mapping**: maps aspect ratios (`16:9`, `3:2`, `9:16`, `2:3`) to appropriate subscription dimensions (`1536x1024` / `1024x1536`) instead of falling back to default square `1024x1024`.
79
85
 
86
+
87
+ ### 🚀 What's New in v0.10.10 (#201, #203)
88
+ * **Settings GUI Stabilization (#201)**: Fixed `booleanField` spec in client runtime that prevented the settings configuration pane from rendering in DSH Web UI. All configuration fields (providers, model identifiers, API credentials, style presets, LLM enhancer, timeouts, cache retention) render cleanly.
89
+ * **Streamlined Settings UI (#203)**: Removed the bulky in-settings history gallery to keep the configuration panel focused, fast, and organized.
90
+ * **Comprehensive Localization (#201)**: Added 100% dictionary coverage for all provider credentials, endpoints, and field hints in English and Russian.
91
+ * **Robust Slot Mounting**: Implemented `registerFirst` helper with graceful fallback between `settings.plugin.item` and `settings.section`.
92
+
80
93
  ## 🎨 Supported Generation Backends
81
94
 
82
95
  * **`fal`** (Default): FAL.ai queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
package/README.ru.md CHANGED
@@ -116,6 +116,12 @@ graph LR
116
116
  ---
117
117
 
118
118
 
119
+
120
+ ## 🚀 Обновления v0.10.8: унификация Lossless JSON, Dual-Output и строгая валидация (#199)
121
+ - **Унификация Lossless JSON и Dual-Output**: Инструменты `upscale_image`, `remove_background`, `blend_images`, `vectorize_image` теперь гарантированно возвращают валидный `toLosslessJson` и генерируют структурированный `summary` с превью для текстовых LLM.
122
+ - **Строгая валидация входных изображений**: В инструментах `extract_design_tokens`, `image_to_css_gradient` и `check_image_contrast` добавлена явная проверка существования файла с информативной ошибкой (`Image not found or unreadable: <path>`).
123
+ - **Сквозное модульное тестирование**: Добавлен тест-сьют `test/tool-consistency.test.mjs` для проверки отказоустойчивости дизайн- и PWA-инструментов.
124
+
119
125
  ## 🚀 Обновления v0.10.4: жизненный цикл Cordis, полнота настроек и интернационализация
120
126
  - **Жизненный цикл Cordis (#136)**: регистрация всех 7 инструментов обёрнута в `ctx.effect` для штатной отписки при перезагрузке плагина.
121
127
  - **Полнота настроек в GUI (#137)**: в карточку настроек добавлены поля для Replicate, SeaDream, Gemini, Local ComfyUI/A1111, пресетов стилей и LLM-улучшителя.
@@ -123,6 +129,13 @@ graph LR
123
129
  - **Оптимизация компонентов (#139)**: из `FalImageCard` удалён неиспользуемый мёртвый стейт.
124
130
  - **100% интернационализация (#140)**: устранены захардкоженные русские строки, добавлены переводы на русский, английский и китайский языки.
125
131
 
132
+
133
+ ### 🚀 Что нового в v0.10.10 (#201, #203)
134
+ * **Стабилизация окна настроек GUI (#201)**: Исправлена спецификация `booleanField` в клиентском коде, вызывавшая падение рендера карточки настроек плагина в DSH Web UI. Форма настроек теперь открывается штатно со всеми полями, селекторами провайдеров, моделями, ссылками на ключи и пресетами.
135
+ * **Очистка интерфейса настроек (#203)**: Из окна настроек убрана встроенная галерея истории генераций, загромождавшая карточку настроек.
136
+ * **Полная локализация (#201)**: Добавлены все недостающие строки переводов и подсказок на русском и английском языках для всех поддерживаемых провайдеров.
137
+ * **Надёжная регистрация слотов**: Реализован `registerFirst` с плавным откатом между `settings.plugin.item` и `settings.section`.
138
+
126
139
  ## 🎨 Поддерживаемые провайдеры генерации
127
140
 
128
141
  * **`fal`** *(дефолт)*: Сверхбыстрая очередь FAL.ai (`FLUX.1-schnell`, `FLUX-dev`, `SDXL`, BiRefNet, Clarity Upscaler).
package/lib/client.js CHANGED
@@ -82,12 +82,7 @@ window.__ModuleLoader__.load({
82
82
  '.fal_input:disabled,.fal_select:disabled{opacity:.6}' +
83
83
  '.fal_hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}' +
84
84
  '.fal_invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px}' +
85
- '.fal_sep{height:1px;background:var(--dsw-alias-border-l2);margin:2px 0 0}' +
86
- '.fal_gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-top:12px}' +
87
- '.fal_galleryItem{display:flex;flex-direction:column;gap:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}' +
88
- '.fal_galleryImg{width:100%;height:90px;object-fit:cover;display:block}' +
89
- '.fal_galleryMeta{font-size:11px;color:var(--dsw-alias-label-secondary);padding:4px 6px}' +
90
- '.fal_galleryEmpty{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:12px}'
85
+ '.fal_sep{height:1px;background:var(--dsw-alias-border-l2);margin:2px 0 0}'
91
86
  const tagId = 'dsh-image-gen/settings-card.module.css'
92
87
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {
93
88
  const tag = document.createElement('style')
@@ -124,11 +119,6 @@ window.__ModuleLoader__.load({
124
119
  save: 'fal_save',
125
120
  select: 'fal_select',
126
121
  sep: 'fal_sep',
127
- gallery: 'fal_gallery',
128
- galleryItem: 'fal_galleryItem',
129
- galleryImg: 'fal_galleryImg',
130
- galleryMeta: 'fal_galleryMeta',
131
- galleryEmpty: 'fal_galleryEmpty',
132
122
  }
133
123
 
134
124
  // -------------------------------------------------------------- chrome
@@ -136,7 +126,7 @@ window.__ModuleLoader__.load({
136
126
  function PluginSettingsCard(props) {
137
127
  // Карточка живёт в собственном разделе настроек и на странице одна, так
138
128
  // что открыта сразу: свёрнутая выглядела бы как пустая страница.
139
- const [open, setOpen] = react.useState(false)
129
+ const [open, setOpen] = react.useState(props.defaultOpen ?? true)
140
130
  const { state } = props
141
131
  // Раньше здесь было `return null`, и раздел оказывался пустым, если хост
142
132
  // ещё не объявил настройки. Пустая страница ничего не объясняет — лучше
@@ -464,7 +454,17 @@ window.__ModuleLoader__.load({
464
454
  const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
465
455
 
466
456
  /** Field specs in display order. */
467
- const booleanField = (name) => ({ name, type: 'boolean', parse: (v) => v === 'true' || v === true, format: (v) => String(!!v) })
457
+ const booleanField = (field) => ({
458
+ field,
459
+ type: 'boolean',
460
+ format: (value) => (typeof value === 'boolean' ? String(value) : (value === 'true' || value === 'false' ? value : '')),
461
+ parse: (text) => {
462
+ if (text === '') return { kind: 'clear' }
463
+ if (text === 'true') return { kind: 'set', value: true }
464
+ if (text === 'false') return { kind: 'set', value: false }
465
+ return void 0
466
+ },
467
+ })
468
468
  const STYLE_PRESET_OPTIONS = ['', 'cinematic', 'anime', 'digital_art', 'photographic', 'fantasy', 'isometric', 'cyberpunk', 'watercolor', 'origami', 'oil_painting', 'pixel_art', 'logo']
469
469
 
470
470
  const FIELDS = [
@@ -511,7 +511,6 @@ window.__ModuleLoader__.load({
511
511
  { field: 'deliverAs', spec: selectField('deliverAs', DELIVERY_MODES), kind: 'select', options: DELIVERY_MODES, labelKey: 'f.deliverAs', hintKey: 'f.deliverAsHint' },
512
512
  { field: 'outputDir', spec: textField('outputDir'), kind: 'text', labelKey: 'f.outputDir', hintKey: 'f.outputDirHint', placeholderKey: 'p.outputDir' },
513
513
  { field: 'cacheTtlDays', spec: numberField('cacheTtlDays'), kind: 'number', labelKey: 'f.cacheTtlDays', hintKey: 'f.cacheTtlDaysHint', placeholderKey: 'p.cacheTtlDays' },
514
- { field: 'maxHistoryEntries', spec: numberField('maxHistoryEntries'), kind: 'number', labelKey: 'f.maxHistoryEntries', hintKey: 'f.maxHistoryEntriesHint', placeholderKey: 'p.maxHistoryEntries' },
515
514
  ]
516
515
 
517
516
  function Field(props) {
@@ -539,66 +538,11 @@ window.__ModuleLoader__.load({
539
538
  return react_jsx_runtime.jsx(ValueField, { ...base, numeric: entry.kind === 'number', placeholder: entry.placeholderKey ? t(entry.placeholderKey) : '' })
540
539
  }
541
540
 
542
- /** Render the plugin settings card. */
543
- /** Галерея истории генераций: тянет /history и рендерит миниатюры. */
544
- function HistoryGallery(props) {
545
- const [entries, setEntries] = react.useState([])
546
- const [loading, setLoading] = react.useState(true)
547
- const [filter, setFilter] = react.useState('')
548
- react.useEffect(() => {
549
- let cancelled = false
550
- fetch('/dsh-image-gen/history')
551
- .then((r) => r.json())
552
- .then((data) => { if (!cancelled) { setEntries(Array.isArray(data) ? data : []); setLoading(false) } })
553
- .catch(() => { if (!cancelled) setLoading(false) })
554
- return () => { cancelled = true }
555
- }, [])
556
- const t = props.t
557
- if (loading) return react_jsx_runtime.jsx('p', { className: cssDefault.galleryEmpty, children: t('history.loading') })
558
- if (!entries.length) return react_jsx_runtime.jsx('p', { className: cssDefault.galleryEmpty, children: t('history.empty') })
559
- const providers = [...new Set(entries.map((e) => e.provider).filter(Boolean))]
560
- const filtered = filter ? entries.filter((e) => e.provider === filter) : entries
561
- return react_jsx_runtime.jsxs('div', {
562
- className: cssDefault.gallery,
563
- children: [
564
- react_jsx_runtime.jsx('select', {
565
- className: cssDefault.select,
566
- value: filter,
567
- onChange: (e) => setFilter(e.target.value),
568
- children: [
569
- react_jsx_runtime.jsx('option', { value: '', children: t('history.all') }),
570
- providers.map((p) => react_jsx_runtime.jsx('option', { value: p, children: p }, p)),
571
- ],
572
- }),
573
- filtered.map((e) =>
574
- react_jsx_runtime.jsxs('a', {
575
- className: cssDefault.galleryItem,
576
- key: e.path,
577
- href: e.thumbnailUrl || '#',
578
- target: '_blank',
579
- rel: 'noreferrer',
580
- children: [
581
- e.thumbnailUrl ? react_jsx_runtime.jsx('img', { className: cssDefault.galleryImg, src: e.thumbnailUrl, alt: e.prompt || '', loading: 'lazy' }) : null,
582
- react_jsx_runtime.jsx('div', { className: cssDefault.galleryMeta, children: `${e.provider || ''} · ${e.size || ''}` }),
583
- e.seed !== undefined ? react_jsx_runtime.jsx('button', {
584
- type: 'button',
585
- className: 'fal-repeat',
586
- onClick: (ev) => {
587
- ev.preventDefault()
588
- ev.stopPropagation()
589
- try { navigator.clipboard.writeText(String(e.seed)) } catch (err) { /* нет доступа к буферу */ }
590
- },
591
- }, `seed ${e.seed}`) : null,
592
- ],
593
- }),
594
- ),
595
- ],
596
- })
597
- }
598
-
599
541
  function FalSettingsCard(props) {
600
- const { t } = props
601
- const state = props.useFalSettingsCard((snapshot) => snapshot)
542
+ const t = props.t || ((k) => k)
543
+ const state = (typeof props.useFalSettingsCard === 'function'
544
+ ? props.useFalSettingsCard((snapshot) => snapshot)
545
+ : null) || props.state || { available: true, writable: true, provider: { text: 'fal' } }
602
546
  const disabled = !state.writable
603
547
  const currentProvider = (state.provider && state.provider.text) || 'fal'
604
548
  const visibleFields = FIELDS.filter((entry) => !entry.when || entry.when.includes(currentProvider))
@@ -616,7 +560,7 @@ window.__ModuleLoader__.load({
616
560
  onSave: props.save,
617
561
  onDiscard: props.discard,
618
562
  children: [
619
- react_jsx_runtime.jsx('p', { className: cssDefault.hint, children: t('settings.credentialHint') }),
563
+ react_jsx_runtime.jsx('p', { className: cssDefault.hint, key: 'credHint', children: t('settings.credentialHint') }),
620
564
  visibleFields.map((entry) =>
621
565
  react_jsx_runtime.jsx(
622
566
  Field,
@@ -625,15 +569,12 @@ window.__ModuleLoader__.load({
625
569
  state: state[entry.field] ?? { text: '', overridden: false, invalid: false },
626
570
  fieldProps,
627
571
  t,
628
- onEdit: (text) => props.edit(entry.field, text),
629
- onReset: () => props.resetField(entry.field),
572
+ onEdit: (text) => props.edit && props.edit(entry.field, text),
573
+ onReset: () => props.resetField && props.resetField(entry.field),
630
574
  },
631
575
  entry.field,
632
576
  ),
633
577
  ),
634
- react_jsx_runtime.jsx('div', { className: cssDefault.sep, key: 'hsep' }),
635
- react_jsx_runtime.jsx('p', { className: cssDefault.hint, key: 'hlabel', children: t('history.title') }),
636
- react_jsx_runtime.jsx(HistoryGallery, { t, key: 'history' }),
637
578
  ],
638
579
  })
639
580
  }
@@ -680,6 +621,53 @@ window.__ModuleLoader__.load({
680
621
  'settings.saving': 'Saving…',
681
622
  'settings.saveFailed': 'Save failed',
682
623
  'f.inherit': 'Inherit (default)',
624
+ 'f.enabled': 'Enabled',
625
+ 'f.enabledHint': 'Master switch: enable or disable the generate_image tool.',
626
+ 'f.needFal': 'requires FAL API key',
627
+ 'f.needCustom': 'requires custom endpoint URL',
628
+ 'f.needCodex': 'uses Codex subscription session',
629
+ 'f.needGrok': 'uses Grok subscription session',
630
+ 'f.needLocal': 'connects to local ComfyUI/A1111',
631
+ 'f.needSeedream': 'requires SeaDream API key',
632
+ 'f.needGemini': 'requires Gemini API key',
633
+ 'f.needReplicate': 'requires Replicate API token',
634
+ 'f.replicateModel': 'Model (Replicate)',
635
+ 'f.replicateModelHint': 'Replicate model identifier, e.g. black-forest-labs/flux-schnell.',
636
+ 'f.replicateKeyEnv': 'API key reference (Replicate)',
637
+ 'f.replicateKeyEnvHint': 'Credential reference / env var holding the Replicate API token.',
638
+ 'p.replicateModel': 'Example: black-forest-labs/flux-schnell',
639
+ 'p.replicateKeyEnv': 'Example: REPLICATE_API_TOKEN',
640
+ 'f.seedreamModel': 'Model (SeaDream)',
641
+ 'f.seedreamModelHint': 'SeaDream model identifier.',
642
+ 'f.seedreamKeyEnv': 'API key reference (SeaDream)',
643
+ 'f.seedreamKeyEnvHint': 'Credential reference / env var holding the SeaDream API key.',
644
+ 'f.seedreamBaseURL': 'Base URL (SeaDream)',
645
+ 'f.seedreamBaseURLHint': 'SeaDream endpoint URL (Volcengine Ark API).',
646
+ 'p.seedreamModel': 'Example: Doubao-SeaDream-3.0',
647
+ 'p.seedreamKeyEnv': 'Example: SEADREAM_API_KEY',
648
+ 'p.seedreamBaseURL': 'Example: https://ark.cn-beijing.volces.com/api/v3',
649
+ 'f.geminiModel': 'Model (Gemini)',
650
+ 'f.geminiModelHint': 'Gemini Imagen model identifier.',
651
+ 'f.geminiKeyEnv': 'API key reference (Gemini)',
652
+ 'f.geminiKeyEnvHint': 'Credential reference / env var holding the Gemini API key.',
653
+ 'p.geminiModel': 'Example: imagen-3.0-generate-002',
654
+ 'p.geminiKeyEnv': 'Example: GEMINI_API_KEY',
655
+ 'f.localComfyUrl': 'ComfyUI URL',
656
+ 'f.localComfyUrlHint': 'Base URL of local ComfyUI server.',
657
+ 'p.localComfyUrl': 'Example: http://127.0.0.1:8188',
658
+ 'f.localA1111Url': 'Automatic1111 URL',
659
+ 'f.localA1111UrlHint': 'Base URL of local Automatic1111 / SD WebUI.',
660
+ 'p.localA1111Url': 'Example: http://127.0.0.1:7860',
661
+ 'f.stylePreset': 'Default style preset',
662
+ 'f.stylePresetHint': 'Style preset automatically applied if the tool call does not specify one.',
663
+ 'f.enableLlmEnhancer': 'LLM prompt enhancer',
664
+ 'f.enableLlmEnhancerHint': 'Automatically refine and expand prompts before generation using session LLM.',
665
+ 'f.cacheTtlDays': 'Cache retention (days)',
666
+ 'f.cacheTtlDaysHint': 'How many days downloaded images are stored in cache before cleanup.',
667
+ 'p.cacheTtlDays': 'Example: 7',
668
+ 'f.maxHistoryEntries': 'Max history entries',
669
+ 'f.maxHistoryEntriesHint': 'Maximum number of items shown in generation history gallery.',
670
+ 'p.maxHistoryEntries': 'Example: 50',
683
671
  'f.provider': 'Provider',
684
672
  'f.providerHint': 'fal — the FAL queue configured below. custom — any OpenAI-compatible API, configured further down. '
685
673
  + 'codex and grok — through a subscription you are logged into, no key needed here.',
@@ -743,6 +731,53 @@ window.__ModuleLoader__.load({
743
731
  'settings.saving': 'Сохранение…',
744
732
  'settings.saveFailed': 'Не удалось сохранить',
745
733
  'f.inherit': 'Унаследовать (по умолчанию)',
734
+ 'f.enabled': 'Включено',
735
+ 'f.enabledHint': 'Главный переключатель: включить или отключить инструмент generate_image.',
736
+ 'f.needFal': 'требуется ключ API FAL',
737
+ 'f.needCustom': 'требуется базовый URL API',
738
+ 'f.needCodex': 'использует сессию подписки Codex',
739
+ 'f.needGrok': 'использует сессию подписки Grok',
740
+ 'f.needLocal': 'подключается к локальному ComfyUI/A1111',
741
+ 'f.needSeedream': 'требуется ключ API SeaDream',
742
+ 'f.needGemini': 'требуется ключ API Gemini',
743
+ 'f.needReplicate': 'требуется токен API Replicate',
744
+ 'f.replicateModel': 'Модель (Replicate)',
745
+ 'f.replicateModelHint': 'Идентификатор модели Replicate, например black-forest-labs/flux-schnell.',
746
+ 'f.replicateKeyEnv': 'Ссылка на ключ (Replicate)',
747
+ 'f.replicateKeyEnvHint': 'Имя credential-ссылки / переменной окружения с токеном Replicate.',
748
+ 'p.replicateModel': 'Пример: black-forest-labs/flux-schnell',
749
+ 'p.replicateKeyEnv': 'Пример: REPLICATE_API_TOKEN',
750
+ 'f.seedreamModel': 'Модель (SeaDream)',
751
+ 'f.seedreamModelHint': 'Идентификатор модели SeaDream.',
752
+ 'f.seedreamKeyEnv': 'Ссылка на ключ (SeaDream)',
753
+ 'f.seedreamKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом SeaDream.',
754
+ 'f.seedreamBaseURL': 'Базовый URL (SeaDream)',
755
+ 'f.seedreamBaseURLHint': 'Базовый URL API SeaDream (Volcengine Ark).',
756
+ 'p.seedreamModel': 'Пример: Doubao-SeaDream-3.0',
757
+ 'p.seedreamKeyEnv': 'Пример: SEADREAM_API_KEY',
758
+ 'p.seedreamBaseURL': 'Пример: https://ark.cn-beijing.volces.com/api/v3',
759
+ 'f.geminiModel': 'Модель (Gemini)',
760
+ 'f.geminiModelHint': 'Идентификатор модели Gemini Imagen.',
761
+ 'f.geminiKeyEnv': 'Ссылка на ключ (Gemini)',
762
+ 'f.geminiKeyEnvHint': 'Имя credential-ссылки / переменной окружения с ключом Gemini.',
763
+ 'p.geminiModel': 'Пример: imagen-3.0-generate-002',
764
+ 'p.geminiKeyEnv': 'Пример: GEMINI_API_KEY',
765
+ 'f.localComfyUrl': 'URL ComfyUI',
766
+ 'f.localComfyUrlHint': 'Базовый URL локального сервера ComfyUI.',
767
+ 'p.localComfyUrl': 'Пример: http://127.0.0.1:8188',
768
+ 'f.localA1111Url': 'URL Automatic1111',
769
+ 'f.localA1111UrlHint': 'Базовый URL локального SD WebUI / Automatic1111.',
770
+ 'p.localA1111Url': 'Пример: http://127.0.0.1:7860',
771
+ 'f.stylePreset': 'Стилевой пресет по умолчанию',
772
+ 'f.stylePresetHint': 'Пресет стилизации, применяемый если инструмент вызван без указания стиля.',
773
+ 'f.enableLlmEnhancer': 'Улучшение промпта через LLM',
774
+ 'f.enableLlmEnhancerHint': 'Автоматически детализировать и улучшать промпты через чат-модель перед генерацией.',
775
+ 'f.cacheTtlDays': 'Срок хранения кэша (дней)',
776
+ 'f.cacheTtlDaysHint': 'Сколько дней скачанные картинки хранятся в кэше до очистки.',
777
+ 'p.cacheTtlDays': 'Пример: 7',
778
+ 'f.maxHistoryEntries': 'Максимум записей в истории',
779
+ 'f.maxHistoryEntriesHint': 'Максимальное количество картинок в галерее истории генераций.',
780
+ 'p.maxHistoryEntries': 'Пример: 50',
746
781
  'f.provider': 'Провайдер',
747
782
  'f.providerHint': 'fal — очередь FAL, настроенная ниже. custom — любой OpenAI-совместимый API, настраивается дальше. '
748
783
  + 'codex и grok — через подписку, в которую вы вошли; ключ здесь не нужен.',
@@ -1226,36 +1261,40 @@ function FalImageCard(props) {
1226
1261
  // Запасной путь: слот объявлен пакетом настроек ядра. Если в сборке его
1227
1262
  // нет, регистрация бросает — тогда остаётся прежний раздел в боковом
1228
1263
  // списке, чтобы настройки не пропали.
1229
- let placedInPluginsTab = false
1230
- try {
1231
- ctx.slots.register(
1232
- {
1233
- name: 'settings.plugin.item',
1234
- key: SETTINGS_NS,
1235
- locale: NS,
1236
- inject: () => cardOnce().inject(),
1237
- },
1238
- FalSettingsCard,
1239
- )
1240
- placedInPluginsTab = true
1241
- } catch (noPluginItemSlot) {
1242
- placedInPluginsTab = false
1243
- }
1244
- if (!placedInPluginsTab) {
1245
- ctx.slots.inject('settings.section', () =>
1246
- ctx.slots.register(
1247
- {
1248
- name: 'settings.section',
1249
- id: '@goodandready/dsh-image-gen',
1250
- order: 34,
1251
- locale: NS,
1252
- label: () => t('settings.navLabel'),
1253
- inject: () => cardOnce().inject(),
1254
- },
1255
- FalSettingsCard,
1256
- ),
1257
- )
1264
+ function registerFirst(ctx, candidates, component) {
1265
+ if (!ctx || !ctx.slots) return undefined
1266
+ for (const entry of candidates) {
1267
+ try {
1268
+ if (typeof ctx.slots?.has === 'function' && !ctx.slots.has(entry.name)) {
1269
+ continue
1270
+ }
1271
+ let ok = false
1272
+ ctx.slots.inject(entry.name, () => {
1273
+ ctx.slots.register(entry, component)
1274
+ ok = true
1275
+ })
1276
+ if (ok) return entry.name
1277
+ } catch {}
1258
1278
  }
1279
+ return undefined
1280
+ }
1281
+
1282
+ registerFirst(ctx, [
1283
+ {
1284
+ name: 'settings.plugin.item',
1285
+ key: SETTINGS_NS,
1286
+ locale: NS,
1287
+ inject: () => cardOnce().inject(),
1288
+ },
1289
+ {
1290
+ name: 'settings.section',
1291
+ id: '@goodandready/dsh-image-gen',
1292
+ order: 34,
1293
+ locale: NS,
1294
+ label: () => t('settings.navLabel'),
1295
+ inject: () => cardOnce().inject(),
1296
+ },
1297
+ ], FalSettingsCard)
1259
1298
  }
1260
1299
  exports.apply = apply
1261
1300
  exports.inject = inject
package/lib/index.js CHANGED
@@ -1070,14 +1070,25 @@ export function apply(ctx, config) {
1070
1070
  const filePath = path.join(outDir, name)
1071
1071
  await writeFile(filePath, res.bytes)
1072
1072
  const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
1073
- return {
1073
+ const summary = buildDualOutputMarkdown({
1074
+ action: 'background removed',
1075
+ filePath,
1076
+ width: res.width || attachment.width,
1077
+ height: res.height || attachment.height,
1078
+ mediaType: 'image/png',
1079
+ provider: 'fal',
1080
+ model: args.model || 'fal-ai/birefnet',
1081
+ attachmentId: attachment.attachmentId,
1082
+ })
1083
+ return toLosslessJson({
1084
+ summary,
1074
1085
  path: filePath,
1075
1086
  url: localUrl,
1076
1087
  width: res.width || attachment.width,
1077
1088
  height: res.height || attachment.height,
1078
1089
  format: 'png',
1079
1090
  attachment,
1080
- }
1091
+ })
1081
1092
  },
1082
1093
  }),
1083
1094
  )
@@ -1128,14 +1139,25 @@ export function apply(ctx, config) {
1128
1139
  const filePath = path.join(outDir, name)
1129
1140
  await writeFile(filePath, res.bytes)
1130
1141
  const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
1131
- return {
1142
+ const summary = buildDualOutputMarkdown({
1143
+ action: `upscaled (${args.scale || 2}x)`,
1144
+ filePath,
1145
+ width: res.width || attachment.width,
1146
+ height: res.height || attachment.height,
1147
+ mediaType: 'image/png',
1148
+ provider: 'fal',
1149
+ model: 'fal-ai/clarity-upscaler',
1150
+ attachmentId: attachment.attachmentId,
1151
+ })
1152
+ return toLosslessJson({
1153
+ summary,
1132
1154
  path: filePath,
1133
1155
  url: localUrl,
1134
1156
  scale: args.scale || 2,
1135
1157
  width: res.width || attachment.width,
1136
1158
  height: res.height || attachment.height,
1137
1159
  attachment,
1138
- }
1160
+ })
1139
1161
  },
1140
1162
  }),
1141
1163
  )
@@ -1173,11 +1195,13 @@ export function apply(ctx, config) {
1173
1195
  await mkdir(outDir, { recursive: true })
1174
1196
  const filePath = path.join(outDir, name)
1175
1197
  await writeFile(filePath, res.bytes)
1176
- return {
1198
+ const summary = `### Vectorized SVG Export\n- **File**: ${filePath}\n- **Format**: SVG (XML vector)\n- **Color mode**: ${args.color_mode || 'color'}\n- **Palette size**: ${res.palette ? res.palette.length : 16} colors\n`
1199
+ return toLosslessJson({
1200
+ summary,
1177
1201
  path: filePath,
1178
1202
  svg: res.svg,
1179
1203
  format: 'svg',
1180
- }
1204
+ })
1181
1205
  },
1182
1206
  }),
1183
1207
  )
@@ -1232,13 +1256,24 @@ export function apply(ctx, config) {
1232
1256
  const filePath = path.join(outDir, name)
1233
1257
  await writeFile(filePath, res.bytes)
1234
1258
  const localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
1235
- return {
1259
+ const summary = buildDualOutputMarkdown({
1260
+ action: 'blended composition',
1261
+ filePath,
1262
+ width: res.width || attachment.width,
1263
+ height: res.height || attachment.height,
1264
+ mediaType: 'image/png',
1265
+ provider: 'fal',
1266
+ model: 'fal-ai/image-blend',
1267
+ attachmentId: attachment.attachmentId,
1268
+ })
1269
+ return toLosslessJson({
1270
+ summary,
1236
1271
  path: filePath,
1237
1272
  url: localUrl,
1238
1273
  width: res.width || attachment.width,
1239
1274
  height: res.height || attachment.height,
1240
1275
  attachment,
1241
- }
1276
+ })
1242
1277
  },
1243
1278
  }),
1244
1279
  )
@@ -1610,12 +1645,13 @@ export function apply(ctx, config) {
1610
1645
  if (!colors || colors.length === 0) {
1611
1646
  if (args.image) {
1612
1647
  const source = await resolveSource(ctx, exec, args.image)
1613
- if (source && source.bytes) {
1614
- colors = extractSampleColorsFromBuffer(source.bytes)
1648
+ if (!source || !source.bytes) {
1649
+ throw new Error('Image not found or unreadable: ' + args.image)
1615
1650
  }
1651
+ colors = extractSampleColorsFromBuffer(source.bytes)
1616
1652
  }
1617
1653
  }
1618
- return extractDesignTokens(colors, { prefix: args.prefix })
1654
+ return toLosslessJson(extractDesignTokens(colors, { prefix: args.prefix }))
1619
1655
  },
1620
1656
  }),
1621
1657
  )
@@ -1648,12 +1684,13 @@ export function apply(ctx, config) {
1648
1684
  if (!colors || colors.length === 0) {
1649
1685
  if (args.image) {
1650
1686
  const source = await resolveSource(ctx, exec, args.image)
1651
- if (source && source.bytes) {
1652
- colors = extractSampleColorsFromBuffer(source.bytes)
1687
+ if (!source || !source.bytes) {
1688
+ throw new Error('Image not found or unreadable: ' + args.image)
1653
1689
  }
1690
+ colors = extractSampleColorsFromBuffer(source.bytes)
1654
1691
  }
1655
1692
  }
1656
- return generateCssGradient(colors, args.type || 'mesh')
1693
+ return toLosslessJson(generateCssGradient(colors, args.type || 'mesh'))
1657
1694
  },
1658
1695
  }),
1659
1696
  )
@@ -1689,12 +1726,13 @@ export function apply(ctx, config) {
1689
1726
  if (!colors || colors.length === 0) {
1690
1727
  if (args.image) {
1691
1728
  const source = await resolveSource(ctx, exec, args.image)
1692
- if (source && source.bytes) {
1693
- colors = extractSampleColorsFromBuffer(source.bytes)
1729
+ if (!source || !source.bytes) {
1730
+ throw new Error('Image not found or unreadable: ' + args.image)
1694
1731
  }
1732
+ colors = extractSampleColorsFromBuffer(source.bytes)
1695
1733
  }
1696
1734
  }
1697
- return checkWcagContrast(colors, args.text_color || '#ffffff')
1735
+ return toLosslessJson(checkWcagContrast(colors, args.text_color || '#ffffff'))
1698
1736
  },
1699
1737
  }),
1700
1738
  )
@@ -1769,7 +1807,7 @@ export function apply(ctx, config) {
1769
1807
  },
1770
1808
  },
1771
1809
  async execute(args, exec) {
1772
- return generatePwaIconSuite({
1810
+ const res = await generatePwaIconSuite({
1773
1811
  name: args.name || 'App',
1774
1812
  shortName: args.short_name || args.name || 'App',
1775
1813
  themeColor: args.theme_color || '#0f172a',
@@ -1777,6 +1815,7 @@ export function apply(ctx, config) {
1777
1815
  iconsDir: args.icons_dir || 'icons',
1778
1816
  sourcePath: args.image || null,
1779
1817
  })
1818
+ return toLosslessJson(res)
1780
1819
  },
1781
1820
  }),
1782
1821
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.7",
3
+ "version": "0.10.10",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",