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 +2 -0
- package/lib/jobsHub.js +28 -11
- package/package.json +1 -1
- package/renderer/generate.js +3 -2
- package/renderer/notifyPanel.js +156 -0
- package/renderer/state.js +3 -2
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
45
|
-
// полит
|
|
46
|
-
|
|
47
|
-
|
|
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))
|
|
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 {
|
|
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
package/renderer/generate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|