kingkont 0.18.9 → 0.18.11

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.
@@ -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.9",
3
+ "version": "0.18.11",
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');
@@ -2185,10 +2185,21 @@ async function mutateNode(bKey, boardHandle, nodeId, mutator) {
2185
2185
  }
2186
2186
 
2187
2187
  function updateJobsBadge() {
2188
+ // КРИТИЧНО: #jobsInfo был удалён из sidebar-footer в v0.18.6 (юзер
2189
+ // попросил убрать «В фоне: N» из sidebar — счётчик переехал на 🔔).
2190
+ // Без этого guard'а функция бросала TypeError на null.style → ломала
2191
+ // ВСЕ функции которые её зовут после state.jobs.set/delete (т.е.
2192
+ // startGenerationJob, runTextJob, runMusicJob, runSfxJob, runTTSJob).
2193
+ // Симптом: generation-задачи не уходили на сервер, /api/generate
2194
+ // никогда не вызывался. Resume-job'ы умирали в .catch().
2188
2195
  const el = $('jobsInfo');
2189
- const n = state.jobs.size;
2190
- if (n === 0) { el.style.display = 'none'; return; }
2191
- el.style.display = '';
2192
- el.innerHTML = `<span class="spinner"></span>В фоне: ${n}`;
2196
+ if (el) {
2197
+ const n = state.jobs.size;
2198
+ if (n === 0) { el.style.display = 'none'; }
2199
+ else { el.style.display = ''; el.innerHTML = `<span class="spinner"></span>В фоне: ${n}`; }
2200
+ }
2201
+ // Bell-state показывает то же самое (число + описание справа).
2202
+ // Триггерим перерисовку — чтобы не ждать 1.5s poll.
2203
+ if (window.notifyPanel?.refreshBellState) window.notifyPanel.refreshBellState();
2193
2204
  }
2194
2205
 
@@ -427,6 +427,9 @@
427
427
  close: () => setOpen(false),
428
428
  toggle, addEvent, render,
429
429
  isManuallyOpen,
430
+ // Для generate.js:updateJobsBadge — синхронный rerender bell-state
431
+ // когда state.jobs изменился (без ожидания 1.5s poll).
432
+ refreshBellState: _renderBellState,
430
433
  };
431
434
 
432
435
  // Init: ensure UI exists ASAP, drain pending toast queue, subscribe WS.
@@ -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; }