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 +14 -14
- package/lib/chatSession.js +36 -7
- package/lib/providers.js +15 -4
- package/package.json +1 -1
- package/renderer/chat.js +24 -18
- package/renderer/styles.css +14 -5
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
|
-
<
|
|
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
|
-
</
|
|
391
|
-
<
|
|
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
|
-
</
|
|
402
|
-
<
|
|
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
|
-
</
|
|
409
|
-
<
|
|
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
|
-
</
|
|
423
|
-
<
|
|
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
|
-
</
|
|
431
|
-
<
|
|
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
|
-
</
|
|
465
|
-
<
|
|
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
|
-
</
|
|
472
|
+
</div>
|
|
473
473
|
<label id="voiceRow" style="display: none;">Голос
|
|
474
474
|
<select id="genVoice"></select>
|
|
475
475
|
</label>
|
package/lib/chatSession.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
162
|
-
.replace(
|
|
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
|
-
|
|
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
|
|
754
|
-
|
|
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
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
622
|
-
.replace(
|
|
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
|
-
|
|
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
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
|
|
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 =
|
|
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');
|
package/renderer/styles.css
CHANGED
|
@@ -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;
|
|
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
|
|
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;
|