kingkont 0.10.0 → 0.10.2

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.0",
3
+ "version": "0.10.2",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -213,8 +213,10 @@ async function renderWelcomeIdentity() {
213
213
  let status = null;
214
214
  try { status = await window.appChatium?.status?.(); } catch {}
215
215
  if (status?.connected) {
216
- // Имя: предпочитаем display-name (login/name из auth~me), fallback — userId.
217
- const name = status.login || status.name || status.userId || 'KingKont';
216
+ // Имя: предпочитаем email (наиболее узнаваемый), затем login/name,
217
+ // в крайнем случае userId. Subtitle = userId-обрезок если он
218
+ // отличается от показываемого имени.
219
+ const name = status.email || status.login || status.fullName || status.name || status.userId || 'KingKont';
218
220
  const sub = status.userId && status.userId !== name ? `· ${status.userId.slice(0, 8)}` : '';
219
221
  wrap.innerHTML = `
220
222
  <span style="color:#5c5; font-size:13px; line-height:1;">●</span>
@@ -949,6 +951,9 @@ async function closeProject() {
949
951
  // Помечаем что юзер вышел явно — на следующем старте autoload пропускается.
950
952
  // Cmd+R после close = welcome, а не реоткрытие.
951
953
  try { localStorage.setItem('welcomeOnNextStart', '1'); } catch {}
954
+ // Чат привязан к одному проекту — чистим историю при выходе чтобы
955
+ // не путал контекст следующего открытого проекта.
956
+ if (window.kingChat?.clear) window.kingChat.clear();
952
957
  stopExternalWatcher();
953
958
  // Сбрасываем UI таймлайна/превью — иначе при возврате через welcome
954
959
  // в новый проект остаётся фрейм/дорожки прошлого.
package/renderer/chat.js CHANGED
@@ -225,6 +225,7 @@
225
225
  // ============== TOOL-CALL PARSER ==============
226
226
  function parseToolCalls(text) {
227
227
  const out = [];
228
+ if (typeof text !== 'string' || !text) return out;
228
229
  const re = /<tool>\s*([\s\S]*?)\s*<\/tool>/g;
229
230
  let m;
230
231
  while ((m = re.exec(text)) !== null) {
@@ -238,6 +239,7 @@
238
239
  return out;
239
240
  }
240
241
  function stripToolCalls(text) {
242
+ if (typeof text !== 'string') return '';
241
243
  return text.replace(/<tool>[\s\S]*?<\/tool>/g, '').trim();
242
244
  }
243
245
 
@@ -293,29 +295,41 @@
293
295
 
294
296
  // ============== LLM call + tool-loop ==============
295
297
  async function callLLM(messages, system) {
298
+ const body = {
299
+ messages, system,
300
+ model: 'anthropic/claude-sonnet-4.6',
301
+ };
302
+ console.log('[chat] → POST /api/text', { messages: messages.length, hasSystem: !!system });
296
303
  const r = await fetch('/api/text', {
297
304
  method: 'POST',
298
305
  headers: { 'Content-Type': 'application/json' },
299
- body: JSON.stringify({
300
- messages, system,
301
- model: 'anthropic/claude-sonnet-4.6',
302
- }),
306
+ body: JSON.stringify(body),
303
307
  });
308
+ const respText = await r.text();
309
+ let d; try { d = JSON.parse(respText); } catch { d = { _raw: respText }; }
304
310
  if (!r.ok) {
305
- const err = await r.json().catch(() => ({}));
306
- throw new Error(err.error || `HTTP ${r.status}`);
311
+ console.error('[chat] /api/text failed', r.status, d);
312
+ throw new Error(d?.error || d?._raw?.slice(0, 200) || `HTTP ${r.status}`);
307
313
  }
308
- const d = await r.json();
309
- return d.text || '';
314
+ console.log('[chat] /api/text ok', { textLen: (d?.text || '').length });
315
+ return d?.text || '';
310
316
  }
311
317
 
312
- // Превращает internal history → массив для /api/text. tool_results
313
- // объединяем в user-message (модель видит их как ответы юзера на её запрос).
318
+ // Превращает internal history → массив для /api/text. Используем
319
+ // Anthropic-формат content-blocks (`[{type:'text', text:...}]`), потому
320
+ // что Chatium-side @start/sdk требует именно его — со строкой content
321
+ // падает «Cannot read properties of undefined (reading 'length')».
322
+ // OpenRouter этот формат тоже принимает (универсальный путь).
314
323
  function historyToMessages() {
315
324
  const out = [];
316
325
  for (const m of history) {
317
326
  if (m.role === 'system') continue;
318
- out.push({ role: m.role, content: m.content });
327
+ const content = String(m.content || '');
328
+ if (!content) continue; // Anthropic не любит пустые messages
329
+ out.push({
330
+ role: m.role,
331
+ content: [{ type: 'text', text: content }],
332
+ });
319
333
  }
320
334
  return out;
321
335
  }
@@ -196,7 +196,46 @@
196
196
  async function openCloudProject(projectId, suggestedName, opts = {}) {
197
197
  const s = await getSettings();
198
198
  if (!isLoggedIn(s)) { alert('Войдите в KingKont'); return; }
199
- // 1. Проверяем синхронизирован ли уже локально.
199
+
200
+ // 0. Если этот cloud-проект ранее был сохранён ИЗ папочного — открываем
201
+ // оригинальную папку (с уже скачанными файлами) и не качаем ничего.
202
+ // Mapping cloudFolder:<projectId> → folderName ставит saveCloudProject.
203
+ // Папка должна быть granted в FSAH (юзер раз дал — handle живёт в IDB).
204
+ if (!opts.forceRefresh) {
205
+ try {
206
+ const folderName = await idbGet(`cloudFolder:${projectId}`);
207
+ if (folderName) {
208
+ const folderHandle = await idbGet(`handle:${folderName}`);
209
+ if (folderHandle) {
210
+ // Verify permission still granted (без user-gesture не запрашиваем).
211
+ let g = 'prompt';
212
+ try { g = await folderHandle.queryPermission({ mode: 'readwrite' }); } catch {}
213
+ if (g === 'granted') {
214
+ // Дополнительно проверим что в папке лежит meta с тем же cloudProjectId
215
+ // (юзер мог перенести/изменить — тогда fallback на download).
216
+ try {
217
+ const metaFh = await folderHandle.getFileHandle('.kingkont-meta.json');
218
+ const metaTxt = await (await metaFh.getFile()).text();
219
+ const meta = JSON.parse(metaTxt);
220
+ if (meta.cloudProjectId === projectId) {
221
+ state.cloudProjectId = projectId;
222
+ state.cloudDirty = false;
223
+ touchCloudOpened(projectId);
224
+ await openFilm(folderHandle);
225
+ setCloudButtonsVisibility();
226
+ return;
227
+ }
228
+ } catch {}
229
+ // Permission granted но meta не совпала — обнуляем mapping
230
+ // (idbSet(null) — простой способ без отдельного idbDel).
231
+ try { await idbSet(`cloudFolder:${projectId}`, null); } catch {}
232
+ }
233
+ }
234
+ }
235
+ } catch {}
236
+ }
237
+
238
+ // 1. Проверяем синхронизирован ли уже в userData/cloud-projects/<id>/.
200
239
  if (window.cloudFs) {
201
240
  try {
202
241
  const metaText = await window.cloudFs.readText(projectId, '.kingkont-meta.json');
@@ -489,6 +528,22 @@
489
528
  });
490
529
  }
491
530
 
531
+ // 5) Если это ПАПОЧНЫЙ проект (не cloud-shim), запоминаем mapping
532
+ // cloudProjectId → folderName в IDB. Когда юзер позже откроет
533
+ // этот же cloud-проект из welcome, openCloudProject подхватит
534
+ // оригинальную папку (с уже скачанными файлами) и НЕ будет
535
+ // качать всё заново. Mapping живёт в той же IDB-схеме что
536
+ // handle:<name>: cloudFolder:<projectId> = name.
537
+ if (!window.cloudFsShim?.isCloudHandle?.(state.filmHandle)) {
538
+ try {
539
+ await idbSet(`cloudFolder:${projectId}`, state.filmHandle.name);
540
+ // Также запоминаем cloudProjectId на текущей сессии — чтобы
541
+ // следующий save без перезахода не ловил «папочный → новый cloud».
542
+ state.cloudProjectId = projectId;
543
+ if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
544
+ } catch {}
545
+ }
546
+
492
547
  PROGRESS.hide();
493
548
  markClean();
494
549
  const stats = reused
@@ -243,8 +243,8 @@
243
243
  .welcome-status-identity button:hover { background: rgba(255,255,255,0.06); color: #cde; }
244
244
  .welcome-status-balances {
245
245
  pointer-events: auto;
246
- display: flex; flex-direction: row; gap: 6px; flex-wrap: wrap;
247
- justify-content: flex-end;
246
+ display: flex; flex-direction: column; gap: 6px;
247
+ align-items: flex-end;
248
248
  }
249
249
  /* Используем те же .balance-info pill'ы что и в sidebar-footer'е. */
250
250