kingkont 0.18.0 → 0.18.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -475,7 +475,13 @@ $('textGenSubmit').addEventListener('click', async () => {
475
475
  type: 'text', file: `texts/${mdName}`, text: '',
476
476
  x: spot.x, y: spot.y,
477
477
  status: 'generating',
478
- generated: { kind: 'text', rawPrompt, prompt: resolvedPrompt, model, state: 'submitting' },
478
+ generated: {
479
+ kind: 'text', rawPrompt, prompt: resolvedPrompt, model, state: 'submitting',
480
+ // refs — для resume после reload. boardHandle не сериализуется,
481
+ // на resume re-resolve через state.currentBoard.handle (тот же
482
+ // board — мы только что в нём создали ноду).
483
+ refs: imageRefs.map(r => ({ name: r.name, key: r.key, type: r.type, file: r.file })),
484
+ },
479
485
  };
480
486
  state.currentBoard.metadata.nodes.push(node);
481
487
  canvas.appendChild(await createNodeEl(node));
@@ -487,30 +493,57 @@ $('textGenSubmit').addEventListener('click', async () => {
487
493
  });
488
494
 
489
495
  async function _imageRefToDataUrl(ref) {
496
+ // Возвращает {url, size, mime} или {error}. Раньше тихо возвращал null
497
+ // на любую ошибку → дебаг был невозможен (юзер видел «картинка не
498
+ // подаётся», но в логах ничего).
490
499
  try {
500
+ if (!ref || !ref.file) return { error: 'no file in ref' };
501
+ if (!ref.boardHandle || typeof ref.boardHandle.getDirectoryHandle !== 'function') {
502
+ return { error: 'no boardHandle in ref' };
503
+ }
491
504
  const fh = await resolveBoardFile(ref.boardHandle, ref.file);
492
505
  const file = await fh.getFile();
493
- return await new Promise((res, rej) => {
506
+ const url = await new Promise((res, rej) => {
494
507
  const r = new FileReader();
495
508
  r.onload = () => res(r.result);
496
509
  r.onerror = () => rej(r.error);
497
510
  r.readAsDataURL(file);
498
511
  });
499
- } catch (e) { return null; }
512
+ return { url, size: file.size, mime: file.type || 'image/*' };
513
+ } catch (e) {
514
+ return { error: e?.message || String(e) };
515
+ }
500
516
  }
501
517
 
502
518
  async function runTextJob(node, prompt, model, boardHandle, bKey, imageRefs) {
503
519
  state.jobs.set(node.id, { boardKey: bKey, kind: 'text', nodeId: node.id });
504
520
  if (typeof updateJobsBadge === 'function') updateJobsBadge();
505
- logJob(node.id, `text-gen start model=${model} prompt="${(prompt||'').slice(0,80)}" images=${imageRefs?.length||0}`);
521
+ logJob(node.id, `text-gen start model=${model} prompt="${(prompt||'').slice(0,200)}" images=${imageRefs?.length||0}`);
522
+ // Дамп refs ДО загрузки — видно сразу что именно собирается прикрутить
523
+ // к запросу (имя, файл, доска). Если refs.length=0 при @-mention в
524
+ // промпте — значит gatherMediaRefs не нашёл (имя не совпало / нода
525
+ // другого типа). Если refs есть, но загрузка падает — следующий лог
526
+ // покажет error по каждому.
527
+ if (imageRefs?.length) {
528
+ logJob(node.id, `image refs (${imageRefs.length}): ${imageRefs.map((r, i) => `[${i+1}] @${r.key||r.name} file=${r.file} board=${r.boardHandle?.name||'—'}`).join(' | ')}`);
529
+ } else {
530
+ logJob(node.id, 'image refs: НЕТ — модель получит только текст. Если ожидалась картинка, проверь @-mention (имя ноды должно совпадать) и тип ноды.');
531
+ }
506
532
  try {
507
533
  const images = [];
508
- for (const ref of (imageRefs || [])) {
509
- const url = await _imageRefToDataUrl(ref);
510
- if (url) images.push({ name: ref.name, url });
534
+ for (let i = 0; i < (imageRefs || []).length; i++) {
535
+ const ref = imageRefs[i];
536
+ const result = await _imageRefToDataUrl(ref);
537
+ if (result.url) {
538
+ images.push({ name: ref.name, url: result.url });
539
+ const sizeKb = (result.size / 1024).toFixed(1);
540
+ logJob(node.id, ` ✓ image [${i+1}] @${ref.key||ref.name}: ${result.mime}, ${sizeKb}KB → [image ${images.length}] в промпте`);
541
+ } else {
542
+ logJob(node.id, ` ✗ image [${i+1}] @${ref.key||ref.name} НЕ ЗАГРУЖЕНА: ${result.error || 'unknown'} (file=${ref.file})`);
543
+ }
511
544
  }
512
545
  const provider = await plannedProvider('text');
513
- logJob(node.id, `→ POST /api/text → ${provider} (model=${model})`);
546
+ logJob(node.id, `→ POST /api/text → ${provider} (model=${model}, images.length=${images.length}, prompt.length=${(prompt||'').length}ch)`);
514
547
  const r = await fetch('/api/text', {
515
548
  method: 'POST',
516
549
  headers: { 'Content-Type': 'application/json' },
@@ -1923,7 +1956,12 @@ async function resumeJob(node, bKey, boardHandle) {
1923
1956
  await runTTSJob(node, node.generated.prompt, boardHandle, bKey, node.generated.voiceId);
1924
1957
  }
1925
1958
  } else if (kind === 'text') {
1926
- const imageRefs = (node.generated.refs || []).filter(r => r.type === 'image' && r.file);
1959
+ // Re-attach boardHandle к refs JSON не сохраняет JS-handle.
1960
+ // Все refs изначально с одной (текущей) доски (см. textGenSubmit),
1961
+ // так что подставляем boardHandle resume-доски.
1962
+ const imageRefs = (node.generated.refs || [])
1963
+ .filter(r => r.type === 'image' && r.file)
1964
+ .map(r => ({ ...r, boardHandle }));
1927
1965
  const model = node.generated.model || node.generated.modelKey || 'anthropic/claude-sonnet-4';
1928
1966
  await runTextJob(node, node.generated.prompt, model, boardHandle, bKey, imageRefs);
1929
1967
  } else {
@@ -125,9 +125,32 @@
125
125
  }
126
126
 
127
127
  // Public API.
128
+ // Дедуп: gen-completion идёт двумя путями (local pollJob → showToast,
129
+ // и server _startPoller → WS jobs:all). Без дедупа юзер видит ДВА
130
+ // одинаковых события с разницей в секунды. Ключ дедупа — kind+nodeId
131
+ // (если есть target.nodeId) или kind+text (fallback). Окно — 30 сек.
132
+ const DEDUP_WINDOW_MS = 30 * 1000;
133
+ function _dedupKey(kind, text, target) {
134
+ if (target?.nodeId && (kind === 'ok' || kind === 'error')) {
135
+ return `${kind}:${target.nodeId}`;
136
+ }
137
+ return null; // не дедупим chat / info-сообщения, у них нет nodeId
138
+ }
128
139
  function addEvent({ kind, text, raw, target }) {
129
140
  if (!text) return;
130
- events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw, target: target || null });
141
+ const k = kind || 'info';
142
+ const dk = _dedupKey(k, text, target);
143
+ if (dk) {
144
+ const cutoff = Date.now() - DEDUP_WINDOW_MS;
145
+ for (let i = events.length - 1; i >= 0; i--) {
146
+ if (events[i].ts < cutoff) break;
147
+ if (events[i]._dk === dk) {
148
+ // Уже было — игнорируем (первый источник победил).
149
+ return;
150
+ }
151
+ }
152
+ }
153
+ events.push({ ts: Date.now(), kind: k, text: String(text).slice(0, 300), raw, target: target || null, _dk: dk });
131
154
  if (events.length > MAX_EVENTS) events.shift();
132
155
  if (!panelOpen) {
133
156
  unread++;