kingkont 0.18.1 → 0.18.3
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/providers.js +14 -1
- package/package.json +1 -1
- package/renderer/cloudFs.js +23 -2
- package/renderer/generate.js +42 -7
- package/renderer/notifyPanel.js +45 -13
- package/renderer/state.js +29 -32
- package/renderer/styles.css +5 -2
package/lib/providers.js
CHANGED
|
@@ -515,7 +515,20 @@ async function generateText({ prompt, messages: msgsIn, model = 'anthropic/claud
|
|
|
515
515
|
});
|
|
516
516
|
const text = await r.text();
|
|
517
517
|
let data; try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
518
|
-
if (!r.ok)
|
|
518
|
+
if (!r.ok) {
|
|
519
|
+
// OpenRouter заворачивает upstream-ошибки в {error:{message, metadata:{provider_name, raw}}}.
|
|
520
|
+
// Без metadata.raw юзер видит generic «Provider returned error» — бесполезно.
|
|
521
|
+
const baseMsg = data?.error?.message || data?.raw || `HTTP ${r.status}`;
|
|
522
|
+
const meta = data?.error?.metadata || {};
|
|
523
|
+
const provName = meta.provider_name ? ` [${meta.provider_name}]` : '';
|
|
524
|
+
let extra = '';
|
|
525
|
+
if (meta.raw) {
|
|
526
|
+
const rawStr = typeof meta.raw === 'string' ? meta.raw : JSON.stringify(meta.raw);
|
|
527
|
+
extra = ` — ${rawStr.slice(0, 400)}`;
|
|
528
|
+
}
|
|
529
|
+
console.error('[OpenRouter error]', JSON.stringify(data, null, 2).slice(0, 2000));
|
|
530
|
+
throw new Error(`${baseMsg}${provName}${extra}`);
|
|
531
|
+
}
|
|
519
532
|
return {
|
|
520
533
|
text: data?.choices?.[0]?.message?.content || '',
|
|
521
534
|
model: data?.model || model,
|
package/package.json
CHANGED
package/renderer/cloudFs.js
CHANGED
|
@@ -53,6 +53,24 @@
|
|
|
53
53
|
const i = n.lastIndexOf('/');
|
|
54
54
|
return i < 0 ? { dir: '', name: n } : { dir: n.slice(0, i), name: n.slice(i + 1) };
|
|
55
55
|
}
|
|
56
|
+
// MIME-detection по расширению. Нужен в getFile() — нативный FSAH ставит
|
|
57
|
+
// type из расширения, наш shim строил Blob без type → readAsDataURL давал
|
|
58
|
+
// битый `data:base64,...` → Anthropic/OpenRouter reject image_url.
|
|
59
|
+
const _MIME_MAP = {
|
|
60
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp',
|
|
61
|
+
gif: 'image/gif', avif: 'image/avif', heic: 'image/heic', heif: 'image/heif',
|
|
62
|
+
bmp: 'image/bmp', svg: 'image/svg+xml',
|
|
63
|
+
mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', mkv: 'video/x-matroska',
|
|
64
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4',
|
|
65
|
+
flac: 'audio/flac', aac: 'audio/aac',
|
|
66
|
+
json: 'application/json', md: 'text/markdown', txt: 'text/plain',
|
|
67
|
+
pdf: 'application/pdf',
|
|
68
|
+
};
|
|
69
|
+
function _mimeFromName(name) {
|
|
70
|
+
const m = String(name || '').match(/\.([a-zA-Z0-9]+)$/);
|
|
71
|
+
if (!m) return '';
|
|
72
|
+
return _MIME_MAP[m[1].toLowerCase()] || '';
|
|
73
|
+
}
|
|
56
74
|
|
|
57
75
|
// === Backend factories. =====================================================
|
|
58
76
|
// Возвращает объект с минимальным API над хранилищем (диск или память).
|
|
@@ -209,8 +227,11 @@
|
|
|
209
227
|
const buf = await backend.read(relPath);
|
|
210
228
|
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
211
229
|
const stat = await backend.stat(relPath);
|
|
212
|
-
//
|
|
213
|
-
|
|
230
|
+
// КРИТИЧНО: Blob БЕЗ type → file.type='' → FileReader.readAsDataURL
|
|
231
|
+
// даёт `data:base64,...` без MIME → Anthropic/OpenRouter reject
|
|
232
|
+
// image_url. Нативный FSAH ставит type из расширения, мимикрируем.
|
|
233
|
+
const type = _mimeFromName(_name);
|
|
234
|
+
const blob = new Blob([u8], type ? { type } : undefined);
|
|
214
235
|
return Object.assign(blob, {
|
|
215
236
|
name: _name,
|
|
216
237
|
lastModified: stat?.mtimeMs ?? Date.now(),
|
package/renderer/generate.js
CHANGED
|
@@ -492,6 +492,20 @@ $('textGenSubmit').addEventListener('click', async () => {
|
|
|
492
492
|
runTextJob(node, resolvedPrompt, model, state.currentBoard.handle, state.currentBoard.key, imageRefs);
|
|
493
493
|
});
|
|
494
494
|
|
|
495
|
+
// Лёгкий fallback MIME-detector по расширению — на случай когда
|
|
496
|
+
// Blob-обёртка пришла без `type` (некоторые пути могут потерять).
|
|
497
|
+
// Anthropic/OpenRouter rejects data URL без MIME → обязательный fix.
|
|
498
|
+
function _mimeFromFilename(name) {
|
|
499
|
+
const m = String(name || '').match(/\.([a-zA-Z0-9]+)$/);
|
|
500
|
+
if (!m) return 'image/jpeg'; // дефолт для картинок без расширения
|
|
501
|
+
const ext = m[1].toLowerCase();
|
|
502
|
+
return ({
|
|
503
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
|
|
504
|
+
webp: 'image/webp', gif: 'image/gif', avif: 'image/avif',
|
|
505
|
+
heic: 'image/heic', bmp: 'image/bmp',
|
|
506
|
+
})[ext] || 'image/jpeg';
|
|
507
|
+
}
|
|
508
|
+
|
|
495
509
|
async function _imageRefToDataUrl(ref) {
|
|
496
510
|
// Возвращает {url, size, mime} или {error}. Раньше тихо возвращал null
|
|
497
511
|
// на любую ошибку → дебаг был невозможен (юзер видел «картинка не
|
|
@@ -503,13 +517,34 @@ async function _imageRefToDataUrl(ref) {
|
|
|
503
517
|
}
|
|
504
518
|
const fh = await resolveBoardFile(ref.boardHandle, ref.file);
|
|
505
519
|
const file = await fh.getFile();
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
520
|
+
// Гарантируем правильный MIME — иначе data URL получается как
|
|
521
|
+
// `data:base64,...` (без MIME) → Anthropic возвращает 502 «Provider
|
|
522
|
+
// returned error» через OpenRouter. Bug был в cloudFs shim'е (Blob
|
|
523
|
+
// без type), но защищаемся здесь тоже на случай других source'ов.
|
|
524
|
+
const mime = file.type && file.type !== 'application/octet-stream'
|
|
525
|
+
? file.type
|
|
526
|
+
: _mimeFromFilename(ref.file);
|
|
527
|
+
let url;
|
|
528
|
+
if (file.type === mime) {
|
|
529
|
+
// type уже правильный — FileReader даст правильный data URL.
|
|
530
|
+
url = await new Promise((res, rej) => {
|
|
531
|
+
const r = new FileReader();
|
|
532
|
+
r.onload = () => res(r.result);
|
|
533
|
+
r.onerror = () => rej(r.error);
|
|
534
|
+
r.readAsDataURL(file);
|
|
535
|
+
});
|
|
536
|
+
} else {
|
|
537
|
+
// type пустой/неправильный — пересобираем Blob с явным type.
|
|
538
|
+
const buf = await file.arrayBuffer();
|
|
539
|
+
const blob = new Blob([buf], { type: mime });
|
|
540
|
+
url = await new Promise((res, rej) => {
|
|
541
|
+
const r = new FileReader();
|
|
542
|
+
r.onload = () => res(r.result);
|
|
543
|
+
r.onerror = () => rej(r.error);
|
|
544
|
+
r.readAsDataURL(blob);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return { url, size: file.size, mime };
|
|
513
548
|
} catch (e) {
|
|
514
549
|
return { error: e?.message || String(e) };
|
|
515
550
|
}
|
package/renderer/notifyPanel.js
CHANGED
|
@@ -54,13 +54,19 @@
|
|
|
54
54
|
$('notifyClear').addEventListener('click', () => { events.length = 0; unread = 0; render(); });
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// openMode: 'manual' (юзер сам нажал на 🔔 — НЕ автозакрываем),
|
|
58
|
+
// 'auto' (открыли по событию — закроется через таймер),
|
|
59
|
+
// null (закрыто).
|
|
60
|
+
let openMode = null;
|
|
61
|
+
function setOpen(open, mode) {
|
|
58
62
|
_ensureUI();
|
|
59
63
|
panelOpen = open;
|
|
64
|
+
openMode = open ? (mode || 'manual') : null;
|
|
60
65
|
$('notifyPanel').style.display = open ? 'flex' : 'none';
|
|
61
66
|
if (open) { unread = 0; updateBadge(); render(); }
|
|
62
67
|
}
|
|
63
|
-
function toggle() { setOpen(!panelOpen); }
|
|
68
|
+
function toggle() { setOpen(!panelOpen, 'manual'); }
|
|
69
|
+
function isManuallyOpen() { return panelOpen && openMode === 'manual'; }
|
|
64
70
|
|
|
65
71
|
function updateBadge() {
|
|
66
72
|
const b = $('notifyBadge');
|
|
@@ -158,6 +164,22 @@
|
|
|
158
164
|
} else {
|
|
159
165
|
render();
|
|
160
166
|
}
|
|
167
|
+
// Авто-открытие панели на 3.5s — юзер видит событие сразу, потом
|
|
168
|
+
// панель прячется. Если юзер сам открыл — не трогаем (проверка в
|
|
169
|
+
// openAuto). Каждое новое событие сбрасывает таймер.
|
|
170
|
+
_scheduleAutoFlash();
|
|
171
|
+
}
|
|
172
|
+
// Auto-flash: открыть в auto-режиме и закрыть через таймер. Срабатывает
|
|
173
|
+
// на КАЖДОЕ addEvent. Если юзер уже manual'но открыл — открытие no-op'ится.
|
|
174
|
+
let _autoFlashT = null;
|
|
175
|
+
function _scheduleAutoFlash() {
|
|
176
|
+
if (panelOpen && openMode === 'manual') return; // юзер сам открыл — не лезем
|
|
177
|
+
if (!panelOpen) setOpen(true, 'auto');
|
|
178
|
+
if (_autoFlashT) clearTimeout(_autoFlashT);
|
|
179
|
+
_autoFlashT = setTimeout(() => {
|
|
180
|
+
_autoFlashT = null;
|
|
181
|
+
if (openMode === 'auto') setOpen(false);
|
|
182
|
+
}, 3500);
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
// Navigate from notification to scene/node.
|
|
@@ -224,20 +246,30 @@
|
|
|
224
246
|
setTimeout(() => { el.style.boxShadow = ''; }, 2500);
|
|
225
247
|
}, 200);
|
|
226
248
|
}
|
|
227
|
-
|
|
249
|
+
// openAuto — для showToast: открывает в режиме auto (закроется по таймеру
|
|
250
|
+
// если showToast.close('auto') не отменён). open() без аргумента — manual.
|
|
251
|
+
window.notifyPanel = {
|
|
252
|
+
open: (mode) => setOpen(true, mode || 'manual'),
|
|
253
|
+
openAuto: () => setOpen(true, 'auto'),
|
|
254
|
+
closeAuto: () => { if (openMode === 'auto') setOpen(false); },
|
|
255
|
+
close: () => setOpen(false),
|
|
256
|
+
toggle, addEvent, render,
|
|
257
|
+
isManuallyOpen,
|
|
258
|
+
};
|
|
228
259
|
|
|
229
|
-
// Init: ensure UI exists ASAP,
|
|
260
|
+
// Init: ensure UI exists ASAP, drain pending toast queue, subscribe WS.
|
|
230
261
|
document.addEventListener('DOMContentLoaded', () => {
|
|
231
262
|
_ensureUI();
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
263
|
+
// Дренируем showToast'ы которые вызвались до загрузки notifyPanel.
|
|
264
|
+
// (state.js:showToast копит в __pendingToastQueue если addEvent
|
|
265
|
+
// ещё не доступен.)
|
|
266
|
+
try {
|
|
267
|
+
const q = window.__pendingToastQueue;
|
|
268
|
+
if (Array.isArray(q)) {
|
|
269
|
+
for (const ev of q) addEvent(ev);
|
|
270
|
+
q.length = 0;
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
241
273
|
// WS-канал jobs:all для start/end/done/failed events. Connect к /ws.
|
|
242
274
|
function _connect() {
|
|
243
275
|
let ws;
|
package/renderer/state.js
CHANGED
|
@@ -177,8 +177,10 @@ async function systemNotify(title, body, opts = {}) {
|
|
|
177
177
|
try { perm = await Notification.requestPermission(); } catch {}
|
|
178
178
|
}
|
|
179
179
|
if (perm !== 'granted') {
|
|
180
|
-
//
|
|
181
|
-
|
|
180
|
+
// Без OS-нотификации — событие УЖЕ добавлено в notifyPanel через
|
|
181
|
+
// showToast-wrapper (single-source-of-truth). Раньше тут был fallback
|
|
182
|
+
// на showToast → дубль события с префиксом «🔔 KingKont:» БЕЗ target,
|
|
183
|
+
// поэтому без клика. Теперь молча пропускаем.
|
|
182
184
|
return null;
|
|
183
185
|
}
|
|
184
186
|
try {
|
|
@@ -197,36 +199,27 @@ async function systemNotify(title, body, opts = {}) {
|
|
|
197
199
|
}
|
|
198
200
|
window.systemNotify = systemNotify;
|
|
199
201
|
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// -
|
|
203
|
-
// -
|
|
202
|
+
// Глобальная сигналка событий. Раньше показывался bottom-left toast,
|
|
203
|
+
// но дубль с notifyPanel'ом юзера утомил («toast не нужен»). Теперь:
|
|
204
|
+
// - событие добавляется в notifyPanel (через wrapper в notifyPanel.js)
|
|
205
|
+
// - панель открывается АВТОМАТИЧЕСКИ ненадолго (3s) — юзер видит
|
|
206
|
+
// событие сразу, потом панель скрывается, бейдж остаётся.
|
|
207
|
+
// Возвращаем фейковый «токен» для совместимости с местами которые ждут
|
|
208
|
+
// результат showToast (там ничего с ним не делают, но на всякий случай).
|
|
204
209
|
function showToast(text, kind, opts) {
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
ok: 'background:#1f4a1f; border:1px solid #4cc44c; color:#dcffdc;',
|
|
219
|
-
error: 'background:#4a1f1f; border:1px solid #c44c4c; color:#ffdcdc;',
|
|
220
|
-
info: 'background:#1f2f4a; border:1px solid #4c7cc4; color:#dceaff;',
|
|
221
|
-
};
|
|
222
|
-
t.style.cssText = `${colors[kind] || colors.info} padding:10px 14px; border-radius:8px; font-size:13px; font-weight:500; pointer-events:auto; box-shadow:0 6px 20px rgba(0,0,0,0.5); cursor:pointer; line-height:1.4; word-break:break-word;`;
|
|
223
|
-
t.textContent = text;
|
|
224
|
-
t.title = 'Кликни чтобы закрыть';
|
|
225
|
-
t.addEventListener('click', () => t.remove());
|
|
226
|
-
host.appendChild(t);
|
|
227
|
-
const ttl = kind === 'error' ? 30000 : 10000;
|
|
228
|
-
setTimeout(() => t.remove(), ttl);
|
|
229
|
-
return t;
|
|
210
|
+
// Bottom-left toast убран — событие напрямую идёт в notifyPanel,
|
|
211
|
+
// панель сама подсветится (см. _scheduleAutoFlash в notifyPanel.js).
|
|
212
|
+
// Если notifyPanel ещё не загружен (DOM-readiness race) — копим
|
|
213
|
+
// в очередь, она дренируется при создании панели.
|
|
214
|
+
try {
|
|
215
|
+
if (window.notifyPanel?.addEvent) {
|
|
216
|
+
window.notifyPanel.addEvent({ kind: kind || 'info', text, target: opts?.target || null });
|
|
217
|
+
} else {
|
|
218
|
+
(window.__pendingToastQueue = window.__pendingToastQueue || [])
|
|
219
|
+
.push({ kind: kind || 'info', text, target: opts?.target || null });
|
|
220
|
+
}
|
|
221
|
+
} catch {}
|
|
222
|
+
return { _stub: true };
|
|
230
223
|
}
|
|
231
224
|
window.showToast = showToast;
|
|
232
225
|
|
|
@@ -669,10 +662,14 @@ async function plannedProvider(kind) {
|
|
|
669
662
|
let s;
|
|
670
663
|
try { s = await window.appSettings.get(); } catch { s = {}; }
|
|
671
664
|
const hasChatium = !!(s.useChatium && s.chatium?.token && s.chatium?.base);
|
|
665
|
+
const hasOpenrouter = !!(s.useOpenrouter && s.openrouterKey);
|
|
672
666
|
switch (kind) {
|
|
673
667
|
case 'text':
|
|
668
|
+
// Зеркалит логику в lib/providers.js:generateText — directOpenrouter
|
|
669
|
+
// имеет приоритет над Chatium (юзер явно включил → значит хочет
|
|
670
|
+
// не тратить kingkont-кредиты).
|
|
671
|
+
if (hasOpenrouter) return 'openrouter';
|
|
674
672
|
if (hasChatium) return 'kingkont';
|
|
675
|
-
if (s.useOpenrouter === true) return 'openrouter';
|
|
676
673
|
return 'none';
|
|
677
674
|
case 'audio': case 'tts': case 'sfx': case 'music':
|
|
678
675
|
// Если ElevenLabs включён С ключом — приоритет прямого ElevenLabs
|
package/renderer/styles.css
CHANGED
|
@@ -124,8 +124,11 @@
|
|
|
124
124
|
display: flex; flex-direction: column; gap: 6px;
|
|
125
125
|
font-size: 11px; color: #777;
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
/* «В фоне: N» и hint «Перетаскивай файлы…» — отступаем влево на ширину
|
|
128
|
+
🔔-кнопки (32px + 12px gap слева от sidebar = 44px), чтобы кнопка
|
|
129
|
+
событий (position:fixed; left:12px; bottom:12px) их не перекрывала. */
|
|
130
|
+
.sidebar-footer .hint { color: #777; font-size: 11px; line-height: 1.4; padding-left: 44px; }
|
|
131
|
+
.sidebar-footer .jobs-info { color: #aaccdd; font-size: 11px; padding-left: 44px; }
|
|
129
132
|
.sidebar-footer .balance-info {
|
|
130
133
|
display: flex; align-items: center; gap: 6px; font-size: 11px;
|
|
131
134
|
color: #c4c4c4; padding: 4px 8px; background: #2a2a2a;
|