kingkont 0.18.14 → 0.19.0
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/eventStore.js +12 -2
- package/lib/providers.js +130 -5
- package/lib/settings.js +18 -0
- package/main.js +10 -0
- package/package.json +1 -1
- package/preload.js +5 -0
- package/renderer/board.js +267 -102
- package/renderer/cloudFs.js +8 -0
- package/renderer/cloudProjects.js +28 -3
- package/renderer/generate.js +4 -1
- package/renderer/notifyPanel.js +81 -39
- package/renderer/state.js +5 -0
- package/renderer/styles.css +31 -2
- package/server.js +180 -2
package/renderer/board.js
CHANGED
|
@@ -44,23 +44,43 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
44
44
|
// - logout → чистим кэш + перерисовываем (cloud-карточки исчезнут,
|
|
45
45
|
// fetchListCached не дёрнет /api/projects без логина).
|
|
46
46
|
// setCloudButtonsVisibility пересоберёт sidebar-кнопки (☁ Новый и пр.).
|
|
47
|
+
// Универсальный handler смены auth (login/logout) — общий для Electron-IPC
|
|
48
|
+
// и web WS-event 'auth:changed'.
|
|
49
|
+
async function _onAuthChanged(state) {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.removeItem('cloudProjectsCache');
|
|
52
|
+
localStorage.removeItem('cloudProjectsLastOpened');
|
|
53
|
+
localStorage.removeItem('chatiumStatusCache');
|
|
54
|
+
renderWelcomeIdentity({ force: true }).catch(() => {});
|
|
55
|
+
refreshBalance().catch(() => {});
|
|
56
|
+
if (!window.cloudProjects) return;
|
|
57
|
+
if (window.cloudProjects.setVisibility) window.cloudProjects.setVisibility();
|
|
58
|
+
if (!document.body.classList.contains('no-project')) return;
|
|
59
|
+
await renderWelcomeRecents();
|
|
60
|
+
} catch (e) { vlog('err', 'auth-changed handler: ' + (e?.message || e)); }
|
|
61
|
+
}
|
|
47
62
|
if (window.appChatium?.onAuthChanged) {
|
|
48
|
-
window.appChatium.onAuthChanged(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
window.appChatium.onAuthChanged(_onAuthChanged);
|
|
64
|
+
} else {
|
|
65
|
+
// Web-mode: подписка на WS-канал auth:changed (server publish'ит
|
|
66
|
+
// после web-login flow / logout).
|
|
67
|
+
try {
|
|
68
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
69
|
+
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
70
|
+
ws.onopen = () => {
|
|
71
|
+
try { ws.send(JSON.stringify({ type: 'subscribe', channel: 'auth:changed' })); } catch {}
|
|
72
|
+
};
|
|
73
|
+
ws.onmessage = (e) => {
|
|
74
|
+
let m; try { m = JSON.parse(e.data); } catch { return; }
|
|
75
|
+
if (m?.type === 'event' && m.channel === 'auth:changed') {
|
|
76
|
+
_onAuthChanged(m.event?.kind || 'login');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// Если WS отвалится — потеряем live-уведомления auth-changed,
|
|
80
|
+
// но identity всё равно подтянется при следующем render'е через
|
|
81
|
+
// 10-минутный refresh-window. Не reconnect'имся аггресивно.
|
|
82
|
+
ws.onerror = () => {};
|
|
83
|
+
} catch (e) { vlog('warn', 'auth WS subscribe failed: ' + e?.message); }
|
|
64
84
|
}
|
|
65
85
|
// Восстановить состояние панелей таймлайна/превью/реплик
|
|
66
86
|
const tlOpen = localStorage.getItem('timelineOpen') === '1';
|
|
@@ -100,18 +120,25 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
100
120
|
refreshBalance().catch(() => {});
|
|
101
121
|
setInterval(() => { refreshBalance().catch(() => {}); }, 60_000);
|
|
102
122
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
123
|
+
// Autoload-стратегия: открыть последний проект ТОЛЬКО при Cmd+R (reload),
|
|
124
|
+
// когда юзер был в проекте. На cold start (полное закрытие → запуск)
|
|
125
|
+
// показываем welcome. Юзер: «при запуске я вижу страницу проекта (а не
|
|
126
|
+
// стартовую страницу)».
|
|
127
|
+
//
|
|
128
|
+
// Differentiation: main-process IPC `app:consume-cold-start` возвращает
|
|
129
|
+
// true ровно один раз за app-launch. Cmd+R → false (main продолжает
|
|
130
|
+
// жить, renderer перезагружается). Полный quit+restart → новый main →
|
|
131
|
+
// флаг снова true. Это надёжнее sessionStorage (которое в Electron
|
|
132
|
+
// может переживать quit при persistent partition).
|
|
133
|
+
let isColdStart = false;
|
|
134
|
+
try { isColdStart = await window.appProject?.consumeColdStart?.(); } catch {}
|
|
135
|
+
// skipAutoload: cold start ИЛИ explicit welcome-флаги.
|
|
136
|
+
const skipAutoload = isColdStart
|
|
137
|
+
|| localStorage.getItem('welcomeOnNextStart') === '1'
|
|
111
138
|
|| localStorage.getItem('lastLocation') === 'welcome';
|
|
112
139
|
if (skipAutoload) {
|
|
113
140
|
localStorage.removeItem('welcomeOnNextStart');
|
|
114
|
-
vlog('info',
|
|
141
|
+
vlog('info', `restore: skipped (cold-start=${isColdStart}, welcome state persisted)`);
|
|
115
142
|
} else {
|
|
116
143
|
try {
|
|
117
144
|
const recents = await getRecents();
|
|
@@ -135,8 +162,27 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
135
162
|
const openSettingsFromLogo = () => {
|
|
136
163
|
if (window.appSettings?.openSettingsWindow) window.appSettings.openSettingsWindow();
|
|
137
164
|
};
|
|
138
|
-
document.getElementById('brandLogo')?.addEventListener('dblclick', openSettingsFromLogo);
|
|
139
165
|
document.getElementById('welcomeLogo')?.addEventListener('dblclick', openSettingsFromLogo);
|
|
166
|
+
// Brand-logo в шапке проекта: single-click → возврат на welcome,
|
|
167
|
+
// dblclick → настройки. Юзер: «на странице проекта сделай клик по
|
|
168
|
+
// левому-верхнему логотипу возвратом в список проектов».
|
|
169
|
+
// Single-click задерживаем на 250ms — за это время dblclick отменит таймер.
|
|
170
|
+
const brandLogo = document.getElementById('brandLogo');
|
|
171
|
+
if (brandLogo) {
|
|
172
|
+
brandLogo.style.cursor = 'pointer';
|
|
173
|
+
brandLogo.title = 'Клик — на главную; дабл-клик — настройки';
|
|
174
|
+
brandLogo.addEventListener('click', () => {
|
|
175
|
+
if (brandLogo._dblTimer) { clearTimeout(brandLogo._dblTimer); brandLogo._dblTimer = null; }
|
|
176
|
+
brandLogo._dblTimer = setTimeout(() => {
|
|
177
|
+
brandLogo._dblTimer = null;
|
|
178
|
+
if (state.filmHandle && typeof closeProject === 'function') closeProject();
|
|
179
|
+
}, 250);
|
|
180
|
+
});
|
|
181
|
+
brandLogo.addEventListener('dblclick', () => {
|
|
182
|
+
if (brandLogo._dblTimer) { clearTimeout(brandLogo._dblTimer); brandLogo._dblTimer = null; }
|
|
183
|
+
openSettingsFromLogo();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
140
186
|
// ПКМ на brand-area (sidebar header) — действия проектного уровня
|
|
141
187
|
// (сохранить как шаблон / обновить шаблон).
|
|
142
188
|
document.querySelector('.brand')?.addEventListener('contextmenu', e => {
|
|
@@ -202,6 +248,7 @@ async function refreshBalance() {
|
|
|
202
248
|
}
|
|
203
249
|
}
|
|
204
250
|
|
|
251
|
+
function _renderBalancesInto(data) {
|
|
205
252
|
// Один pill на провайдер. Если у провайдера нет данных (выключен или
|
|
206
253
|
// API не дал баланс) — pill не рендерим.
|
|
207
254
|
const pills = [
|
|
@@ -210,10 +257,13 @@ async function refreshBalance() {
|
|
|
210
257
|
{ key: 'openrouter', label: 'OpenRouter', low: 0.5, fmt: (a) => `<b>$${a.toFixed(2)}</b>` },
|
|
211
258
|
{ key: 'elevenlabs', label: 'ElevenLabs', low: 1000, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b> chars` },
|
|
212
259
|
];
|
|
213
|
-
function renderInto(wrap) {
|
|
260
|
+
function renderInto(wrap, opts = {}) {
|
|
214
261
|
if (!wrap) return;
|
|
215
262
|
wrap.innerHTML = '';
|
|
216
263
|
for (const p of pills) {
|
|
264
|
+
// На welcome (welcomeStatusBalances) kingkont-кредиты показываются
|
|
265
|
+
// ВНУТРИ identity-pill (см. _renderIdentity) — здесь не дублируем.
|
|
266
|
+
if (opts.skipKingkont && p.key === 'kingkont') continue;
|
|
217
267
|
const d = data[p.key];
|
|
218
268
|
if (!d || typeof d.amount !== 'number') continue;
|
|
219
269
|
const pill = document.createElement('span');
|
|
@@ -229,10 +279,15 @@ async function refreshBalance() {
|
|
|
229
279
|
wrap.appendChild(pill);
|
|
230
280
|
}
|
|
231
281
|
}
|
|
232
|
-
// Sidebar-footer (когда открыт проект)
|
|
282
|
+
// Sidebar-footer (когда открыт проект) — все балансы.
|
|
233
283
|
renderInto(document.getElementById('balancesAll'));
|
|
234
|
-
// Welcome top-right (
|
|
235
|
-
renderInto(document.getElementById('welcomeStatusBalances'));
|
|
284
|
+
// Welcome top-right — БЕЗ kingkont (он встроен в identity-pill).
|
|
285
|
+
renderInto(document.getElementById('welcomeStatusBalances'), { skipKingkont: true });
|
|
286
|
+
// refreshBalance мог обновить cache → identity-pill должен показать
|
|
287
|
+
// новое значение. Зовём только если уже на welcome (не лезть в проект).
|
|
288
|
+
if (document.body.classList.contains('no-project')) {
|
|
289
|
+
renderWelcomeIdentity().catch(() => {});
|
|
290
|
+
}
|
|
236
291
|
}
|
|
237
292
|
window.refreshBalance = refreshBalance;
|
|
238
293
|
|
|
@@ -240,13 +295,19 @@ window.refreshBalance = refreshBalance;
|
|
|
240
295
|
// при возврате на welcome (без flicker «пусто → потом запрос → потом текст»).
|
|
241
296
|
// На фоне дёргаем настоящий status и обновляем pill.
|
|
242
297
|
const _STATUS_CACHE_KEY = 'chatiumStatusCache';
|
|
298
|
+
// Refresh-окно: фоновый запрос профиля делаем не чаще раз в 10 минут.
|
|
299
|
+
// Юзер: «перестань запрашивать профиль каждый раз когда я возвращаюсь
|
|
300
|
+
// на главную страницу». Раньше каждый show welcome → новый HTTP-запрос
|
|
301
|
+
// к Chatium /auth~me — лишний трафик, мигание pill'а.
|
|
302
|
+
const _STATUS_REFRESH_WINDOW_MS = 10 * 60 * 1000;
|
|
303
|
+
const _STATUS_HARD_TTL_MS = 24 * 3600 * 1000;
|
|
243
304
|
function _readStatusCache() {
|
|
244
305
|
try {
|
|
245
306
|
const raw = localStorage.getItem(_STATUS_CACHE_KEY);
|
|
246
307
|
if (!raw) return null;
|
|
247
308
|
const obj = JSON.parse(raw);
|
|
248
|
-
if (Date.now() - (obj.ts || 0) >
|
|
249
|
-
return obj.status || null;
|
|
309
|
+
if (Date.now() - (obj.ts || 0) > _STATUS_HARD_TTL_MS) return null; // 24h hard max
|
|
310
|
+
return { status: obj.status || null, ts: obj.ts || 0 };
|
|
250
311
|
} catch { return null; }
|
|
251
312
|
}
|
|
252
313
|
function _writeStatusCache(status) {
|
|
@@ -255,19 +316,31 @@ function _writeStatusCache(status) {
|
|
|
255
316
|
|
|
256
317
|
// Identity-pill в правом верхнем углу welcome-экрана.
|
|
257
318
|
// Показывает кто залогинен в KingKont (или предлагает войти).
|
|
258
|
-
|
|
319
|
+
// opts.force=true — игнорирует rate-limit, всё равно фетчит (login/logout flow).
|
|
320
|
+
async function renderWelcomeIdentity(opts = {}) {
|
|
259
321
|
const wrap = document.getElementById('welcomeStatusIdentity');
|
|
260
322
|
if (!wrap) return;
|
|
261
|
-
// 1) Сначала рендерим из кэша (если есть) — мгновенно.
|
|
262
323
|
const cached = _readStatusCache();
|
|
263
|
-
if (cached) _renderIdentity(wrap, cached);
|
|
264
|
-
//
|
|
324
|
+
if (cached?.status) _renderIdentity(wrap, cached.status);
|
|
325
|
+
// Re-fetch только если: кэш отсутствует ИЛИ старше 10 мин ИЛИ force.
|
|
326
|
+
const ageMs = cached ? (Date.now() - cached.ts) : Infinity;
|
|
327
|
+
const needFetch = opts.force || !cached || ageMs > _STATUS_REFRESH_WINDOW_MS;
|
|
328
|
+
if (!needFetch) return;
|
|
265
329
|
let status = null;
|
|
330
|
+
// Electron preload — preferred (читает свой settings + дёргает auth~me).
|
|
266
331
|
try { status = await window.appChatium?.status?.(); } catch {}
|
|
332
|
+
// Browser-fallback: тот же ответ, но через server endpoint.
|
|
333
|
+
// appChatium в web undefined → status здесь будет null.
|
|
334
|
+
if (!status) {
|
|
335
|
+
try {
|
|
336
|
+
const r = await fetch('/api/auth/status');
|
|
337
|
+
if (r.ok) status = await r.json();
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
267
340
|
if (status) {
|
|
268
341
|
_writeStatusCache(status);
|
|
269
342
|
_renderIdentity(wrap, status);
|
|
270
|
-
} else if (!cached) {
|
|
343
|
+
} else if (!cached?.status) {
|
|
271
344
|
// Ни кэша, ни ответа — рисуем «не залогинен» по дефолту.
|
|
272
345
|
_renderIdentity(wrap, { connected: false });
|
|
273
346
|
}
|
|
@@ -275,33 +348,75 @@ async function renderWelcomeIdentity() {
|
|
|
275
348
|
function _renderIdentity(wrap, status) {
|
|
276
349
|
wrap.innerHTML = '';
|
|
277
350
|
if (status?.connected) {
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
351
|
+
// Chatium-профиль: всё в `userInfo` (displayName/firstName/lastName/
|
|
352
|
+
// confirmedEmail/confirmedPhone/imageUrl). Также проверяем legacy-
|
|
353
|
+
// имена (user/profile/account) — на случай других версий API.
|
|
354
|
+
const u = status.userInfo || status.user || status.profile || status.account || {};
|
|
355
|
+
let name = status.displayName || status.name || status.fullName
|
|
356
|
+
|| status.login || status.username || status.userName
|
|
357
|
+
|| u.displayName || u.name || u.fullName
|
|
358
|
+
|| (u.firstName && u.lastName ? `${u.firstName} ${u.lastName}` : null)
|
|
359
|
+
|| u.firstName || u.lastName
|
|
360
|
+
|| u.login || u.username
|
|
361
|
+
|| status.confirmedEmail || status.email
|
|
362
|
+
|| u.confirmedEmail || u.email
|
|
363
|
+
|| u.confirmedPhone || u.phone
|
|
364
|
+
|| null;
|
|
365
|
+
let sub = '';
|
|
366
|
+
// sub: email если основное имя — НЕ email; иначе userId-префикс.
|
|
367
|
+
const subEmail = u.confirmedEmail || u.email || status.confirmedEmail || status.email;
|
|
368
|
+
if (name && subEmail && subEmail !== name) {
|
|
369
|
+
sub = `· ${subEmail}`;
|
|
370
|
+
} else if (name && status.userId && status.userId !== name) {
|
|
371
|
+
sub = `· ${status.userId.slice(0, 8)}`;
|
|
372
|
+
}
|
|
373
|
+
// Avatar — слева от имени, если imageUrl есть. Через server proxy
|
|
374
|
+
// (CDN-картинка может требовать referer).
|
|
375
|
+
const avatarUrl = u.imageUrl || u.avatarUrl || u.photoUrl || null;
|
|
376
|
+
if (!name) {
|
|
377
|
+
// Fallback на userId-prefix.
|
|
378
|
+
name = '👤 ' + (status.userId || 'KingKont').slice(0, 10);
|
|
379
|
+
sub = status.userId ? `…${status.userId.slice(-4)}` : '';
|
|
380
|
+
}
|
|
289
381
|
// Диагностика: если ни одного «человеческого» поля не нашлось —
|
|
290
382
|
// громкий лог в консоль с полным дампом, чтобы видеть что прислал
|
|
291
383
|
// сервер (см. main.js [chatium:status] логи тоже).
|
|
292
384
|
const noHumanField = !status.displayName && !status.name && !status.fullName
|
|
293
385
|
&& !status.login && !status.confirmedEmail && !status.email
|
|
294
|
-
&& !u.displayName && !u.
|
|
386
|
+
&& !u.displayName && !u.firstName && !u.lastName && !u.confirmedEmail;
|
|
295
387
|
if (noHumanField) {
|
|
296
388
|
console.warn('[chat-identity] No human-readable name field. Status object:', status);
|
|
297
389
|
console.warn('[chat-identity] _allKeys:', status._allKeys);
|
|
298
390
|
console.warn('[chat-identity] _raw:', status._raw);
|
|
299
391
|
}
|
|
392
|
+
// Avatar (если есть imageUrl) — слева. Прокси через server, чтобы CDN
|
|
393
|
+
// не требовал referer. Lazy-error → скрыть img при failed load.
|
|
394
|
+
const avatarHtml = avatarUrl
|
|
395
|
+
? `<img src="/api/proxy?url=${encodeURIComponent(avatarUrl)}" alt="" class="who-avatar" onerror="this.remove()">`
|
|
396
|
+
: `<span style="color:#5c5; font-size:13px; line-height:1;">●</span>`;
|
|
300
397
|
wrap.innerHTML = `
|
|
301
|
-
|
|
398
|
+
${avatarHtml}
|
|
302
399
|
<span class="who">${escapeHtml(name)}</span>
|
|
303
400
|
<span class="who-sub">${escapeHtml(sub)}</span>
|
|
304
401
|
`;
|
|
402
|
+
// KingKont-кредиты — встраиваем chip ВНУТРИ identity-pill (юзер
|
|
403
|
+
// попросил «кредиты kingkont внеси туда же где профиль»). Берём
|
|
404
|
+
// из balances-кэша (refreshBalance его наполняет). Из общего
|
|
405
|
+
// welcomeStatusBalances kingkont теперь убирается (см. ниже).
|
|
406
|
+
const balances = _readBalancesCache();
|
|
407
|
+
const kk = balances?.kingkont;
|
|
408
|
+
if (kk && typeof kk.amount === 'number') {
|
|
409
|
+
const credits = document.createElement('span');
|
|
410
|
+
credits.className = 'who-credits';
|
|
411
|
+
const low = kk.amount > 0 && kk.amount < 100;
|
|
412
|
+
const empty = kk.amount <= 0;
|
|
413
|
+
if (low) credits.classList.add('low');
|
|
414
|
+
if (empty) credits.classList.add('empty');
|
|
415
|
+
credits.title = 'Баланс KingKont · клик — лог списаний';
|
|
416
|
+
credits.innerHTML = `<b>${kk.amount.toLocaleString('ru-RU')}</b> credits`;
|
|
417
|
+
credits.addEventListener('click', () => window.openTxLog?.());
|
|
418
|
+
wrap.appendChild(credits);
|
|
419
|
+
}
|
|
305
420
|
// Если имя не найдено — добавляем visible debug-pill с available keys,
|
|
306
421
|
// чтобы юзер мог тебе показать что отдаёт Chatium без копания в консоли.
|
|
307
422
|
if (noHumanField && Array.isArray(status._allKeys) && status._allKeys.length) {
|
|
@@ -316,7 +431,14 @@ function _renderIdentity(wrap, status) {
|
|
|
316
431
|
logoutBtn.title = 'Logout из KingKont';
|
|
317
432
|
logoutBtn.addEventListener('click', async () => {
|
|
318
433
|
if (!confirm('Выйти из KingKont?')) return;
|
|
319
|
-
|
|
434
|
+
// Electron: preload IPC. Web: server endpoint.
|
|
435
|
+
try {
|
|
436
|
+
if (window.appChatium?.logout) await window.appChatium.logout();
|
|
437
|
+
else await fetch('/api/auth/logout', { method: 'POST' });
|
|
438
|
+
} catch {}
|
|
439
|
+
// Очистим status-кэш и форсим re-render.
|
|
440
|
+
try { localStorage.removeItem('chatiumStatusCache'); } catch {}
|
|
441
|
+
renderWelcomeIdentity({ force: true }).catch(() => {});
|
|
320
442
|
});
|
|
321
443
|
wrap.appendChild(logoutBtn);
|
|
322
444
|
} else {
|
|
@@ -328,7 +450,21 @@ function _renderIdentity(wrap, status) {
|
|
|
328
450
|
loginBtn.textContent = 'Войти';
|
|
329
451
|
loginBtn.title = 'Войти в KingKont';
|
|
330
452
|
loginBtn.addEventListener('click', async () => {
|
|
331
|
-
|
|
453
|
+
// Electron: preload IPC (откроет внешний браузер, дождётся callback'а
|
|
454
|
+
// на random localhost-port). Web: server endpoint (этот же сервер
|
|
455
|
+
// принимает callback на /api/auth/login/callback).
|
|
456
|
+
if (window.appChatium?.login) {
|
|
457
|
+
try { await window.appChatium.login(); } catch (e) { alert('Login failed: ' + (e?.message || e)); }
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const r = await fetch('/api/auth/login/start', { method: 'POST' });
|
|
462
|
+
const d = await r.json();
|
|
463
|
+
if (!r.ok || !d?.loginUrl) throw new Error(d?.error || 'login start failed');
|
|
464
|
+
// Открываем в новой вкладке. После успеха она auto-close,
|
|
465
|
+
// а наша WS-подписка (см. _wireAuthWS) подтянет новый status.
|
|
466
|
+
window.open(d.loginUrl, '_blank', 'noopener');
|
|
467
|
+
} catch (e) { alert('Login failed: ' + (e?.message || e)); }
|
|
332
468
|
});
|
|
333
469
|
wrap.appendChild(loginBtn);
|
|
334
470
|
}
|
|
@@ -595,10 +731,18 @@ async function _renderWelcomeRecentsInner() {
|
|
|
595
731
|
|
|
596
732
|
// ☁ Облачные проекты — видна только если залогинен в Chatium.
|
|
597
733
|
// Создаёт серверную запись и открывает её как новый проект.
|
|
734
|
+
// Web fallback: если preload appSettings нет — спрашиваем сервер
|
|
735
|
+
// через /api/auth/status (тот же chatium-токен из settings.json).
|
|
598
736
|
let isLoggedIn = false;
|
|
599
737
|
try {
|
|
600
|
-
|
|
601
|
-
|
|
738
|
+
if (window.appSettings?.get) {
|
|
739
|
+
const s = await window.appSettings.get();
|
|
740
|
+
isLoggedIn = !!(s?.useChatium && s?.chatium?.token);
|
|
741
|
+
} else {
|
|
742
|
+
const r = await fetch('/api/auth/status');
|
|
743
|
+
const st = await r.json();
|
|
744
|
+
isLoggedIn = !!st?.connected;
|
|
745
|
+
}
|
|
602
746
|
} catch {}
|
|
603
747
|
if (isLoggedIn) {
|
|
604
748
|
const cloudCard = document.createElement('div');
|
|
@@ -1393,65 +1537,77 @@ async function openFilm(handle) {
|
|
|
1393
1537
|
// Закрыть текущий проект: выгрузить state, скрыть секции, восстановить
|
|
1394
1538
|
// шапку, оставить запись в idb (чтобы Recent работал).
|
|
1395
1539
|
async function closeProject() {
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
|
|
1540
|
+
// Каждый шаг wrap'нут try/catch — раньше любой throw ломал весь close
|
|
1541
|
+
// (юзер видел «приложение падает», на самом деле renderer вис в полузакрытом
|
|
1542
|
+
// состоянии). Логируем что упало, но продолжаем дальше.
|
|
1543
|
+
const safe = (label, fn) => {
|
|
1544
|
+
try { return fn(); }
|
|
1545
|
+
catch (e) { console.warn(`[closeProject] ${label} failed:`, e?.message || e); }
|
|
1546
|
+
};
|
|
1547
|
+
const safeAwait = async (label, fn) => {
|
|
1548
|
+
try { await fn(); }
|
|
1549
|
+
catch (e) { console.warn(`[closeProject] ${label} failed:`, e?.message || e); }
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
safe('persist welcome flag', () => {
|
|
1399
1553
|
localStorage.setItem('welcomeOnNextStart', '1');
|
|
1400
1554
|
localStorage.setItem('lastLocation', 'welcome');
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
if (
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
//
|
|
1417
|
-
if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
|
|
1555
|
+
});
|
|
1556
|
+
await safeAwait('chat resetInMemory', async () => {
|
|
1557
|
+
if (window.kingChat?.resetInMemory) await window.kingChat.resetInMemory();
|
|
1558
|
+
});
|
|
1559
|
+
safe('chat close', () => { window.kingChat?.close?.(); });
|
|
1560
|
+
safe('stopExternalWatcher', () => { if (typeof stopExternalWatcher === 'function') stopExternalWatcher(); });
|
|
1561
|
+
safe('resetTimelineUI', () => { if (typeof resetTimelineUI === 'function') resetTimelineUI(); });
|
|
1562
|
+
safe('revoke board urls', () => {
|
|
1563
|
+
if (state.currentBoard?.urls) {
|
|
1564
|
+
for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
safe('revokeMentionThumbCache', () => {
|
|
1568
|
+
if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
|
|
1569
|
+
});
|
|
1570
|
+
// Reset state.
|
|
1418
1571
|
state.filmHandle = null;
|
|
1419
1572
|
state.currentBoard = null;
|
|
1420
|
-
// Сбрасываем cloud-маркеры (если был открыт облачный проект).
|
|
1421
1573
|
state.cloudProjectId = null;
|
|
1422
1574
|
state.cloudDirty = false;
|
|
1423
|
-
window.appProject?.notifyState(false);
|
|
1424
|
-
|
|
1425
|
-
|
|
1575
|
+
safe('notifyState', () => { window.appProject?.notifyState(false); });
|
|
1576
|
+
safe('dispatch project-changed', () => {
|
|
1577
|
+
window.dispatchEvent(new CustomEvent('project-changed'));
|
|
1578
|
+
});
|
|
1426
1579
|
state.charactersInfo = [];
|
|
1427
1580
|
state.locationsInfo = [];
|
|
1428
1581
|
state.selectedNodeIds.clear();
|
|
1429
1582
|
state.selectedClipIds.clear();
|
|
1430
1583
|
state.selectedTrackIds.clear();
|
|
1431
1584
|
document.body.classList.add('no-project');
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1585
|
+
safe('cloud setVisibility', () => { window.cloudProjects?.setVisibility?.(); });
|
|
1586
|
+
safe('reset sidebar header', () => {
|
|
1587
|
+
const sub = $('brandSub');
|
|
1588
|
+
if (sub) { sub.textContent = 'Видео-редактор'; sub.classList.remove('has-project'); }
|
|
1589
|
+
const boardEl = $('brandBoard');
|
|
1590
|
+
if (boardEl) { boardEl.style.display = 'none'; boardEl.innerHTML = ''; }
|
|
1591
|
+
const rootInfo = $('rootInfo'); if (rootInfo) rootInfo.textContent = '';
|
|
1592
|
+
});
|
|
1593
|
+
safe('clear sidebar lists', () => {
|
|
1594
|
+
for (const id of ['characterList','locationList','episodeList']) {
|
|
1595
|
+
const el = $(id); if (el) el.innerHTML = '';
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
safe('disable buttons', () => {
|
|
1599
|
+
for (const id of ['newEpisode','newCharacter','newLocation','repliquesBtn',
|
|
1600
|
+
'addText','genText','genAudio','genImage','genVideo','genSfx','genMusic']) {
|
|
1601
|
+
const b = $(id); if (b) b.disabled = true;
|
|
1602
|
+
}
|
|
1603
|
+
const bb = $('boardBadge'); if (bb) bb.style.display = 'none';
|
|
1604
|
+
const cs = $('charSettingsBtn'); if (cs) cs.style.display = 'none';
|
|
1605
|
+
});
|
|
1606
|
+
safe('clearCanvasKeepSvg', () => {
|
|
1607
|
+
if (typeof clearCanvasKeepSvg === 'function') clearCanvasKeepSvg();
|
|
1608
|
+
});
|
|
1609
|
+
await safeAwait('renderWelcomeRecents', () => renderWelcomeRecents());
|
|
1610
|
+
safe('showEmpty', () => { if (typeof showEmpty === 'function') showEmpty(); });
|
|
1455
1611
|
}
|
|
1456
1612
|
|
|
1457
1613
|
// =================== Sidebar ===================
|
|
@@ -2756,7 +2912,16 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
2756
2912
|
if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
|
|
2757
2913
|
if (node.status === 'generating') {
|
|
2758
2914
|
add('⏹ Остановить', () => stopJob(node.id));
|
|
2759
|
-
|
|
2915
|
+
// «Перезапустить» — открывает модалку с предзаполненными параметрами
|
|
2916
|
+
// (как «Перегенерировать» для готовой ноды). Раньше тут был
|
|
2917
|
+
// restartJob(id) — рестартил без модалки. Юзер: «нужно чтобы показывал
|
|
2918
|
+
// как раньше». Сначала стопим текущую генерацию, чтобы regenerateNode
|
|
2919
|
+
// пропустил guard `if (status==='generating') return`.
|
|
2920
|
+
add('↻ Перезапустить', async () => {
|
|
2921
|
+
await stopJob(node.id);
|
|
2922
|
+
// status уже сброшен на 'error' в stopJob → regenerateNode пройдёт.
|
|
2923
|
+
regenerateNode(node);
|
|
2924
|
+
});
|
|
2760
2925
|
} else if (node.status === 'draft') {
|
|
2761
2926
|
add('▶ Запустить генерацию', () => regenerateNode(node));
|
|
2762
2927
|
add('✎ Изменить и запустить', () => regenerateNode(node));
|
package/renderer/cloudFs.js
CHANGED
|
@@ -288,5 +288,13 @@
|
|
|
288
288
|
getCloudProjectId,
|
|
289
289
|
// exposed для save-to-server: пройтись по всем файлам in-memory map'а.
|
|
290
290
|
_memProject,
|
|
291
|
+
// Web-only helper: прямой write в backend этого проекта. Используется
|
|
292
|
+
// в cloudProjects.openCloudProject для скачивания manifest+файлов с
|
|
293
|
+
// сервера в память (window.cloudFs только в Electron preload).
|
|
294
|
+
async writeFile(projectId, relPath, data) {
|
|
295
|
+
const backend = makeBackend(projectId);
|
|
296
|
+
await backend.ensure();
|
|
297
|
+
return backend.write(relPath, data);
|
|
298
|
+
},
|
|
291
299
|
};
|
|
292
300
|
})();
|
|
@@ -25,7 +25,29 @@
|
|
|
25
25
|
// -------------- helpers --------------
|
|
26
26
|
function $(id) { return document.getElementById(id); }
|
|
27
27
|
function isLoggedIn(s) { return !!(s?.useChatium && s?.chatium?.token); }
|
|
28
|
-
|
|
28
|
+
// Electron: preload IPC возвращает полный settings.json. В web preload
|
|
29
|
+
// недоступен → опрашиваем /api/auth/status и эмулируем settings-shape
|
|
30
|
+
// (с token-плейсхолдером — реальный токен на сервере, renderer'у он
|
|
31
|
+
// не нужен напрямую, HTTP-запросы идут на наш же сервер с auth-header'ом).
|
|
32
|
+
async function getSettings() {
|
|
33
|
+
if (window.appSettings?.get) {
|
|
34
|
+
try { return await window.appSettings.get(); } catch { return {}; }
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const r = await fetch('/api/auth/status');
|
|
38
|
+
const status = await r.json();
|
|
39
|
+
if (status?.connected) {
|
|
40
|
+
return {
|
|
41
|
+
useChatium: true,
|
|
42
|
+
useOpenrouter: true,
|
|
43
|
+
useElevenlabs: true,
|
|
44
|
+
useKie: true,
|
|
45
|
+
chatium: { token: 'present', userId: status.userId, base: status.base },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
29
51
|
|
|
30
52
|
// Прогресс — переиспользуем существующий tplProgress из templates.js.
|
|
31
53
|
const PROGRESS = (typeof TPL_PROGRESS !== 'undefined') ? TPL_PROGRESS : {
|
|
@@ -360,8 +382,11 @@
|
|
|
360
382
|
|
|
361
383
|
async function writeCloudFile(projectId, relPath, data) {
|
|
362
384
|
if (window.cloudFs) return window.cloudFs.write(projectId, relPath, data);
|
|
363
|
-
//
|
|
364
|
-
//
|
|
385
|
+
// Web fallback: пишем в shared in-memory backend, тот же что
|
|
386
|
+
// используют cloudFsShim handle'ы (т.е. файлы появятся в filmHandle
|
|
387
|
+
// через FSAH-like API). Без этого openCloudProject в браузере
|
|
388
|
+
// download'ил manifest+файлы и просто терял их → sidebar пустой.
|
|
389
|
+
if (window.cloudFsShim?.writeFile) return window.cloudFsShim.writeFile(projectId, relPath, data);
|
|
365
390
|
return false;
|
|
366
391
|
}
|
|
367
392
|
|
package/renderer/generate.js
CHANGED
|
@@ -2065,7 +2065,10 @@ async function pollJob(job, nodeId, bKey, boardHandle, kind) {
|
|
|
2065
2065
|
}
|
|
2066
2066
|
if (pd.status === 'done') {
|
|
2067
2067
|
logJob(nodeId, `done, downloading ${pd.url?.slice(0,80)}`);
|
|
2068
|
-
|
|
2068
|
+
// data: URLs (OpenRouter image-gen возвращает inline base64) — берём
|
|
2069
|
+
// напрямую без proxy. Браузерный fetch их понимает natively.
|
|
2070
|
+
const fetchUrl = pd.url.startsWith('data:') ? pd.url : `/api/proxy?url=${encodeURIComponent(pd.url)}`;
|
|
2071
|
+
const blob = await (await fetch(fetchUrl)).blob();
|
|
2069
2072
|
const ext = kind === 'image' ? 'jpg' : 'mp4';
|
|
2070
2073
|
const sub = kind === 'image' ? 'frames' : 'clips';
|
|
2071
2074
|
const dir = await getOrCreateBoardSubdir(boardHandle, sub);
|