kingkont 0.18.8 → 0.18.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/index.html CHANGED
@@ -381,14 +381,14 @@
381
381
  <div class="modal hidden" id="genModal">
382
382
  <div class="modal-card">
383
383
  <h3 id="genTitle">Сгенерировать ноду</h3>
384
- <label style="display:none;">Тип
384
+ <div class="field-row" style="display:none;">Тип
385
385
  <div class="seg-control">
386
386
  <button class="seg active" data-kind="image" type="button">Картинка</button>
387
387
  <button class="seg" data-kind="video" type="button">Видео</button>
388
388
  <button class="seg" data-kind="audio" type="button">Голос</button>
389
389
  </div>
390
- </label>
391
- <label id="imageModelRow">Модель для картинки
390
+ </div>
391
+ <div class="field-row" id="imageModelRow">Модель для картинки
392
392
  <div class="seg-control">
393
393
  <button class="seg active" data-img-model="nano-banana-2" type="button" title="Высокое качество, медленно">Nano Banana 2</button>
394
394
  <button class="seg" data-img-model="nano-banana-pro" type="button" title="Pro-версия nano-banana, выше качество">Nano Banana Pro</button>
@@ -398,15 +398,15 @@
398
398
  <button class="seg" data-img-model="flux-schnell" type="button" title="Очень быстрая (3-5с)">⚡ Flux Schnell</button>
399
399
  <button class="seg" data-img-model="sdxl-lightning" type="button" title="Очень быстрая (3-5с)">⚡ SDXL Lightning</button>
400
400
  </div>
401
- </label>
402
- <label id="imageQualityRow">Качество
401
+ </div>
402
+ <div class="field-row" id="imageQualityRow">Качество
403
403
  <div class="seg-control" id="imageQualityCtl">
404
404
  <button class="seg" data-img-quality="low" type="button" title="Быстрее, дешевле">low</button>
405
405
  <button class="seg active" data-img-quality="medium" type="button">medium</button>
406
406
  <button class="seg" data-img-quality="high" type="button" title="Лучше детализация, медленнее">high</button>
407
407
  </div>
408
- </label>
409
- <label id="imageOptionsRow">Соотношение сторон
408
+ </div>
409
+ <div class="field-row" id="imageOptionsRow">Соотношение сторон
410
410
  <div class="seg-control" id="imageAspectCtl">
411
411
  <button class="seg" data-img-asp="source" type="button" id="imgAspSource"
412
412
  title="Взять формат у исходной картинки-референса"
@@ -419,16 +419,16 @@
419
419
  <button class="seg" data-img-asp="4:3" type="button" title="Не поддерживается Grok">4:3</button>
420
420
  <button class="seg" data-img-asp="3:4" type="button" title="Не поддерживается Grok">3:4</button>
421
421
  </div>
422
- </label>
423
- <label id="videoModelRow" style="display: none;">Модель для видео
422
+ </div>
423
+ <div class="field-row" id="videoModelRow" style="display: none;">Модель для видео
424
424
  <div class="seg-control">
425
425
  <button class="seg active" data-vid-model="seedance-2" type="button" title="ByteDance Seedance 2 — основная">Seedance 2</button>
426
426
  <button class="seg" data-vid-model="seedance-2-fast" type="button" title="Быстрее, чуть проще качество">⚡ Seedance 2 Fast</button>
427
427
  <button class="seg" data-vid-model="kling-o1" type="button" title="Kling O1 — креативный, поддерживает reference-видео">Kling O1</button>
428
428
  <button class="seg" data-vid-model="kling-3.0" type="button" title="Kling 3.0 — multi-shot до 15с">Kling 3.0</button>
429
429
  </div>
430
- </label>
431
- <label id="videoOptionsRow" style="display: none;">Параметры видео
430
+ </div>
431
+ <div class="field-row" id="videoOptionsRow" style="display: none;">Параметры видео
432
432
  <div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 6px;">
433
433
  <div style="flex: 1; min-width: 200px;">
434
434
  <div style="font-size: 11px; color: #888; margin-bottom: 4px;">Длительность (сек)</div>
@@ -461,15 +461,15 @@
461
461
  </div>
462
462
  </div>
463
463
  </div>
464
- </label>
465
- <label id="ttsModelRow" style="display: none;">Модель TTS
464
+ </div>
465
+ <div class="field-row" id="ttsModelRow" style="display: none;">Модель TTS
466
466
  <div class="seg-control" style="flex-wrap:wrap;">
467
467
  <button class="seg active" data-tts-model="qwen/qwen3-tts" type="button" title="Qwen TTS — мульти-язык, ready-голоса">Qwen TTS</button>
468
468
  <button class="seg" data-tts-model="elevenlabs/v3" type="button" title="ElevenLabs v3 — лучший EN, тоны">ElevenLabs v3</button>
469
469
  <button class="seg" data-tts-model="minimax/speech-02-hd" type="button" title="MiniMax Speech HD — клон-голоса">MiniMax Speech HD</button>
470
470
  <button class="seg" data-tts-model="google/gemini-3.1-flash-tts-preview" type="button" title="Gemini 3.1 Flash TTS">Gemini Flash TTS</button>
471
471
  </div>
472
- </label>
472
+ </div>
473
473
  <label id="voiceRow" style="display: none;">Голос
474
474
  <select id="genVoice"></select>
475
475
  </label>
@@ -138,19 +138,47 @@ function schedulePersist(session) {
138
138
  }
139
139
 
140
140
  // ============== TOOL-CALL PARSER ==============
141
+ // Толерантные regex'ы — модель иногда выдаёт варианты:
142
+ // <tool>{...}</tool> — наш канонический формат
143
+ // <tool name="x">{...}</tool> — с XML-attrs (Sonnet иногда так делает)
144
+ // <tool_call>{...}</tool_call> — альтернативное имя (model hallucinates)
145
+ // ```tool\n{...}\n``` — code-fence variant
146
+ // Без толерантности теги остаются в .content → юзер видит сырые теги в чате.
147
+ const _TOOL_RE = /<(tool|tool_call|tool_use|function_call)\b[^>]*>([\s\S]*?)<\/\1>/g;
148
+ const _TOOL_RESULT_RE = /<(tool_result|tool_response|function_response)\b[^>]*>[\s\S]*?<\/\1>/g;
149
+ const _TOOL_FENCE_RE = /```\s*(?:tool|tool_call|json)\s*\n([\s\S]*?)\n```/g;
150
+
141
151
  function parseToolCalls(text) {
142
152
  if (typeof text !== 'string' || !text) return [];
143
153
  const out = [];
144
- const re = /<tool>\s*([\s\S]*?)\s*<\/tool>/g;
145
154
  let m;
146
- while ((m = re.exec(text)) !== null) {
155
+ // 1) Tag-форма (с/без attrs).
156
+ _TOOL_RE.lastIndex = 0;
157
+ while ((m = _TOOL_RE.exec(text)) !== null) {
158
+ const inner = (m[2] || '').trim();
147
159
  try {
148
- const obj = JSON.parse(m[1]);
160
+ const obj = JSON.parse(inner);
149
161
  if (obj && typeof obj.name === 'string') {
150
- out.push({ id: crypto.randomUUID(), name: obj.name, args: obj.args || {} });
162
+ out.push({ id: crypto.randomUUID(), name: obj.name, args: obj.args || obj.arguments || obj.input || {} });
151
163
  }
152
164
  } catch (e) {
153
- out.push({ id: crypto.randomUUID(), _parseError: e.message, raw: m[1] });
165
+ out.push({ id: crypto.randomUUID(), _parseError: e.message, raw: inner });
166
+ }
167
+ }
168
+ // 2) Code-fence форма (```tool ... ```). Только если tag-формы не нашлось —
169
+ // иначе риск двойного парсинга (один и тот же call в обеих формах).
170
+ if (!out.length) {
171
+ _TOOL_FENCE_RE.lastIndex = 0;
172
+ while ((m = _TOOL_FENCE_RE.exec(text)) !== null) {
173
+ const inner = (m[1] || '').trim();
174
+ try {
175
+ const obj = JSON.parse(inner);
176
+ if (obj && typeof obj.name === 'string') {
177
+ out.push({ id: crypto.randomUUID(), name: obj.name, args: obj.args || obj.arguments || obj.input || {} });
178
+ }
179
+ } catch (e) {
180
+ out.push({ id: crypto.randomUUID(), _parseError: e.message, raw: inner });
181
+ }
154
182
  }
155
183
  }
156
184
  return out;
@@ -158,8 +186,9 @@ function parseToolCalls(text) {
158
186
  function stripToolCalls(text) {
159
187
  if (typeof text !== 'string') return '';
160
188
  return text
161
- .replace(/<tool>[\s\S]*?<\/tool>/g, '')
162
- .replace(/<tool_result>[\s\S]*?<\/tool_result>/g, '')
189
+ .replace(_TOOL_RE, '')
190
+ .replace(_TOOL_RESULT_RE, '')
191
+ .replace(_TOOL_FENCE_RE, '')
163
192
  .replace(/[ \t]{2,}/g, ' ')
164
193
  .replace(/\n{3,}/g, '\n\n')
165
194
  .trim();
package/lib/providers.js CHANGED
@@ -745,20 +745,31 @@ async function fetchBalances(s) {
745
745
  if (r.ok && typeof d.balance === 'number') out.kingkont = { unit: 'credits', amount: d.balance };
746
746
  } catch {}
747
747
  }
748
- if (s.useElevenlabs && process.env.ELEVENLABS_API_KEY) {
748
+ // ElevenLabs balance: тянем если ключ задан — даже если toggle выключен.
749
+ // Раньше требовалось `s.useElevenlabs && key` — юзер с настроенным ключом,
750
+ // но не включённым toggle'ом не видел баланс. Toggle отвечает за
751
+ // ИСПОЛЬЗОВАНИЕ ElevenLabs для генерации, не за показ баланса.
752
+ if (process.env.ELEVENLABS_API_KEY) {
749
753
  try {
750
754
  const r = await fetch(`${ELEVEN_BASE}/v1/user/subscription`, {
751
755
  headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY },
752
756
  });
753
- const d = await r.json().catch(() => ({}));
754
- if (r.ok && typeof d.character_count === 'number' && typeof d.character_limit === 'number') {
757
+ const text = await r.text();
758
+ let d; try { d = JSON.parse(text); } catch { d = {}; }
759
+ if (!r.ok) {
760
+ console.warn('[balance] ElevenLabs HTTP', r.status, text.slice(0, 200));
761
+ } else if (typeof d.character_count === 'number' && typeof d.character_limit === 'number') {
755
762
  out.elevenlabs = {
756
763
  unit: 'chars',
757
764
  amount: Math.max(0, d.character_limit - d.character_count),
758
765
  limit: d.character_limit,
759
766
  };
767
+ } else {
768
+ console.warn('[balance] ElevenLabs unexpected response shape:', JSON.stringify(d).slice(0, 300));
760
769
  }
761
- } catch {}
770
+ } catch (e) {
771
+ console.warn('[balance] ElevenLabs fetch failed:', e?.message || e);
772
+ }
762
773
  }
763
774
  if (s.useOpenrouter && process.env.OPENROUTER_API_KEY) {
764
775
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.18.8",
3
+ "version": "0.18.10",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/chat.js CHANGED
@@ -594,32 +594,35 @@
594
594
  }
595
595
 
596
596
  // ============== TOOL-CALL PARSER ==============
597
+ // Регексы синхронизированы с lib/chatSession.js — толерантные к атрибутам
598
+ // и альтернативным именам тегов (модель иногда галлюцинирует <tool_call>
599
+ // вместо <tool>, или с XML-attrs).
600
+ const _TOOL_RE = /<(tool|tool_call|tool_use|function_call)\b[^>]*>([\s\S]*?)<\/\1>/g;
601
+ const _TOOL_RESULT_RE = /<(tool_result|tool_response|function_response)\b[^>]*>[\s\S]*?<\/\1>/g;
602
+ const _TOOL_FENCE_RE = /```\s*(?:tool|tool_call|json)\s*\n([\s\S]*?)\n```/g;
603
+
597
604
  function parseToolCalls(text) {
598
605
  const out = [];
599
606
  if (typeof text !== 'string' || !text) return out;
600
- const re = /<tool>\s*([\s\S]*?)\s*<\/tool>/g;
601
607
  let m;
602
- while ((m = re.exec(text)) !== null) {
608
+ _TOOL_RE.lastIndex = 0;
609
+ while ((m = _TOOL_RE.exec(text)) !== null) {
610
+ const inner = (m[2] || '').trim();
603
611
  try {
604
- const obj = JSON.parse(m[1]);
605
- if (obj && typeof obj.name === 'string') out.push({ name: obj.name, args: obj.args || {} });
612
+ const obj = JSON.parse(inner);
613
+ if (obj && typeof obj.name === 'string') out.push({ name: obj.name, args: obj.args || obj.arguments || obj.input || {} });
606
614
  } catch (e) {
607
- out.push({ _parseError: e.message, raw: m[1] });
615
+ out.push({ _parseError: e.message, raw: inner });
608
616
  }
609
617
  }
610
618
  return out;
611
619
  }
612
620
  function stripToolCalls(text) {
613
621
  if (typeof text !== 'string') return '';
614
- // Убираем оба тэга:
615
- // <tool>JSON</tool> — наш command-protocol (модель так зовёт tools)
616
- // <tool_result>JSON</tool_result> — модель иногда его галюцинирует
617
- // в outputе, копируя format из user-msg.
618
- // После стрипа нормализуем whitespace — иначе остаются «дыры» и
619
- // подряд идущие space'ы (3+ → 1, blank lines 3+ → 2).
620
622
  return text
621
- .replace(/<tool>[\s\S]*?<\/tool>/g, '')
622
- .replace(/<tool_result>[\s\S]*?<\/tool_result>/g, '')
623
+ .replace(_TOOL_RE, '')
624
+ .replace(_TOOL_RESULT_RE, '')
625
+ .replace(_TOOL_FENCE_RE, '')
623
626
  .replace(/[ \t]{2,}/g, ' ')
624
627
  .replace(/\n{3,}/g, '\n\n')
625
628
  .trim();
@@ -1445,7 +1448,9 @@
1445
1448
  list.innerHTML = '';
1446
1449
  for (const m of history) {
1447
1450
  if (m.role === 'system') continue;
1448
- if (m.role === 'user' && m.content?.startsWith('<tool_result>')) continue; // системный turn
1451
+ // System-turn user-message с tool_result-блоками скрываем целиком.
1452
+ // Толерантно к attrs: starts с любого варианта tool_result-тега.
1453
+ if (m.role === 'user' && /^<(tool_result|tool_response|function_response)\b/.test(m.content || '')) continue;
1449
1454
  // Пустой assistant без tools — не рендерим (бывает при streaming/parse-fail).
1450
1455
  const hasContent = !!(m.content && m.content.trim());
1451
1456
  const hasTools = Array.isArray(m.tools) && m.tools.length;
@@ -1469,11 +1474,12 @@
1469
1474
  }
1470
1475
  div.appendChild(lbl);
1471
1476
  if (hasContent) {
1472
- // Парсим [ctx: ...] префикс если есть рендерим как маленький
1473
- // сабтайтл над основным текстом.
1474
- let mainText = m.content;
1477
+ // Defensive strip: если сервер не поймал tool/tool_result-теги
1478
+ // (старая persisted-история или edge-case), убираем тут перед
1479
+ // показом. Иначе юзер видит сырые «<tool_result>{...}</tool_result>».
1480
+ let mainText = stripToolCalls(m.content);
1475
1481
  let ctxLine = null;
1476
- const cm = m.content.match(/^\[ctx:\s*([^\]\n]+)\]\n?([\s\S]*)$/);
1482
+ const cm = mainText.match(/^\[ctx:\s*([^\]\n]+)\]\n?([\s\S]*)$/);
1477
1483
  if (cm) { ctxLine = cm[1].trim(); mainText = cm[2].trim(); }
1478
1484
  if (ctxLine) {
1479
1485
  const cx = document.createElement('div');
@@ -120,12 +120,14 @@
120
120
  background: #2e2e2e; color: #ddd; border-color: #444;
121
121
  }
122
122
  .sidebar-footer {
123
- border-top: 1px solid #333; padding: 10px 12px;
123
+ border-top: 1px solid #333;
124
+ /* padding-bottom 50px — освобождаем место под 🔔 кнопку (height 32px,
125
+ bottom:12px → top edge at 44px от низа). Без этого padding'а балансы
126
+ стелились в самый низ и кнопка их перекрывала. Юзер: «подними
127
+ балансы на высоту колокольчика». */
128
+ padding: 10px 12px 50px;
124
129
  display: flex; flex-direction: column; gap: 6px;
125
130
  font-size: 11px; color: #777;
126
- /* Отступ сохранён даже когда jobsInfo и hint убраны — иначе sidebar
127
- «прыгнет» вверх. 56px ≈ высота прежних двух строк + padding. */
128
- min-height: 56px;
129
131
  }
130
132
  .sidebar-footer .hint { color: #777; font-size: 11px; line-height: 1.4; }
131
133
  .sidebar-footer .jobs-info { color: #aaccdd; font-size: 11px; }
@@ -1668,7 +1670,14 @@
1668
1670
  position: relative;
1669
1671
  }
1670
1672
  .modal-card h3 { font-size: 16px; }
1671
- .modal-card label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: #aaa; text-transform: uppercase; letter-spacing: 0.5px; }
1673
+ .modal-card label,
1674
+ .modal-card .field-row { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: #aaa; text-transform: uppercase; letter-spacing: 0.5px; }
1675
+ /* .field-row — replacement для <label> в строках с .seg-control. Раньше
1676
+ был <label>, но без `for=` HTML семантика «click на label → click
1677
+ на ПЕРВЫЙ labelable element внутри» приводила к багу: брызги мыши
1678
+ по тексту «Качество» или паддингу активировали первую кнопку (low /
1679
+ Nano Banana 2) — выглядело как «застрявший hover». <div> такой
1680
+ семантики не имеет. CSS — те же стили, через общий селектор выше. */
1672
1681
  .modal-card input, .modal-card textarea, .modal-card select {
1673
1682
  background: #1e1e1e; color: #e0e0e0; border: 1px solid #383838;
1674
1683
  border-radius: 4px; padding: 8px 10px; font-family: inherit; font-size: 14px;