@goodandready/dsh-image-gen 0.8.5 → 0.8.6

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/lib/client.js CHANGED
@@ -99,7 +99,7 @@ window.__ModuleLoader__.load({
99
99
  function PluginSettingsCard(props) {
100
100
  // Карточка живёт в собственном разделе настроек и на странице одна, так
101
101
  // что открыта сразу: свёрнутая выглядела бы как пустая страница.
102
- const [open, setOpen] = react.useState(true)
102
+ const [open, setOpen] = react.useState(false)
103
103
  const { state } = props
104
104
  // Раньше здесь было `return null`, и раздел оказывался пустым, если хост
105
105
  // ещё не объявил настройки. Пустая страница ничего не объясняет — лучше
@@ -407,6 +407,16 @@ window.__ModuleLoader__.load({
407
407
 
408
408
  // ------------------------------------------------------------- fields
409
409
  const IMAGE_SIZES = ['square_hd', 'square', 'portrait_4_3', 'portrait_16_9', 'landscape_4_3', 'landscape_16_9']
410
+ // Перевод именованного размера в пиксели для подсказки в карточке
411
+ // (значения совпадают с SIZE_PIXELS в хостовой половине).
412
+ const SIZE_PIXELS = {
413
+ square_hd: '1024x1024',
414
+ square: '512x512',
415
+ portrait_4_3: '768x1024',
416
+ portrait_16_9: '576x1024',
417
+ landscape_4_3: '1024x768',
418
+ landscape_16_9: '1024x576',
419
+ }
410
420
  const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
411
421
  const DELIVERY_MODES = ['link', 'image']
412
422
  const PROVIDERS = ['fal', 'custom', 'codex', 'grok']
@@ -417,7 +427,7 @@ window.__ModuleLoader__.load({
417
427
  { field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model' },
418
428
  { field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv' },
419
429
  { field: 'baseURL', spec: textField('baseURL'), kind: 'text', labelKey: 'f.baseURL', hintKey: 'f.baseURLHint', placeholderKey: 'p.baseURL' },
420
- { field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint' },
430
+ { field: 'defaultSize', spec: selectField('defaultSize', IMAGE_SIZES), kind: 'select', options: IMAGE_SIZES, labelKey: 'f.defaultSize', hintKey: 'f.defaultSizeHint', sizePixels: SIZE_PIXELS },
421
431
  { field: 'defaultFormat', spec: selectField('defaultFormat', OUTPUT_FORMATS), kind: 'select', options: OUTPUT_FORMATS, labelKey: 'f.defaultFormat', hintKey: 'f.defaultFormatHint' },
422
432
  { field: 'pollIntervalMs', spec: numberField('pollIntervalMs'), kind: 'number', labelKey: 'f.pollIntervalMs', hintKey: 'f.pollIntervalMsHint', placeholderKey: 'p.pollIntervalMs' },
423
433
  { field: 'timeoutMs', spec: numberField('timeoutMs'), kind: 'number', labelKey: 'f.timeoutMs', hintKey: 'f.timeoutMsHint', placeholderKey: 'p.timeoutMs' },
@@ -446,7 +456,9 @@ window.__ModuleLoader__.load({
446
456
  onReset,
447
457
  }
448
458
  if (entry.kind === 'select') {
449
- return react_jsx_runtime.jsx(SelectField, { ...base, options: entry.options, inheritLabel: t('f.inherit') })
459
+ const px = entry.sizePixels && entry.sizePixels[state.text] ? ` — ${entry.sizePixels[state.text]}` : ''
460
+ const hint = base.hint + px
461
+ return react_jsx_runtime.jsx(SelectField, { ...base, hint, options: entry.options, inheritLabel: t('f.inherit') })
450
462
  }
451
463
  return react_jsx_runtime.jsx(ValueField, { ...base, numeric: entry.kind === 'number', placeholder: entry.placeholderKey ? t(entry.placeholderKey) : '' })
452
464
  }
@@ -511,7 +523,7 @@ window.__ModuleLoader__.load({
511
523
  /** Settings namespace the card edits (the Host plugin registers it). */
512
524
  const SETTINGS_NS = 'dsh-image-gen'
513
525
  /** Required client services. */
514
- const inject = ['slots', 'settingsScope', 'locale']
526
+ const inject = ['slots', 'settingsScope', 'locale', 'sessions']
515
527
 
516
528
  const en = {
517
529
  'settings.navLabel': 'Image generation',
@@ -636,7 +648,7 @@ window.__ModuleLoader__.load({
636
648
  // Карточки инструментов не рисуют image-блоки (это умеют только сообщения
637
649
  // ассистента), поэтому сгенерированная картинка показывается здесь —
638
650
  // keyed-запись слота tool.call.toolview по имени инструмента.
639
- const CARD_CSS = '.fal-card{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);max-width:520px}'
651
+ const CARD_CSS = '.fal-card{display:flex;flex-direction:column;gap:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);max-width:520px}' + '.fal-repeat{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:5px 14px;font-size:13px}' + '.fal-repeat:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}' + '.fal-repeat:disabled{opacity:.5;cursor:default}'
640
652
  + '.fal-card .fal-head{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}'
641
653
  + '.fal-card img{max-width:100%;border-radius:8px;display:block}'
642
654
  + '.fal-card .fal-prompt{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}'
@@ -697,6 +709,37 @@ window.__ModuleLoader__.load({
697
709
  const head = react.createElement('div', { className: 'fal-head' },
698
710
  running ? 'Генерирую изображение…' : failed ? 'Генерация не удалась' : 'Изображение')
699
711
 
712
+ // «Повторить генерацию»: отправляет в чат новое сообщение с теми же
713
+ // промптом и параметрами, извлечёнными из исходного вызова инструмента.
714
+ // Дальше модель сама решает, как сгенерировать, — это просто повторный
715
+ // запрос, а не вызов инструмента из карточки.
716
+ let args = {}
717
+ try {
718
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
719
+ if (raw) args = JSON.parse(raw)
720
+ } catch (e) { /* ignore */ }
721
+ const repeatText = (() => {
722
+ const parts = [String(args.prompt || '').trim()]
723
+ const size = args.image_size || args.size
724
+ const seed = args.seed
725
+ if (size) parts.push(`Размер: ${size}`)
726
+ if (seed !== undefined) parts.push(`Сид: ${seed}`)
727
+ return parts.filter(Boolean).join(', ')
728
+ })()
729
+ const repeat = async () => {
730
+ try {
731
+ const sessions = props.sessions
732
+ const current = sessions && sessions.list && sessions.list.current
733
+ const binding = current && sessions.binding(current)
734
+ const session = binding && binding.session
735
+ if (!session || !session.prompt) {
736
+ return
737
+ }
738
+ const text = repeatText || String(args.prompt || '')
739
+ await session.prompt([{ type: 'text', text }], 'queue')
740
+ } catch (e) { /* контур сам покажет ошибку отправки */ }
741
+ }
742
+
700
743
  const body = []
701
744
  if (prompt) body.push(react.createElement('div', { className: 'fal-prompt', key: 'p' }, prompt))
702
745
  if (failed) {
@@ -729,6 +772,27 @@ window.__ModuleLoader__.load({
729
772
  body.push(react.createElement('div', { className: 'fal-meta', key: 'm' }, parsed.text))
730
773
  }
731
774
 
775
+ // Диагностика: метаданные генерации (prompt/seed/size/format), чтобы было
776
+ // понятно, по каким параметрам получилась именно эта картинка.
777
+ const metaParts = []
778
+ if (args.image_size || args.size) metaParts.push(`size ${args.image_size || args.size}`)
779
+ if (args.output_format || args.format) metaParts.push(`format ${args.output_format || args.format}`)
780
+ if (args.seed !== undefined) metaParts.push(`seed ${args.seed}`)
781
+ const meta = metaParts.join(' · ')
782
+
783
+ // Кнопка повтора — только когда генерация завершена (не в процессе и не ошибка).
784
+ if (!running && !failed && meta) {
785
+ body.push(react.createElement('div', { key: 'meta', className: 'fal-meta' }, meta))
786
+ }
787
+ if (!running && !failed && repeatText) {
788
+ body.push(react.createElement('button', {
789
+ key: 'repeat',
790
+ type: 'button',
791
+ className: 'fal-repeat',
792
+ onClick: repeat,
793
+ }, 'Повторить генерацию'))
794
+ }
795
+
732
796
  return react.createElement('div', { className: 'fal-card' }, head, body)
733
797
  }
734
798
 
@@ -810,7 +874,7 @@ window.__ModuleLoader__.load({
810
874
  }
811
875
  ctx.slots.inject('tool.call.toolview', () =>
812
876
  ctx.slots.register(
813
- { name: 'tool.call.toolview', key: 'generate_image', locale: NS },
877
+ { name: 'tool.call.toolview', key: 'generate_image', locale: NS, inject: () => ({ sessions: ctx.sessions }) },
814
878
  FalImageCard,
815
879
  ),
816
880
  )
package/lib/index.js CHANGED
@@ -24,6 +24,8 @@ import {
24
24
  buildSidecar,
25
25
  makeProviders,
26
26
  normalizeCount,
27
+ tryGenerate,
28
+ fallbackOrder,
27
29
  normalizeMediaType,
28
30
  } from './providers.js'
29
31
 
@@ -276,6 +278,14 @@ export function apply(ctx, config) {
276
278
  type: 'integer',
277
279
  description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
278
280
  },
281
+ negative_prompt: {
282
+ type: 'string',
283
+ description: 'Optional text describing what NOT to draw; sent to providers that support it (ignored otherwise).',
284
+ },
285
+ guidance_scale: {
286
+ type: 'number',
287
+ description: 'Optional prompt adherence (e.g. 1-20); sent to providers that support it (ignored otherwise).',
288
+ },
279
289
  },
280
290
  output: {
281
291
  schema: {
@@ -353,14 +363,14 @@ export function apply(ctx, config) {
353
363
 
354
364
  const providers = makeProviders(
355
365
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
356
- { prompt: args.prompt, size, format, seed: args.seed, signal: exec.signal },
366
+ { prompt: args.prompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: args.negative_prompt, guidanceScale: args.guidance_scale },
357
367
  )
358
368
  // Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
359
369
  // отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
360
370
  // байтами, и пользователь получал битую карточку вместо внятного отказа.
361
371
  const guard = (generated) => { if (generated && generated.ok === false) throw new Error(generated.reason) }
362
- const one = async (jobSeed) => {
363
- const gen = await providers[provider](jobSeed)
372
+ const one = async (jobSeed, providerKey) => {
373
+ const gen = await providers[providerKey](jobSeed)
364
374
  guard(gen)
365
375
  const bytes = gen.bytes
366
376
  const mediaType = gen.mediaType
@@ -426,9 +436,13 @@ export function apply(ctx, config) {
426
436
 
427
437
  const count = normalizeCount(args.count)
428
438
  const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
439
+ // Fallback-цепочка: пробуем текущий провайдер, при отказе — следующий
440
+ // по порядку (fal → custom → codex → grok), собирая причины отказов.
441
+ const order = fallbackOrder(provider)
442
+ const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [k, (s) => one(s, k)]))
429
443
  const images = []
430
444
  for (let i = 0; i < count; i += 1) {
431
- images.push(await one(seedBase + i))
445
+ images.push(await tryGenerate(generators, order, seedBase + i))
432
446
  }
433
447
  const first = images[0]
434
448
  return { ...first, images }
package/lib/providers.js CHANGED
@@ -10,6 +10,30 @@
10
10
  export const PROVIDER_KEYS = ['fal', 'custom', 'codex', 'grok']
11
11
 
12
12
  /** Привести count из аргумента инструмента к диапазону 1..4. */
13
+ /** Порядок провайдеров для fallback: основной первым, остальные по PROVIDER_KEYS. */
14
+ export function fallbackOrder(primary) {
15
+ return [primary, ...PROVIDER_KEYS.filter((k) => k !== primary)]
16
+ }
17
+
18
+ /**
19
+ * Перебирает генераторов по порядку, возвращает первый успешный результат.
20
+ * При отказе всех бросает с перечислением причин.
21
+ * @param generators - массив функций (key, seed) => Promise<generated>.
22
+ * @param order - порядок ключей, длина = generators.length.
23
+ */
24
+ export async function tryGenerate(generators, order, seed) {
25
+ const refusals = []
26
+ for (const key of order) {
27
+ try {
28
+ const produced = await generators[key](seed)
29
+ return produced
30
+ } catch (e) {
31
+ refusals.push(`${key}: ${e.message || String(e)}`)
32
+ }
33
+ }
34
+ throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
35
+ }
36
+
13
37
  export function normalizeCount(count) {
14
38
  const n = Math.floor(Number(count))
15
39
  if (!Number.isFinite(n)) return 1
@@ -135,12 +159,14 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
135
159
  */
136
160
  export function makeProviders(deps, job) {
137
161
  const { fetchImpl, resolveKey, cfg } = deps
138
- const { prompt, size, format, seed, signal } = job
162
+ const { prompt, size, format, seed, signal, negativePrompt, guidanceScale } = job
139
163
 
140
164
  async function fal(seedArg = seed) {
141
165
  const key = await resolveKey(cfg.apiKeyEnv)
142
166
  const body = { prompt, image_size: size, num_images: 1 }
143
167
  if (seedArg !== undefined) body.seed = seedArg
168
+ if (negativePrompt !== undefined) body.negative_prompt = negativePrompt
169
+ if (guidanceScale !== undefined) body.guidance_scale = guidanceScale
144
170
  if (format !== 'png') body.output_format = format
145
171
 
146
172
  const submit = await submitJob(fetchImpl, cfg.baseURL, cfg.model, key, body, signal)
@@ -192,6 +218,8 @@ export function makeProviders(deps, job) {
192
218
  prompt,
193
219
  n: 1,
194
220
  size: cfg.customSize || SIZE_PIXELS[size] || size,
221
+ ...(negativePrompt !== undefined ? { negative_prompt: negativePrompt } : {}),
222
+ ...(guidanceScale !== undefined ? { guidance_scale: guidanceScale } : {}),
195
223
  }),
196
224
  signal,
197
225
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
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",