kingkont 0.17.2 → 0.17.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/index.html CHANGED
@@ -612,6 +612,8 @@
612
612
  <script src="renderer/cloudProjects.js"></script>
613
613
  <!-- chat.js — Claude-чат для управления сценой через tools (Cmd+J). -->
614
614
  <script src="renderer/chat.js"></script>
615
+ <!-- notifyPanel.js — окно событий (генерации, чат-ответы, ошибки). 🔔 в углу. -->
616
+ <script src="renderer/notifyPanel.js"></script>
615
617
 
616
618
  </body>
617
619
  </html>
package/lib/jobsHub.js CHANGED
@@ -36,16 +36,25 @@ function start({ projectKey, jobId, kind, name, type, taskId }) {
36
36
  if (!projectKey || !jobId) return;
37
37
  let m = jobsByProject.get(projectKey);
38
38
  if (!m) { m = new Map(); jobsByProject.set(projectKey, m); }
39
- m.set(jobId, {
40
- kind: kind || null, name: name || null, type: type || null,
41
- taskId: taskId || null, startedAt: Date.now(),
42
- });
39
+ // MERGE с существующей записью — иначе второй call (например
40
+ // bgJobUpdateTaskId) затёр бы kind/name nullами. Это и был bug:
41
+ // после получения taskId server-side poller не стартовал потому что
42
+ // kind становился null.
43
+ const existing = m.get(jobId) || {};
44
+ const merged = {
45
+ kind: kind ?? existing.kind ?? null,
46
+ name: name ?? existing.name ?? null,
47
+ type: type ?? existing.type ?? null,
48
+ taskId: taskId ?? existing.taskId ?? null,
49
+ startedAt: existing.startedAt || Date.now(),
50
+ };
51
+ m.set(jobId, merged);
52
+ console.log('[jobsHub] start/update', { projectKey, jobId, ...merged });
43
53
  _emit(projectKey);
44
- // Если есть taskId + kind — стартуем серверный poller. Renderer тоже
45
- // полит параллельно (старый flow), но если он закрыт — серверный
46
- // продолжает и шлёт WS-event на completion.
47
- if (taskId && (kind === 'image' || kind === 'video')) {
48
- _startPoller({ projectKey, jobId, taskId, kind });
54
+ // Если есть И taskId, И kind — стартуем серверный poller. Renderer тоже
55
+ // полит параллельно. Если renderer закрыт — серверный продолжает.
56
+ if (merged.taskId && (merged.kind === 'image' || merged.kind === 'video')) {
57
+ _startPoller({ projectKey, jobId, taskId: merged.taskId, kind: merged.kind });
49
58
  }
50
59
  }
51
60
 
@@ -62,19 +71,27 @@ function end({ projectKey, jobId }) {
62
71
  }
63
72
 
64
73
  function _startPoller({ projectKey, jobId, taskId, kind }) {
65
- if (pollers.has(jobId)) return;
74
+ if (pollers.has(jobId)) {
75
+ console.log('[jobsHub] poller already running for', jobId);
76
+ return;
77
+ }
78
+ console.log('[jobsHub] _startPoller', { projectKey, jobId, taskId, kind });
66
79
  const providers = require('./providers');
67
80
  const tick = async () => {
68
81
  const entry = pollers.get(jobId);
69
82
  if (!entry) return;
70
83
  entry.lastPollAt = Date.now();
71
84
  let r;
72
- try { r = await providers.pollGeneration(taskId, _settingsGetter()); }
85
+ try {
86
+ r = await providers.pollGeneration(taskId, _settingsGetter());
87
+ console.log('[jobsHub] poll', jobId, '→', r.status, r.state || '');
88
+ }
73
89
  catch (e) {
74
90
  console.warn('[jobsHub] poll failed for', jobId, e?.message);
75
91
  return; // следующий tick попробует снова
76
92
  }
77
93
  if (r.status === 'done') {
94
+ console.log('[jobsHub] DONE', jobId, '→', r.url);
78
95
  _stopPoller(jobId);
79
96
  // Push WS event с completion-info. Renderer (если жив) скачает
80
97
  // через /api/proxy и запишет в FSAH. Если renderer закрыт — event
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -1853,8 +1853,9 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
1853
1853
  logJob(node.id, `taskId=${data.taskId}`);
1854
1854
  // Сообщаем server-side poller'у taskId — теперь сервер тоже опрашивает
1855
1855
  // провайдера. Если renderer закроется/Cmd+R — server продолжит и
1856
- // пушнёт WS-event на completion.
1857
- if (typeof bgJobUpdateTaskId === 'function') bgJobUpdateTaskId(node.id, data.taskId, job.projectKey);
1856
+ // пушнёт WS-event на completion. ВАЖНО передать kind — без него
1857
+ // server-merge не запустит poller (kind остался бы null после второго call).
1858
+ if (typeof bgJobUpdateTaskId === 'function') bgJobUpdateTaskId(node.id, data.taskId, job.projectKey, kind);
1858
1859
  await mutateNode(bKey, boardHandle, node.id, n => {
1859
1860
  n.generated = { ...(n.generated || {}), taskId: data.taskId, state: 'queued' };
1860
1861
  });
@@ -0,0 +1,156 @@
1
+ // renderer/notifyPanel.js — Окно уведомлений (event log).
2
+ //
3
+ // Отдельная панель для всех bg-event'ов: запуск/завершение генераций,
4
+ // чат-ответы, ошибки. Раскрывается кликом на 🔔 в шапке. История
5
+ // держится in-memory (~100 последних) — не персистится.
6
+ //
7
+ // Источники событий (всё через addEvent):
8
+ // - showToast (в state.js) — оборачиваем чтобы дублировать в панель
9
+ // - WS jobs:all kind:done — server-side gen completion
10
+ // - WS chat:<key> kind:final — chat final response
11
+ // - bgjobs:changed — start/end jobs
12
+ //
13
+ // Бейдж на 🔔 — счётчик новых событий (увиденных = открытие панели).
14
+
15
+ (function () {
16
+ function $(id) { return document.getElementById(id); }
17
+ const events = []; // [{ts, kind, text, projectKey?, raw?}]
18
+ const MAX_EVENTS = 100;
19
+ let unread = 0;
20
+ let panelOpen = false;
21
+
22
+ function _ensureUI() {
23
+ if ($('notifyBtn')) return;
24
+ // Кнопка-колокольчик в верхнем тулбаре справа.
25
+ const btn = document.createElement('button');
26
+ btn.id = 'notifyBtn';
27
+ btn.title = 'События (генерации, чат, ошибки)';
28
+ btn.style.cssText = 'position:fixed; top:12px; right:24px; z-index:9998; background:rgba(30,30,40,0.85); border:1px solid #444; color:#ccc; font-size:14px; width:32px; height:32px; border-radius:50%; cursor:pointer; backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center;';
29
+ btn.innerHTML = '🔔';
30
+ document.body.appendChild(btn);
31
+ btn.addEventListener('click', toggle);
32
+ // Бейдж со счётчиком unread.
33
+ const badge = document.createElement('span');
34
+ badge.id = 'notifyBadge';
35
+ badge.style.cssText = 'position:absolute; top:-4px; right:-4px; background:#e33377; color:#fff; font-size:9px; min-width:14px; height:14px; line-height:14px; text-align:center; border-radius:8px; padding:0 4px; display:none; pointer-events:none;';
36
+ btn.appendChild(badge);
37
+
38
+ // Панель.
39
+ const panel = document.createElement('div');
40
+ panel.id = 'notifyPanel';
41
+ panel.style.cssText = 'position:fixed; top:50px; right:16px; width:380px; max-height:75vh; background:#1a1a1a; border:1px solid #333; border-radius:8px; z-index:9999; display:none; flex-direction:column; box-shadow:0 8px 32px rgba(0,0,0,0.55); overflow:hidden;';
42
+ panel.innerHTML = `
43
+ <div style="padding:10px 12px; border-bottom:1px solid #2a2a2a; display:flex; align-items:center; gap:8px;">
44
+ <strong style="font-size:13px; color:#ddd;">События</strong>
45
+ <span style="flex:1;"></span>
46
+ <button id="notifyClear" title="Очистить" style="background:transparent; border:1px solid #333; color:#aaa; padding:2px 8px; border-radius:4px; cursor:pointer; font-size:11px;">⌫</button>
47
+ <button id="notifyClose" title="Закрыть" style="background:transparent; border:1px solid #333; color:#aaa; padding:2px 8px; border-radius:4px; cursor:pointer; font-size:11px;">×</button>
48
+ </div>
49
+ <div id="notifyList" style="flex:1; overflow-y:auto; padding:6px;"></div>
50
+ `;
51
+ document.body.appendChild(panel);
52
+ $('notifyClose').addEventListener('click', () => setOpen(false));
53
+ $('notifyClear').addEventListener('click', () => { events.length = 0; unread = 0; render(); });
54
+ }
55
+
56
+ function setOpen(open) {
57
+ _ensureUI();
58
+ panelOpen = open;
59
+ $('notifyPanel').style.display = open ? 'flex' : 'none';
60
+ if (open) { unread = 0; updateBadge(); render(); }
61
+ }
62
+ function toggle() { setOpen(!panelOpen); }
63
+
64
+ function updateBadge() {
65
+ const b = $('notifyBadge');
66
+ if (!b) return;
67
+ if (unread > 0) {
68
+ b.textContent = unread > 99 ? '99+' : String(unread);
69
+ b.style.display = '';
70
+ } else b.style.display = 'none';
71
+ }
72
+
73
+ function render() {
74
+ const list = $('notifyList');
75
+ if (!list) return;
76
+ list.innerHTML = '';
77
+ if (!events.length) {
78
+ list.innerHTML = '<div style="padding:24px; text-align:center; color:#555; font-size:12px;">Нет событий</div>';
79
+ return;
80
+ }
81
+ for (let i = events.length - 1; i >= 0; i--) {
82
+ const e = events[i];
83
+ const row = document.createElement('div');
84
+ const colors = {
85
+ ok: '#3a6a3a', error: '#8a3a3a', info: '#3a5a8a', warn: '#8a7a3a', chat: '#7a4a8a',
86
+ };
87
+ const c = colors[e.kind] || colors.info;
88
+ const dt = new Date(e.ts);
89
+ const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
90
+ row.style.cssText = `padding:8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:default;`;
91
+ row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start;">
92
+ <div style="color:#e0e0e0; font-size:12px; line-height:1.4; word-break:break-word; flex:1;"></div>
93
+ <span style="color:#777; font-size:10px; font-family:ui-monospace,monospace; flex-shrink:0;">${time}</span>
94
+ </div>`;
95
+ row.querySelector('div > div').textContent = e.text;
96
+ list.appendChild(row);
97
+ }
98
+ }
99
+
100
+ // Public API.
101
+ function addEvent({ kind, text, raw }) {
102
+ if (!text) return;
103
+ events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw });
104
+ if (events.length > MAX_EVENTS) events.shift();
105
+ if (!panelOpen) {
106
+ unread++;
107
+ updateBadge();
108
+ } else {
109
+ render();
110
+ }
111
+ }
112
+ window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
113
+
114
+ // Init: ensure UI exists ASAP, hook into showToast и WS-events.
115
+ document.addEventListener('DOMContentLoaded', () => {
116
+ _ensureUI();
117
+ // Wrap showToast чтобы дублировать в панель.
118
+ if (typeof window.showToast === 'function') {
119
+ const orig = window.showToast;
120
+ window.showToast = function (text, kind) {
121
+ try { addEvent({ kind: kind || 'info', text }); } catch {}
122
+ return orig.apply(this, arguments);
123
+ };
124
+ }
125
+ // WS-канал jobs:all для start/end/done/failed events. Connect к /ws.
126
+ function _connect() {
127
+ let ws;
128
+ try {
129
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
130
+ ws = new WebSocket(`${proto}://${location.host}/ws`);
131
+ } catch { setTimeout(_connect, 5000); return; }
132
+ ws.onopen = () => {
133
+ try { ws.send(JSON.stringify({ type: 'subscribe', channel: 'jobs:all' })); } catch {}
134
+ };
135
+ ws.onmessage = (ev) => {
136
+ let m; try { m = JSON.parse(ev.data); } catch { return; }
137
+ if (m?.type !== 'event') return;
138
+ const e = m.event || {};
139
+ const ch = m.channel || '';
140
+ if (ch === 'jobs:all') {
141
+ if (e.kind === 'done') {
142
+ addEvent({ kind: 'ok', text: `✓ ${e.kindOf || 'gen'} готов в ${e.projectKey || ''} (jobId=${(e.jobId||'').slice(0,8)})` });
143
+ } else if (e.kind === 'failed') {
144
+ addEvent({ kind: 'error', text: `⚠ ${e.kindOf || 'gen'} провалился: ${(e.error || '').slice(0, 100)}` });
145
+ } else if (e.kind === 'changed' && Array.isArray(e.list)) {
146
+ // Только при изменении (start/end) — добавим краткую инфу.
147
+ // Без шума — addEvent только если что-то стало активно или закончилось.
148
+ }
149
+ }
150
+ };
151
+ ws.onclose = () => setTimeout(_connect, 5000);
152
+ ws.onerror = () => {};
153
+ }
154
+ _connect();
155
+ });
156
+ })();
package/renderer/state.js CHANGED
@@ -108,12 +108,13 @@ function bgJobStart(info) {
108
108
  }).catch(() => {});
109
109
  }
110
110
  // Update taskId for an existing job (server didn't have it on initial track).
111
- function bgJobUpdateTaskId(nodeId, taskId, projectKey) {
111
+ // kind важен без него server-side merge оставит null и poller не стартует.
112
+ function bgJobUpdateTaskId(nodeId, taskId, projectKey, kind) {
112
113
  const pk = projectKey || _projectKeyForCurrent();
113
114
  if (!pk || !taskId) return;
114
115
  fetch('/api/jobs/track', {
115
116
  method: 'POST', headers: { 'Content-Type': 'application/json' },
116
- body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId }),
117
+ body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId, kind }),
117
118
  }).catch(() => {});
118
119
  }
119
120
  window.bgJobUpdateTaskId = bgJobUpdateTaskId;