kingkont 0.17.2 → 0.17.4

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
@@ -32,20 +32,30 @@ function _emit(projectKey, extra) {
32
32
  wsHub.publish('jobs:all', { ...evt, projectKey });
33
33
  }
34
34
 
35
- function start({ projectKey, jobId, kind, name, type, taskId }) {
35
+ function start({ projectKey, jobId, kind, name, type, taskId, boardKey }) {
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
+ boardKey: boardKey ?? existing.boardKey ?? null,
50
+ startedAt: existing.startedAt || Date.now(),
51
+ };
52
+ m.set(jobId, merged);
53
+ console.log('[jobsHub] start/update', { projectKey, jobId, ...merged });
43
54
  _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 });
55
+ // Если есть И taskId, И kind — стартуем серверный poller. Renderer тоже
56
+ // полит параллельно. Если renderer закрыт — серверный продолжает.
57
+ if (merged.taskId && (merged.kind === 'image' || merged.kind === 'video')) {
58
+ _startPoller({ projectKey, jobId, taskId: merged.taskId, kind: merged.kind });
49
59
  }
50
60
  }
51
61
 
@@ -62,30 +72,37 @@ function end({ projectKey, jobId }) {
62
72
  }
63
73
 
64
74
  function _startPoller({ projectKey, jobId, taskId, kind }) {
65
- if (pollers.has(jobId)) return;
75
+ if (pollers.has(jobId)) {
76
+ console.log('[jobsHub] poller already running for', jobId);
77
+ return;
78
+ }
79
+ console.log('[jobsHub] _startPoller', { projectKey, jobId, taskId, kind });
66
80
  const providers = require('./providers');
67
81
  const tick = async () => {
68
82
  const entry = pollers.get(jobId);
69
83
  if (!entry) return;
70
84
  entry.lastPollAt = Date.now();
71
85
  let r;
72
- try { r = await providers.pollGeneration(taskId, _settingsGetter()); }
86
+ try {
87
+ r = await providers.pollGeneration(taskId, _settingsGetter());
88
+ console.log('[jobsHub] poll', jobId, '→', r.status, r.state || '');
89
+ }
73
90
  catch (e) {
74
91
  console.warn('[jobsHub] poll failed for', jobId, e?.message);
75
92
  return; // следующий tick попробует снова
76
93
  }
77
94
  if (r.status === 'done') {
95
+ console.log('[jobsHub] DONE', jobId, '→', r.url);
78
96
  _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
- });
97
+ // Извлекаем boardKey + name из jobsByProject для navigation.
98
+ const info = jobsByProject.get(projectKey)?.get(jobId) || {};
99
+ const evt = {
100
+ kind: 'done', jobId, taskId, url: r.url, cost: r.cost ?? null,
101
+ kindOf: kind, provider: r.provider,
102
+ boardKey: info.boardKey || null, nodeName: info.name || null,
103
+ };
104
+ wsHub.publish('jobs:' + projectKey, evt);
105
+ wsHub.publish('jobs:all', { ...evt, projectKey });
89
106
  // Job-end ОТЛОЖЕН — ждём ack от клиента (он скачает файл).
90
107
  // Но если за 60s никто не пришёл — auto-end (чтобы счётчики не зависали).
91
108
  setTimeout(() => {
@@ -93,12 +110,13 @@ function _startPoller({ projectKey, jobId, taskId, kind }) {
93
110
  }, 60000);
94
111
  } else if (r.status === 'error') {
95
112
  _stopPoller(jobId);
96
- wsHub.publish('jobs:' + projectKey, {
113
+ const info = jobsByProject.get(projectKey)?.get(jobId) || {};
114
+ const evt = {
97
115
  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
- });
116
+ boardKey: info.boardKey || null, nodeName: info.name || null,
117
+ };
118
+ wsHub.publish('jobs:' + projectKey, evt);
119
+ wsHub.publish('jobs:all', { ...evt, projectKey });
102
120
  // На ошибке тоже auto-end через 60s.
103
121
  setTimeout(() => {
104
122
  if (!pollers.has(jobId)) end({ projectKey, jobId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.17.2",
3
+ "version": "0.17.4",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -1733,7 +1733,7 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
1733
1733
  state.jobs.set(node.id, job);
1734
1734
  updateJobsBadge();
1735
1735
  // Track в global bg-jobs (для welcome-индикаторов и system-notif при завершении).
1736
- if (typeof bgJobStart === 'function') bgJobStart({ nodeId: node.id, kind, name: node.name, projectKey });
1736
+ if (typeof bgJobStart === 'function') bgJobStart({ nodeId: node.id, kind, name: node.name, projectKey, boardKey: bKey });
1737
1737
  logJob(node.id, `gen start kind=${kind} model=${modelKey || '—'} refs=${mediaRefs?.length || 0} prompt="${(prompt||'').slice(0,200)}"`);
1738
1738
  // Подробный дамп всех refs со всеми полями
1739
1739
  logJob(node.id, `refs dump: ${logSafe((mediaRefs || []).map(r => ({
@@ -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,240 @@
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
+ // не перекрывает sidebar/canvas-toolbar/welcome-status.
26
+ const btn = document.createElement('button');
27
+ btn.id = 'notifyBtn';
28
+ btn.title = 'События (генерации, чат, ошибки)';
29
+ btn.style.cssText = 'position:fixed; bottom:12px; left:12px; 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;';
30
+ btn.innerHTML = '🔔';
31
+ document.body.appendChild(btn);
32
+ btn.addEventListener('click', toggle);
33
+ // Бейдж со счётчиком unread.
34
+ const badge = document.createElement('span');
35
+ badge.id = 'notifyBadge';
36
+ 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;';
37
+ btn.appendChild(badge);
38
+
39
+ // Панель — bottom-left как и кнопка.
40
+ const panel = document.createElement('div');
41
+ panel.id = 'notifyPanel';
42
+ panel.style.cssText = 'position:fixed; bottom:50px; left:12px; width:380px; max-height:60vh; 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;';
43
+ panel.innerHTML = `
44
+ <div style="padding:10px 12px; border-bottom:1px solid #2a2a2a; display:flex; align-items:center; gap:8px;">
45
+ <strong style="font-size:13px; color:#ddd;">События</strong>
46
+ <span style="flex:1;"></span>
47
+ <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>
48
+ <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>
49
+ </div>
50
+ <div id="notifyList" style="flex:1; overflow-y:auto; padding:6px;"></div>
51
+ `;
52
+ document.body.appendChild(panel);
53
+ $('notifyClose').addEventListener('click', () => setOpen(false));
54
+ $('notifyClear').addEventListener('click', () => { events.length = 0; unread = 0; render(); });
55
+ }
56
+
57
+ function setOpen(open) {
58
+ _ensureUI();
59
+ panelOpen = open;
60
+ $('notifyPanel').style.display = open ? 'flex' : 'none';
61
+ if (open) { unread = 0; updateBadge(); render(); }
62
+ }
63
+ function toggle() { setOpen(!panelOpen); }
64
+
65
+ function updateBadge() {
66
+ const b = $('notifyBadge');
67
+ if (!b) return;
68
+ if (unread > 0) {
69
+ b.textContent = unread > 99 ? '99+' : String(unread);
70
+ b.style.display = '';
71
+ } else b.style.display = 'none';
72
+ }
73
+
74
+ function render() {
75
+ const list = $('notifyList');
76
+ if (!list) return;
77
+ list.innerHTML = '';
78
+ if (!events.length) {
79
+ list.innerHTML = '<div style="padding:24px; text-align:center; color:#555; font-size:12px;">Нет событий</div>';
80
+ return;
81
+ }
82
+ for (let i = events.length - 1; i >= 0; i--) {
83
+ const e = events[i];
84
+ const row = document.createElement('div');
85
+ const colors = {
86
+ ok: '#3a6a3a', error: '#8a3a3a', info: '#3a5a8a', warn: '#8a7a3a', chat: '#7a4a8a',
87
+ };
88
+ const c = colors[e.kind] || colors.info;
89
+ const dt = new Date(e.ts);
90
+ const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
91
+ const clickable = !!e.target;
92
+ row.style.cssText = `padding:8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:${clickable ? 'pointer' : 'default'}; transition:background 0.1s;`;
93
+ if (clickable) row.title = 'Перейти к объекту';
94
+ const targetHint = clickable ? `<span style="color:#9ab; font-size:10px; margin-left:6px;">↗</span>` : '';
95
+ row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start;">
96
+ <div style="color:#e0e0e0; font-size:12px; line-height:1.4; word-break:break-word; flex:1;"></div>
97
+ <span style="color:#777; font-size:10px; font-family:ui-monospace,monospace; flex-shrink:0;">${time}${targetHint}</span>
98
+ </div>`;
99
+ row.querySelector('div > div').textContent = e.text;
100
+ if (clickable) {
101
+ row.addEventListener('mouseenter', () => row.style.background = `${c}44`);
102
+ row.addEventListener('mouseleave', () => row.style.background = `${c}22`);
103
+ row.addEventListener('click', () => navigateToTarget(e.target));
104
+ }
105
+ list.appendChild(row);
106
+ }
107
+ }
108
+
109
+ // Public API.
110
+ function addEvent({ kind, text, raw, target }) {
111
+ if (!text) return;
112
+ events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw, target: target || null });
113
+ if (events.length > MAX_EVENTS) events.shift();
114
+ if (!panelOpen) {
115
+ unread++;
116
+ updateBadge();
117
+ } else {
118
+ render();
119
+ }
120
+ }
121
+
122
+ // Navigate from notification to scene/node.
123
+ // target: {projectKey, boardKey, nodeId?}
124
+ async function navigateToTarget(target) {
125
+ if (!target?.projectKey) return;
126
+ setOpen(false);
127
+ const [pkType, pkId] = String(target.projectKey).split(':', 2);
128
+ const currentKey = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
129
+ : state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
130
+ // Если уже в нужном проекте — просто переключаем доску.
131
+ if (currentKey === target.projectKey) {
132
+ if (target.boardKey) await _selectBoardByKey(target.boardKey);
133
+ if (target.nodeId) _highlightNode(target.nodeId);
134
+ return;
135
+ }
136
+ // Иначе открываем проект.
137
+ if (pkType === 'cloud') {
138
+ if (window.cloudProjects?.open) await window.cloudProjects.open(pkId);
139
+ else { alert('Облачный проект недоступен'); return; }
140
+ } else if (pkType === 'folder') {
141
+ // Folder-проект: ищем в recents.
142
+ try {
143
+ const recents = await getRecents();
144
+ const r = recents.find(x => x.name === pkId);
145
+ if (r?.handle) {
146
+ let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
147
+ if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
148
+ if (g) await openFilm(r.handle);
149
+ } else {
150
+ alert(`Проект «${pkId}» не найден в недавних. Открой его вручную.`);
151
+ return;
152
+ }
153
+ } catch (e) { console.warn('navigate to folder failed:', e); return; }
154
+ }
155
+ // После openFilm/cloudProjects.open — выбираем доску.
156
+ if (target.boardKey) {
157
+ // openFilm вызывает selectBoard через lastBoard, может перебить. Wait чуть.
158
+ setTimeout(() => _selectBoardByKey(target.boardKey).then(() => {
159
+ if (target.nodeId) _highlightNode(target.nodeId);
160
+ }), 600);
161
+ }
162
+ }
163
+ async function _selectBoardByKey(boardKey) {
164
+ if (!state.filmHandle) return;
165
+ // boardKey формат: 'episode/Name' / 'character/Name' / 'location/Name'.
166
+ const [kind, ...nameParts] = String(boardKey).split('/');
167
+ const name = nameParts.join('/');
168
+ if (!kind || !name) return;
169
+ try {
170
+ const list = kind === 'character' ? await listCharacters(state.filmHandle)
171
+ : kind === 'location' ? await listLocations(state.filmHandle)
172
+ : await listEpisodes(state.filmHandle);
173
+ const found = list.find(b => b.name === name);
174
+ if (found) await selectBoard({ kind, ...found });
175
+ } catch (e) { console.warn('select board failed:', e); }
176
+ }
177
+ function _highlightNode(nodeId) {
178
+ setTimeout(() => {
179
+ const el = document.querySelector(`.node[data-id="${nodeId}"]`);
180
+ if (!el) return;
181
+ el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
182
+ el.style.boxShadow = '0 0 0 3px #e33377';
183
+ setTimeout(() => { el.style.boxShadow = ''; }, 2500);
184
+ }, 200);
185
+ }
186
+ window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
187
+
188
+ // Init: ensure UI exists ASAP, hook into showToast и WS-events.
189
+ document.addEventListener('DOMContentLoaded', () => {
190
+ _ensureUI();
191
+ // Wrap showToast чтобы дублировать в панель.
192
+ if (typeof window.showToast === 'function') {
193
+ const orig = window.showToast;
194
+ window.showToast = function (text, kind) {
195
+ try { addEvent({ kind: kind || 'info', text }); } catch {}
196
+ return orig.apply(this, arguments);
197
+ };
198
+ }
199
+ // WS-канал jobs:all для start/end/done/failed events. Connect к /ws.
200
+ function _connect() {
201
+ let ws;
202
+ try {
203
+ const proto = location.protocol === 'https:' ? 'wss' : 'ws';
204
+ ws = new WebSocket(`${proto}://${location.host}/ws`);
205
+ } catch { setTimeout(_connect, 5000); return; }
206
+ ws.onopen = () => {
207
+ try { ws.send(JSON.stringify({ type: 'subscribe', channel: 'jobs:all' })); } catch {}
208
+ };
209
+ ws.onmessage = (ev) => {
210
+ let m; try { m = JSON.parse(ev.data); } catch { return; }
211
+ if (m?.type !== 'event') return;
212
+ const e = m.event || {};
213
+ const ch = m.channel || '';
214
+ if (ch === 'jobs:all') {
215
+ // target позволяет click'нуть и перейти к ноде на доске.
216
+ const target = (e.projectKey && e.boardKey) ? {
217
+ projectKey: e.projectKey, boardKey: e.boardKey, nodeId: e.jobId,
218
+ } : null;
219
+ if (e.kind === 'done') {
220
+ const nameLabel = e.nodeName ? `«${e.nodeName}»` : `(jobId=${(e.jobId||'').slice(0,8)})`;
221
+ addEvent({
222
+ kind: 'ok',
223
+ text: `✓ ${e.kindOf || 'gen'} ${nameLabel} готов в ${e.projectKey || ''}`,
224
+ target,
225
+ });
226
+ } else if (e.kind === 'failed') {
227
+ addEvent({
228
+ kind: 'error',
229
+ text: `⚠ ${e.kindOf || 'gen'} ${e.nodeName ? '«'+e.nodeName+'»' : ''} провалился: ${(e.error || '').slice(0, 100)}`,
230
+ target,
231
+ });
232
+ }
233
+ }
234
+ };
235
+ ws.onclose = () => setTimeout(_connect, 5000);
236
+ ws.onerror = () => {};
237
+ }
238
+ _connect();
239
+ });
240
+ })();
package/renderer/state.js CHANGED
@@ -82,15 +82,16 @@ function _projectKeyForCurrent() {
82
82
  return null;
83
83
  }
84
84
  function bgJobStart(info) {
85
- // info: {nodeId, kind, name?, projectKey?, taskId?}
85
+ // info: {nodeId, kind, name?, projectKey?, taskId?, boardKey?}
86
86
  // taskId важен — server-side poller использует его чтобы опрашивать
87
87
  // провайдера сам (Chatium/KIE) даже когда renderer закрыт.
88
+ // boardKey — нужен для navigation из 🔔 уведомления (jump to scene).
88
89
  const pk = info.projectKey || _projectKeyForCurrent();
89
90
  if (!pk) { console.warn('bgJobStart: no projectKey, skipping'); return; }
90
91
  try {
91
92
  const list = JSON.parse(localStorage.getItem(bgJobsKey(pk)) || '[]');
92
93
  if (!list.some(j => j.nodeId === info.nodeId)) {
93
- list.push({ nodeId: info.nodeId, kind: info.kind, name: info.name || null, startedAt: Date.now() });
94
+ list.push({ nodeId: info.nodeId, kind: info.kind, name: info.name || null, boardKey: info.boardKey || null, startedAt: Date.now() });
94
95
  localStorage.setItem(bgJobsKey(pk), JSON.stringify(list));
95
96
  window.dispatchEvent(new CustomEvent('bgjobs:changed'));
96
97
  }
@@ -103,17 +104,19 @@ function bgJobStart(info) {
103
104
  jobId: info.nodeId,
104
105
  kind: info.kind,
105
106
  name: info.name,
107
+ boardKey: info.boardKey || null,
106
108
  taskId: info.taskId || null, // ← server-side poller цепляется по taskId
107
109
  }),
108
110
  }).catch(() => {});
109
111
  }
110
112
  // Update taskId for an existing job (server didn't have it on initial track).
111
- function bgJobUpdateTaskId(nodeId, taskId, projectKey) {
113
+ // kind важен без него server-side merge оставит null и poller не стартует.
114
+ function bgJobUpdateTaskId(nodeId, taskId, projectKey, kind) {
112
115
  const pk = projectKey || _projectKeyForCurrent();
113
116
  if (!pk || !taskId) return;
114
117
  fetch('/api/jobs/track', {
115
118
  method: 'POST', headers: { 'Content-Type': 'application/json' },
116
- body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId }),
119
+ body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId, kind }),
117
120
  }).catch(() => {});
118
121
  }
119
122
  window.bgJobUpdateTaskId = bgJobUpdateTaskId;