kingkont 0.16.2 → 0.17.1
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 +29 -1
- package/lib/jobsHub.js +84 -6
- package/package.json +1 -1
- package/renderer/board.js +89 -0
- package/renderer/chat.js +17 -0
- package/renderer/generate.js +4 -0
- package/renderer/state.js +21 -5
- package/server.js +5 -2
package/lib/chatSession.js
CHANGED
|
@@ -199,7 +199,17 @@ async function _runLoop(session, system, settingsGetter) {
|
|
|
199
199
|
session.busy = true;
|
|
200
200
|
session.lastError = null;
|
|
201
201
|
schedulePersist(session);
|
|
202
|
+
// Регистрируем чат как bg-job этого проекта — счётчик «⏳ N в фоне»
|
|
203
|
+
// на welcome-карточке покажет ОБА: генерации нод И активный чат.
|
|
204
|
+
// jobId = 'chat:<sessionKey>' — стабильный, чтобы start был idempotent.
|
|
205
|
+
const jobsHub = require('./jobsHub');
|
|
206
|
+
const projectKey = session.key; // session.key УЖЕ имеет формат 'cloud:..'/'folder:..'
|
|
207
|
+
const chatJobId = 'chat:' + session.key;
|
|
208
|
+
try {
|
|
209
|
+
jobsHub.start({ projectKey, jobId: chatJobId, kind: 'chat', name: 'Чат думает', type: 'chat' });
|
|
210
|
+
} catch {}
|
|
202
211
|
let iter = 0;
|
|
212
|
+
let lastFinalText = '';
|
|
203
213
|
try {
|
|
204
214
|
while (iter < MAX_TOOL_ITERATIONS) {
|
|
205
215
|
iter++;
|
|
@@ -209,7 +219,12 @@ async function _runLoop(session, system, settingsGetter) {
|
|
|
209
219
|
const assistantMsg = { role: 'assistant', content: cleanText, tools: [] };
|
|
210
220
|
session.history.push(assistantMsg);
|
|
211
221
|
schedulePersist(session);
|
|
212
|
-
if (!toolCalls.length)
|
|
222
|
+
if (!toolCalls.length) {
|
|
223
|
+
// Финальный ответ — ни одного tool. Запоминаем чтобы потом
|
|
224
|
+
// notify клиента (separate WS event 'final').
|
|
225
|
+
lastFinalText = cleanText;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
213
228
|
// Выставляем pendingToolCalls и ЖДЁМ что клиент пришлёт results.
|
|
214
229
|
// _waitForToolResults возвращает массив {id, ok, result, error}.
|
|
215
230
|
const results = await _waitForToolResults(session, toolCalls);
|
|
@@ -241,6 +256,19 @@ async function _runLoop(session, system, settingsGetter) {
|
|
|
241
256
|
session.pendingToolCalls = null;
|
|
242
257
|
session.pendingResolve = null;
|
|
243
258
|
schedulePersist(session);
|
|
259
|
+
// Завершаем chat-job на сервере (welcome-badge -1).
|
|
260
|
+
try { jobsHub.end({ projectKey, jobId: chatJobId }); } catch {}
|
|
261
|
+
// Push 'final'-event ТОЛЬКО для финального ответа (не для intermediate
|
|
262
|
+
// tool-iterations). Renderer слушает 'chat:<key>' и при event.kind='final'
|
|
263
|
+
// показывает toast/system-notification — даже если чат-панель скрыта или
|
|
264
|
+
// юзер на другой сцене.
|
|
265
|
+
if (lastFinalText || session.lastError) {
|
|
266
|
+
wsHub.publish('chat:' + session.key, {
|
|
267
|
+
kind: 'final',
|
|
268
|
+
text: (lastFinalText || '').slice(0, 240),
|
|
269
|
+
error: session.lastError || null,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
244
272
|
}
|
|
245
273
|
}
|
|
246
274
|
|
package/lib/jobsHub.js
CHANGED
|
@@ -15,22 +15,43 @@ const wsHub = require('./wsHub');
|
|
|
15
15
|
|
|
16
16
|
const jobsByProject = new Map(); // projectKey → Map<jobId, jobInfo>
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Серверные polling-таймеры. Для каждого jobId с known taskId+kind
|
|
19
|
+
// сервер сам опрашивает провайдера каждые POLL_INTERVAL_MS. На completion
|
|
20
|
+
// — WS-event с готовым URL+cost. Renderer (если жив) скачивает + пишет
|
|
21
|
+
// файл; если был закрыт — при возврате найдёт по scanAllBoardsForPendingJobs.
|
|
22
|
+
const pollers = new Map(); // jobId → { intervalId, taskId, kind, projectKey, lastPollAt }
|
|
23
|
+
const POLL_INTERVAL_MS = 4000;
|
|
24
|
+
|
|
25
|
+
let _settingsGetter = () => ({});
|
|
26
|
+
function setSettingsGetter(fn) { if (typeof fn === 'function') _settingsGetter = fn; }
|
|
27
|
+
|
|
28
|
+
function _emit(projectKey, extra) {
|
|
19
29
|
const list = listForProject(projectKey);
|
|
20
|
-
|
|
21
|
-
wsHub.publish('jobs:
|
|
30
|
+
const evt = { kind: 'changed', list, ...(extra || {}) };
|
|
31
|
+
wsHub.publish('jobs:' + projectKey, evt);
|
|
32
|
+
wsHub.publish('jobs:all', { ...evt, projectKey });
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
function start({ projectKey, jobId, kind, name, type }) {
|
|
35
|
+
function start({ projectKey, jobId, kind, name, type, taskId }) {
|
|
25
36
|
if (!projectKey || !jobId) return;
|
|
26
37
|
let m = jobsByProject.get(projectKey);
|
|
27
38
|
if (!m) { m = new Map(); jobsByProject.set(projectKey, m); }
|
|
28
|
-
m.set(jobId, {
|
|
39
|
+
m.set(jobId, {
|
|
40
|
+
kind: kind || null, name: name || null, type: type || null,
|
|
41
|
+
taskId: taskId || null, startedAt: Date.now(),
|
|
42
|
+
});
|
|
29
43
|
_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 });
|
|
49
|
+
}
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
function end({ projectKey, jobId }) {
|
|
33
53
|
if (!projectKey || !jobId) return null;
|
|
54
|
+
_stopPoller(jobId);
|
|
34
55
|
const m = jobsByProject.get(projectKey);
|
|
35
56
|
if (!m) return null;
|
|
36
57
|
const job = m.get(jobId);
|
|
@@ -40,6 +61,63 @@ function end({ projectKey, jobId }) {
|
|
|
40
61
|
return job || null;
|
|
41
62
|
}
|
|
42
63
|
|
|
64
|
+
function _startPoller({ projectKey, jobId, taskId, kind }) {
|
|
65
|
+
if (pollers.has(jobId)) return;
|
|
66
|
+
const providers = require('./providers');
|
|
67
|
+
const tick = async () => {
|
|
68
|
+
const entry = pollers.get(jobId);
|
|
69
|
+
if (!entry) return;
|
|
70
|
+
entry.lastPollAt = Date.now();
|
|
71
|
+
let r;
|
|
72
|
+
try { r = await providers.pollGeneration(taskId, _settingsGetter()); }
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.warn('[jobsHub] poll failed for', jobId, e?.message);
|
|
75
|
+
return; // следующий tick попробует снова
|
|
76
|
+
}
|
|
77
|
+
if (r.status === 'done') {
|
|
78
|
+
_stopPoller(jobId);
|
|
79
|
+
// Push WS event с completion-info. Renderer (если жив) скачает
|
|
80
|
+
// через /api/proxy и запишет в FSAH. Если renderer закрыт — event
|
|
81
|
+
// пропадёт; при возврате scanAllBoardsForPendingJobs восстановит
|
|
82
|
+
// (там status='generating' + taskId в scene.json).
|
|
83
|
+
wsHub.publish('jobs:' + projectKey, {
|
|
84
|
+
kind: 'done', jobId, taskId, url: r.url, cost: r.cost ?? null, kindOf: kind, provider: r.provider,
|
|
85
|
+
});
|
|
86
|
+
wsHub.publish('jobs:all', {
|
|
87
|
+
kind: 'done', projectKey, jobId, taskId, url: r.url, cost: r.cost ?? null, kindOf: kind, provider: r.provider,
|
|
88
|
+
});
|
|
89
|
+
// Job-end ОТЛОЖЕН — ждём ack от клиента (он скачает файл).
|
|
90
|
+
// Но если за 60s никто не пришёл — auto-end (чтобы счётчики не зависали).
|
|
91
|
+
setTimeout(() => {
|
|
92
|
+
if (!pollers.has(jobId)) end({ projectKey, jobId });
|
|
93
|
+
}, 60000);
|
|
94
|
+
} else if (r.status === 'error') {
|
|
95
|
+
_stopPoller(jobId);
|
|
96
|
+
wsHub.publish('jobs:' + projectKey, {
|
|
97
|
+
kind: 'failed', jobId, taskId, error: r.error, kindOf: kind,
|
|
98
|
+
});
|
|
99
|
+
wsHub.publish('jobs:all', {
|
|
100
|
+
kind: 'failed', projectKey, jobId, taskId, error: r.error, kindOf: kind,
|
|
101
|
+
});
|
|
102
|
+
// На ошибке тоже auto-end через 60s.
|
|
103
|
+
setTimeout(() => {
|
|
104
|
+
if (!pollers.has(jobId)) end({ projectKey, jobId });
|
|
105
|
+
}, 60000);
|
|
106
|
+
}
|
|
107
|
+
// pending → ждём следующий tick (interval сам зовёт)
|
|
108
|
+
};
|
|
109
|
+
// Первый poll — через 2s (даём провайдеру время принять задачу).
|
|
110
|
+
setTimeout(tick, 2000);
|
|
111
|
+
const intervalId = setInterval(tick, POLL_INTERVAL_MS);
|
|
112
|
+
pollers.set(jobId, { intervalId, taskId, kind, projectKey, lastPollAt: 0 });
|
|
113
|
+
}
|
|
114
|
+
function _stopPoller(jobId) {
|
|
115
|
+
const entry = pollers.get(jobId);
|
|
116
|
+
if (!entry) return;
|
|
117
|
+
clearInterval(entry.intervalId);
|
|
118
|
+
pollers.delete(jobId);
|
|
119
|
+
}
|
|
120
|
+
|
|
43
121
|
function listForProject(projectKey) {
|
|
44
122
|
const m = jobsByProject.get(projectKey);
|
|
45
123
|
if (!m) return [];
|
|
@@ -54,4 +132,4 @@ function listAll() {
|
|
|
54
132
|
return out;
|
|
55
133
|
}
|
|
56
134
|
|
|
57
|
-
module.exports = { start, end, listForProject, listAll };
|
|
135
|
+
module.exports = { start, end, listForProject, listAll, setSettingsGetter };
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -632,6 +632,91 @@ async function refreshBgServerCache() {
|
|
|
632
632
|
} catch {}
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
+
// === WS subscription для активного проекта: server-side polling ===
|
|
636
|
+
// Сервер сам опрашивает провайдера (см. lib/jobsHub.js _startPoller).
|
|
637
|
+
// На completion пушит 'done' event с url+cost. Renderer скачивает и
|
|
638
|
+
// записывает файл — это и есть "пробуждение" даже после Cmd+R.
|
|
639
|
+
let _projectJobsWS = null;
|
|
640
|
+
function _subscribeProjectJobsWS() {
|
|
641
|
+
const pk = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
|
|
642
|
+
: state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
|
|
643
|
+
if (!pk) return;
|
|
644
|
+
// Reuse тот же WS что использует welcome для jobs:all — но теперь
|
|
645
|
+
// подписка ещё и на jobs:<pk>.
|
|
646
|
+
if (!_projectJobsWS || _projectJobsWS.readyState !== WebSocket.OPEN) {
|
|
647
|
+
try {
|
|
648
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
649
|
+
_projectJobsWS = new WebSocket(`${proto}://${location.host}/ws`);
|
|
650
|
+
} catch { return; }
|
|
651
|
+
_projectJobsWS.onopen = () => {
|
|
652
|
+
try { _projectJobsWS.send(JSON.stringify({ type: 'subscribe', channel: 'jobs:' + pk })); } catch {}
|
|
653
|
+
};
|
|
654
|
+
_projectJobsWS.onmessage = (e) => {
|
|
655
|
+
let m; try { m = JSON.parse(e.data); } catch { return; }
|
|
656
|
+
if (m?.type === 'event') _handleProjectJobsEvent(m.event);
|
|
657
|
+
};
|
|
658
|
+
_projectJobsWS.onclose = () => { _projectJobsWS = null; setTimeout(_subscribeProjectJobsWS, 5000); };
|
|
659
|
+
_projectJobsWS.onerror = () => {};
|
|
660
|
+
} else {
|
|
661
|
+
try { _projectJobsWS.send(JSON.stringify({ type: 'subscribe', channel: 'jobs:' + pk })); } catch {}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Обработчик WS-event'ов от server-side jobsHub. На 'done' — скачиваем
|
|
666
|
+
// результат через /api/proxy и пишем файл (это работает только если
|
|
667
|
+
// FSAH-handle ещё валиден — т.е. project открыт).
|
|
668
|
+
async function _handleProjectJobsEvent(evt) {
|
|
669
|
+
if (!evt) return;
|
|
670
|
+
if (evt.kind === 'done') {
|
|
671
|
+
if (state.jobs.has(evt.jobId)) {
|
|
672
|
+
// Local pollJob уже видит этот же taskId и сам обработает completion.
|
|
673
|
+
// Server-side push — это backup. Не дублируем работу.
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
// Local job не активен — значит pollJob не запущен (после Cmd+R).
|
|
677
|
+
// Скачиваем + пишем + помечаем done. Сначала найдём ноду.
|
|
678
|
+
if (!state.currentBoard) { vlog('info', `WS done arrived but no currentBoard, jobId=${evt.jobId}`); return; }
|
|
679
|
+
const node = state.currentBoard.metadata.nodes.find(n => n.id === evt.jobId);
|
|
680
|
+
if (!node) { vlog('info', `WS done: nodeId not found in current board ${evt.jobId}`); return; }
|
|
681
|
+
try {
|
|
682
|
+
const r = await fetch('/api/proxy?url=' + encodeURIComponent(evt.url));
|
|
683
|
+
if (!r.ok) throw new Error(`download HTTP ${r.status}`);
|
|
684
|
+
const blob = await r.blob();
|
|
685
|
+
const ext = evt.kindOf === 'image' ? 'jpg' : 'mp4';
|
|
686
|
+
const sub = evt.kindOf === 'image' ? 'frames' : 'clips';
|
|
687
|
+
const dir = await getOrCreateBoardSubdir(state.currentBoard.handle, sub);
|
|
688
|
+
const baseName = await uniqueName(dir, `gen_${Date.now()}.${ext}`);
|
|
689
|
+
await writeFile(dir, baseName, blob);
|
|
690
|
+
const relPath = `${sub}/${baseName}`;
|
|
691
|
+
await mutateNode(state.currentBoard.key, state.currentBoard.handle, evt.jobId, n => {
|
|
692
|
+
n.status = undefined; n.error = undefined; n.file = relPath;
|
|
693
|
+
if (typeof evt.cost === 'number') {
|
|
694
|
+
n.generated = { ...(n.generated || {}), creditsCharged: evt.cost };
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
if (typeof showToast === 'function') {
|
|
698
|
+
showToast(`✓ ${evt.kindOf} «${node.name || node.id.slice(0,6)}» готов (server-side)`, 'ok');
|
|
699
|
+
}
|
|
700
|
+
if (typeof systemNotify === 'function' && document.hidden) {
|
|
701
|
+
systemNotify('KingKont', `${evt.kindOf} «${node.name || ''}» готов`).catch(() => {});
|
|
702
|
+
}
|
|
703
|
+
// Завершаем job на сервере (free up the slot).
|
|
704
|
+
const pk = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
|
|
705
|
+
: state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
|
|
706
|
+
if (pk) bgJobEnd(evt.jobId, pk);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
console.warn('[ws-done] download/write failed:', e?.message);
|
|
709
|
+
}
|
|
710
|
+
} else if (evt.kind === 'failed') {
|
|
711
|
+
if (typeof showToast === 'function') {
|
|
712
|
+
showToast(`⚠ Генерация провалилась: ${evt.error?.slice?.(0,80) || 'unknown'}`, 'error');
|
|
713
|
+
}
|
|
714
|
+
const pk = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
|
|
715
|
+
: state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
|
|
716
|
+
if (pk) bgJobEnd(evt.jobId, pk);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
635
720
|
// Live-update welcome когда bg-jobs меняются (start/end). Перерисовываем
|
|
636
721
|
// только если открыт welcome (нет проекта), иначе UI не виден.
|
|
637
722
|
window.addEventListener('bgjobs:changed', () => {
|
|
@@ -1142,6 +1227,10 @@ async function openFilm(handle) {
|
|
|
1142
1227
|
|
|
1143
1228
|
// Сканируем все доски на незавершённые задачи генерации (после перезагрузки)
|
|
1144
1229
|
scanAllBoardsForPendingJobs(handle).catch(e => console.warn('scan failed', e));
|
|
1230
|
+
// Subscribe на server-side WS events для этого проекта — сервер сам
|
|
1231
|
+
// полит провайдера и пушит 'done'/'failed' даже если у нас нет local
|
|
1232
|
+
// pollJob (например после Cmd+R). См. _handleProjectJobsEvent.
|
|
1233
|
+
_subscribeProjectJobsWS();
|
|
1145
1234
|
// Подгружаем настройки всех локаций
|
|
1146
1235
|
loadAllLocationsInfo().catch(() => {});
|
|
1147
1236
|
// Подгружаем настройки всех персонажей (для add-menu и т.д.)
|
package/renderer/chat.js
CHANGED
|
@@ -1213,6 +1213,23 @@
|
|
|
1213
1213
|
let m;
|
|
1214
1214
|
try { m = JSON.parse(e.data); } catch { return; }
|
|
1215
1215
|
if (m?.type === 'event' && m.channel === 'chat:' + key) {
|
|
1216
|
+
// Финальный ответ — показать toast + system notification
|
|
1217
|
+
// (даже если чат-панель скрыта).
|
|
1218
|
+
if (m.event?.kind === 'final') {
|
|
1219
|
+
if (m.event.error) {
|
|
1220
|
+
if (typeof showToast === 'function') showToast(`💬 KingKont: ⚠ ${m.event.error.slice(0, 80)}`, 'error');
|
|
1221
|
+
if (typeof systemNotify === 'function' && document.hidden) {
|
|
1222
|
+
systemNotify('KingKont chat', '⚠ ' + m.event.error.slice(0, 100), { tag: 'chat-final' }).catch(() => {});
|
|
1223
|
+
}
|
|
1224
|
+
} else if (m.event.text) {
|
|
1225
|
+
const preview = m.event.text.length > 100 ? m.event.text.slice(0, 100) + '…' : m.event.text;
|
|
1226
|
+
if (typeof showToast === 'function') showToast(`💬 KingKont: ${preview}`, 'ok');
|
|
1227
|
+
if (typeof systemNotify === 'function' && document.hidden) {
|
|
1228
|
+
systemNotify('KingKont chat', preview, { tag: 'chat-final' }).catch(() => {});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Любой event — refresh state (иначе пропустим обновление history).
|
|
1216
1233
|
_refreshAndExecute(key).catch(() => {});
|
|
1217
1234
|
}
|
|
1218
1235
|
};
|
package/renderer/generate.js
CHANGED
|
@@ -1851,6 +1851,10 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
|
|
|
1851
1851
|
}
|
|
1852
1852
|
job.taskId = data.taskId;
|
|
1853
1853
|
logJob(node.id, `taskId=${data.taskId}`);
|
|
1854
|
+
// Сообщаем server-side poller'у taskId — теперь сервер тоже опрашивает
|
|
1855
|
+
// провайдера. Если renderer закроется/Cmd+R — server продолжит и
|
|
1856
|
+
// пушнёт WS-event на completion.
|
|
1857
|
+
if (typeof bgJobUpdateTaskId === 'function') bgJobUpdateTaskId(node.id, data.taskId, job.projectKey);
|
|
1854
1858
|
await mutateNode(bKey, boardHandle, node.id, n => {
|
|
1855
1859
|
n.generated = { ...(n.generated || {}), taskId: data.taskId, state: 'queued' };
|
|
1856
1860
|
});
|
package/renderer/state.js
CHANGED
|
@@ -82,10 +82,9 @@ function _projectKeyForCurrent() {
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
function bgJobStart(info) {
|
|
85
|
-
// info: {nodeId, kind, name?, projectKey?}
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// null, и end-call станет no-op).
|
|
85
|
+
// info: {nodeId, kind, name?, projectKey?, taskId?}
|
|
86
|
+
// taskId важен — server-side poller использует его чтобы опрашивать
|
|
87
|
+
// провайдера сам (Chatium/KIE) даже когда renderer закрыт.
|
|
89
88
|
const pk = info.projectKey || _projectKeyForCurrent();
|
|
90
89
|
if (!pk) { console.warn('bgJobStart: no projectKey, skipping'); return; }
|
|
91
90
|
try {
|
|
@@ -98,9 +97,26 @@ function bgJobStart(info) {
|
|
|
98
97
|
} catch {}
|
|
99
98
|
fetch('/api/jobs/track', {
|
|
100
99
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
101
|
-
body: JSON.stringify({
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
action: 'start',
|
|
102
|
+
projectKey: pk,
|
|
103
|
+
jobId: info.nodeId,
|
|
104
|
+
kind: info.kind,
|
|
105
|
+
name: info.name,
|
|
106
|
+
taskId: info.taskId || null, // ← server-side poller цепляется по taskId
|
|
107
|
+
}),
|
|
102
108
|
}).catch(() => {});
|
|
103
109
|
}
|
|
110
|
+
// Update taskId for an existing job (server didn't have it on initial track).
|
|
111
|
+
function bgJobUpdateTaskId(nodeId, taskId, projectKey) {
|
|
112
|
+
const pk = projectKey || _projectKeyForCurrent();
|
|
113
|
+
if (!pk || !taskId) return;
|
|
114
|
+
fetch('/api/jobs/track', {
|
|
115
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId }),
|
|
117
|
+
}).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
window.bgJobUpdateTaskId = bgJobUpdateTaskId;
|
|
104
120
|
function bgJobEnd(nodeId, projectKey) {
|
|
105
121
|
// projectKey ОБЯЗАТЕЛЕН — без него юзер на welcome теряет уведомление
|
|
106
122
|
// и счётчик зависает. Раньше фоллбэк на _projectKeyForCurrent давал
|
package/server.js
CHANGED
|
@@ -355,9 +355,9 @@ async function handleChatClear(req, res) {
|
|
|
355
355
|
async function handleJobsTrack(req, res) {
|
|
356
356
|
try {
|
|
357
357
|
const body = await readJson(req);
|
|
358
|
-
const { action, projectKey, jobId, kind, name, type } = body || {};
|
|
358
|
+
const { action, projectKey, jobId, kind, name, type, taskId } = body || {};
|
|
359
359
|
if (!projectKey || !jobId) return send(res, 400, { error: 'projectKey + jobId обязательны' });
|
|
360
|
-
if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type });
|
|
360
|
+
if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type, taskId });
|
|
361
361
|
else if (action === 'end') jobsHub.end({ projectKey, jobId });
|
|
362
362
|
else return send(res, 400, { error: 'action: start|end' });
|
|
363
363
|
send(res, 200, { ok: true });
|
|
@@ -455,6 +455,9 @@ function start(port = PORT, opts = {}) {
|
|
|
455
455
|
// Init chatSession с userDataDir для персистентности историй.
|
|
456
456
|
// Без opts.userDataDir чат живёт только in-memory (CLI-режим).
|
|
457
457
|
chatSession.init({ userDataDir: opts.userDataDir || null });
|
|
458
|
+
// jobsHub нужен settingsGetter чтобы поллить провайдеров (Chatium token,
|
|
459
|
+
// KIE_API_KEY и т.п.).
|
|
460
|
+
jobsHub.setSettingsGetter(getSettings);
|
|
458
461
|
return new Promise((resolveOk, reject) => {
|
|
459
462
|
server.once('error', reject);
|
|
460
463
|
server.listen(port, () => {
|