kingkont 0.10.9 → 0.11.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.10.9",
3
+ "version": "0.11.1",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/chat.js CHANGED
@@ -182,16 +182,35 @@
182
182
  },
183
183
 
184
184
  generate_node: {
185
- description: 'Запустить генерацию для draft-ноды (image/video/audio). Возвращает сразугенерация идёт в фоне.',
185
+ description: 'Запустить генерацию для draft-ноды (image/video/audio/text). Стартует напрямую в фоне БЕЗ показа диалога не интерактивная UI-форма как regenerateNode.',
186
186
  params: '{"id":"<node-id>"}',
187
187
  async handler({ id }) {
188
188
  const b = state.currentBoard;
189
189
  if (!b) throw new Error('доска не выбрана');
190
190
  const node = b.metadata.nodes.find(n => n.id === id);
191
191
  if (!node) throw new Error(`нода не найдена: ${id}`);
192
- if (typeof regenerateNode !== 'function') throw new Error('regenerateNode недоступен');
193
- regenerateNode(node);
194
- return { ok: true, note: 'генерация запущена в фоне' };
192
+ const prompt = node.generated?.prompt || node.generated?.rawPrompt;
193
+ if (!prompt) throw new Error('у ноды нет промпта');
194
+ const kind = node.type;
195
+ const refs = (node.generated?.refs || []).map(r => ({ name: r.name, type: r.type, file: r.file }));
196
+ const modelKey = node.generated?.modelKey;
197
+ const boardHandle = b.handle;
198
+ const bKey = b.key;
199
+ // Маршрутизация по kind — копия логики из resumeJob, чтобы запустить
200
+ // job напрямую без gen-modal'а.
201
+ if (kind === 'audio') {
202
+ if (typeof runTTSJob !== 'function') throw new Error('runTTSJob недоступен');
203
+ runTTSJob(node, prompt, boardHandle, bKey, node.generated?.voiceId).catch(e => console.error('TTS job failed:', e));
204
+ } else if (kind === 'text') {
205
+ if (typeof runTextJob !== 'function') throw new Error('runTextJob недоступен');
206
+ const imageRefs = refs.filter(r => r.type === 'image' && r.file);
207
+ runTextJob(node, prompt, modelKey || 'anthropic/claude-sonnet-4', boardHandle, bKey, imageRefs).catch(e => console.error('text job failed:', e));
208
+ } else {
209
+ // image / video — через KIE/Chatium.
210
+ if (typeof startGenerationJob !== 'function') throw new Error('startGenerationJob недоступен');
211
+ startGenerationJob(node, kind, prompt, refs, boardHandle, bKey, modelKey).catch(e => console.error('gen job failed:', e));
212
+ }
213
+ return { ok: true, note: 'генерация стартовала в фоне (без gen-modal)' };
195
214
  },
196
215
  },
197
216
  };
@@ -214,10 +233,12 @@
214
233
  }
215
234
  lines.push('');
216
235
  lines.push('Правила:');
217
- lines.push('- Не выдумывай id нод — сначала read_scene/list_scenes.');
218
- lines.push('- Не выдумывай имена сценlist_scenes покажет реальные.');
219
- lines.push('- Если нужен новый id для add_node оставь поле пустым, вернётся в результате.');
220
- lines.push('- Для генерации используй generate_node ПОСЛЕ add_node с promptом.');
236
+ lines.push('- Не выдумывай id нод — id это случайные UUID, угадать нельзя.');
237
+ lines.push(' - Если нужен id существующей ноды сначала read_scene (вернёт массив с реальными id).');
238
+ lines.push(' - После add_node id новой ноды лежит в result.id ответа этого вызова.');
239
+ lines.push('- Не выдумывай имена сцен сначала list_scenes.');
240
+ lines.push('- Для генерации сразу после add_node — ВОЗЬМИ id из result.id ПРЕДЫДУЩЕГО вызова и передай в generate_node.');
241
+ lines.push('- Когда нужно создать несколько нод сразу — выдавай add_node + generate_node чередуя, или сначала все add_node, потом все generate_node (используя id из их result-ов).');
221
242
  lines.push('- Отвечай по-русски, кратко. Объясняй что делаешь, без лишней воды.');
222
243
  lines.push('');
223
244
  lines.push('ВАЖНО: НИКОГДА не пиши <tool_result>...</tool_result> сам — это формат который Я');
@@ -572,40 +572,41 @@ async function writeProjectMeta(filmHandle, meta) {
572
572
  }
573
573
 
574
574
  // =================== Open project template ===================
575
- // Просим юзера выбрать parent-папку. Создаём в ней новую папку с именем
576
- // проекта. Скачиваем все boards со всеми files. Открываем filmHandle.
575
+ // Открываем шаблон проекта как НОВЫЙ облачный проект:
576
+ // 1) GET /api/templates/<id> — получаем manifest + files (cdnUrls)
577
+ // 2) POST /api/projects — создаём cloud-проект, передавая
578
+ // manifest+files КАК ЕСТЬ (файлы уже на CDN, не перезаливаем).
579
+ // 3) cloudProjects.open(newId, name) — синкаемся локально в
580
+ // userData/cloud-projects/<id>/ через стандартный flow.
581
+ //
582
+ // Юзер НЕ выбирает папку — облачный проект автоматически попадает в
583
+ // userData. Без логина в KingKont — ошибка (нет куда class).
577
584
  async function openProjectTemplate(templateId, suggestedName) {
578
585
  if (_openProjectTplInFlight) return;
579
- if (typeof window.showDirectoryPicker !== 'function') {
580
- alert('Браузер не поддерживает выбор папки. Открой проект через File-System-Access.');
581
- return;
582
- }
583
-
584
586
  _openProjectTplInFlight = true;
585
- // 1. Парент-папка от юзера.
586
- let parentHandle;
587
+
587
588
  try {
588
- parentHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
589
- } catch (e) {
590
- _openProjectTplInFlight = false;
591
- if (e.name === 'AbortError') return; // юзер закрыл диалог
592
- alert('Не удалось выбрать папку: ' + e.message);
593
- return;
594
- }
589
+ // 0. Проверяем логин (нужен для POST /api/projects).
590
+ let settings = {};
591
+ try { settings = await window.appSettings?.get(); } catch {}
592
+ if (!(settings?.useChatium && settings?.chatium?.token)) {
593
+ alert('Войдите в KingKont, чтобы развернуть шаблон как облачный проект.');
594
+ return;
595
+ }
595
596
 
596
- // 2. Имя нового проекта.
597
- const projectName = await askName(
598
- 'Имя нового проекта:',
599
- suggestedName || 'Из шаблона',
600
- suggestedName || '',
601
- { okText: 'Создать и открыть' },
602
- );
603
- if (!projectName) { _openProjectTplInFlight = false; return; }
597
+ // 1. Имя нового проекта.
598
+ const projectName = await askName(
599
+ 'Имя нового проекта:',
600
+ suggestedName || 'Из шаблона',
601
+ suggestedName || '',
602
+ { okText: 'Создать в облаке' },
603
+ );
604
+ if (!projectName) return;
604
605
 
605
- TPL_PROGRESS.show('Получение шаблона…');
606
- tplStatus('');
606
+ TPL_PROGRESS.show('Получение шаблона…');
607
+ tplStatus('');
607
608
 
608
- try {
609
+ // 2. Получаем шаблон.
609
610
  const r = await fetch(`/api/templates/${encodeURIComponent(templateId)}`);
610
611
  if (!r.ok) {
611
612
  const err = await r.json().catch(() => ({}));
@@ -615,88 +616,56 @@ async function openProjectTemplate(templateId, suggestedName) {
615
616
  const boards = tpl.manifest?.boards || [];
616
617
  if (!boards.length) throw new Error('В шаблоне нет board\'ов');
617
618
 
618
- // 3. Создаём корень проекта.
619
- const filmHandle = await parentHandle.getDirectoryHandle(projectName, { create: true });
620
-
621
- // 4. Считаем общее число файлов для прогресса.
622
- let totalFiles = 0;
623
- for (const b of boards) totalFiles += Object.keys(b.files || {}).length;
624
- let downloadedTotal = 0;
625
-
626
- // 5. Для каждого board'а создаём папку, качаем файлы, пишем scene.json.
627
- // По ходу собираем fileHashes для meta — `<kind>/<board>/<relPath>` → cdnUrl.
628
- // (size/mtime читаем из созданного файла после write — нужно для dedup.)
629
- const fileHashes = {};
630
- for (const b of boards) {
631
- let parent = filmHandle;
632
- if (b.kind === 'character') {
633
- parent = await filmHandle.getDirectoryHandle(CHAR_DIR, { create: true });
634
- } else if (b.kind === 'location') {
635
- parent = await filmHandle.getDirectoryHandle(LOC_DIR, { create: true });
619
+ // 3. Перепаковываем boards в формат cloud-projects manifest:
620
+ // шаблон: { kind, name, manifest, files: {relPath: cdnUrl} }
621
+ // cloud: { kind, name, scene, files: {relPath: cdnUrl} }
622
+ const cloudBoards = boards.map(b => ({
623
+ kind: b.kind, name: b.name,
624
+ scene: b.manifest || {},
625
+ files: b.files || {},
626
+ }));
627
+ // Плоский files indexsum по boards.
628
+ const allFiles = {};
629
+ for (const b of cloudBoards) {
630
+ for (const [relPath, url] of Object.entries(b.files || {})) {
631
+ allFiles[`${b.kind}/${b.name}/${relPath}`] = url;
636
632
  }
637
- const boardHandle = await parent.getDirectoryHandle(b.name, { create: true });
638
-
639
- const fileEntries = Object.entries(b.files || {});
640
- for (const [relPath, cdnUrl] of fileEntries) {
641
- TPL_PROGRESS.update(downloadedTotal, totalFiles,
642
- `[${b.kind}/${b.name}] ${relPath} (${downloadedTotal + 1}/${totalFiles})`);
643
- const proxyUrl = '/api/proxy?url=' + encodeURIComponent(cdnUrl);
644
- const fr = await fetch(proxyUrl);
645
- if (!fr.ok) throw new Error(`download ${b.name}/${relPath}: HTTP ${fr.status}`);
646
- const buf = await fr.arrayBuffer();
647
- await writeBoardFile(boardHandle, relPath, new Uint8Array(buf));
648
- // Запомним size+mtime для dedup'а при следующем save.
649
- try {
650
- const parts = relPath.split('/');
651
- let dh = boardHandle;
652
- for (let i = 0; i < parts.length - 1; i++) dh = await dh.getDirectoryHandle(parts[i]);
653
- const fh = await dh.getFileHandle(parts[parts.length - 1]);
654
- const f = await fh.getFile();
655
- const key = `${b.kind}/${b.name}/${relPath}`;
656
- fileHashes[key] = { url: cdnUrl, size: f.size, mtime: f.lastModified };
657
- } catch {}
658
- downloadedTotal++;
659
- }
660
- // scene.json — manifest board'а.
661
- await writeBoardFile(boardHandle, 'scene.json', JSON.stringify(b.manifest || {}, null, 2));
662
633
  }
663
634
 
664
- // Meta: запоминаем шаблон-источник + размеры/мтайм всех файлов для dedup'а.
665
- await writeProjectMeta(filmHandle, {
666
- version: 1,
667
- templateId: tpl.id,
668
- templateName: tpl.name || '',
669
- downloadedAt: Date.now(),
670
- fileHashes,
635
+ // 4. Создаём cloud-проект на сервере (без re-upload файлы уже на CDN).
636
+ TPL_PROGRESS.update(0, 1, 'Создание облачного проекта…');
637
+ const cR = await fetch('/api/projects', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({
641
+ name: projectName,
642
+ manifest: { boards: cloudBoards },
643
+ files: allFiles,
644
+ coverUrl: tpl.manifest?.coverUrl || tpl.coverUrl || '',
645
+ }),
671
646
  });
647
+ if (!cR.ok) {
648
+ const err = await cR.json().catch(() => ({}));
649
+ throw new Error('Не удалось создать облачный проект: ' + (err.error || cR.status));
650
+ }
651
+ const created = await cR.json();
672
652
 
673
- // 6. Открываем созданный проект.
674
- TPL_PROGRESS.update(totalFiles, totalFiles, 'Открытие проекта…');
675
- // Чистим stale lastBoard:<name> — иначе если у юзера раньше был
676
- // проект с таким же именем, openFilm попытается auto-select board'а
677
- // которого нет в свежей структуре, что иногда блокирует загрузку.
678
- try { localStorage.removeItem(`lastBoard:${projectName}`); } catch {}
679
- // Закрываем модалку ДО openFilm — иначе её прогресс-бар остаётся
680
- // на экране пока юзер кликает в sidebar и блокирует UX.
653
+ // 5. Закрываем templates-modal и его прогресс — дальше синк ведёт
654
+ // cloudProjects.open() со своим прогрессом (download файлов в
655
+ // userData/cloud-projects/<id>/).
681
656
  TPL_PROGRESS.hide();
682
- document.getElementById('templatesModal').classList.add('hidden');
657
+ document.getElementById('templatesModal')?.classList.add('hidden');
683
658
  tplStatus('');
684
659
 
685
- await openFilm(filmHandle);
686
-
687
- // Auto-select первого board'а в новом проекте — без этого юзер видит
688
- // пустой холст и может думать что данные не загрузились.
689
- if (boards.length) {
690
- const first = boards[0];
691
- const list = first.kind === 'character' ? await listCharacters(filmHandle)
692
- : first.kind === 'location' ? await listLocations(filmHandle)
693
- : await listEpisodes(filmHandle);
694
- const found = list.find(x => x.name === first.name);
695
- if (found) await selectBoard({ kind: first.kind, ...found });
660
+ if (window.cloudProjects?.open) {
661
+ await window.cloudProjects.open(created.id, created.name || projectName);
662
+ } else {
663
+ throw new Error('cloudProjects.open недоступен');
696
664
  }
697
665
  } catch (e) {
698
666
  TPL_PROGRESS.hide();
699
667
  tplStatus('Ошибка открытия проекта: ' + e.message, true);
668
+ alert('Ошибка: ' + (e?.message || e));
700
669
  console.error('open project template failed', e);
701
670
  } finally {
702
671
  _openProjectTplInFlight = false;