kingkont 0.18.9 → 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/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 +6 -4
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; }
|