kingkont 0.17.3 → 0.17.5
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/jobsHub.js +17 -16
- package/package.json +1 -1
- package/renderer/board.js +15 -0
- package/renderer/generate.js +1 -1
- package/renderer/notifyPanel.js +116 -14
- package/renderer/state.js +4 -2
package/lib/jobsHub.js
CHANGED
|
@@ -32,7 +32,7 @@ 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); }
|
|
@@ -46,6 +46,7 @@ function start({ projectKey, jobId, kind, name, type, taskId }) {
|
|
|
46
46
|
name: name ?? existing.name ?? null,
|
|
47
47
|
type: type ?? existing.type ?? null,
|
|
48
48
|
taskId: taskId ?? existing.taskId ?? null,
|
|
49
|
+
boardKey: boardKey ?? existing.boardKey ?? null,
|
|
49
50
|
startedAt: existing.startedAt || Date.now(),
|
|
50
51
|
};
|
|
51
52
|
m.set(jobId, merged);
|
|
@@ -93,16 +94,15 @@ function _startPoller({ projectKey, jobId, taskId, kind }) {
|
|
|
93
94
|
if (r.status === 'done') {
|
|
94
95
|
console.log('[jobsHub] DONE', jobId, '→', r.url);
|
|
95
96
|
_stopPoller(jobId);
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
wsHub.publish('jobs:
|
|
104
|
-
|
|
105
|
-
});
|
|
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 });
|
|
106
106
|
// Job-end ОТЛОЖЕН — ждём ack от клиента (он скачает файл).
|
|
107
107
|
// Но если за 60s никто не пришёл — auto-end (чтобы счётчики не зависали).
|
|
108
108
|
setTimeout(() => {
|
|
@@ -110,12 +110,13 @@ function _startPoller({ projectKey, jobId, taskId, kind }) {
|
|
|
110
110
|
}, 60000);
|
|
111
111
|
} else if (r.status === 'error') {
|
|
112
112
|
_stopPoller(jobId);
|
|
113
|
-
|
|
113
|
+
const info = jobsByProject.get(projectKey)?.get(jobId) || {};
|
|
114
|
+
const evt = {
|
|
114
115
|
kind: 'failed', jobId, taskId, error: r.error, kindOf: kind,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
116
|
+
boardKey: info.boardKey || null, nodeName: info.name || null,
|
|
117
|
+
};
|
|
118
|
+
wsHub.publish('jobs:' + projectKey, evt);
|
|
119
|
+
wsHub.publish('jobs:all', { ...evt, projectKey });
|
|
119
120
|
// На ошибке тоже auto-end через 60s.
|
|
120
121
|
setTimeout(() => {
|
|
121
122
|
if (!pollers.has(jobId)) end({ projectKey, jobId });
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -454,7 +454,22 @@ function fmtRelativeTime(ts) {
|
|
|
454
454
|
return new Date(ts).toLocaleDateString();
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// Дедуп: на Cmd+R / WS-event burst несколько вызовов могли race'иться
|
|
458
|
+
// (clear grid → await getRecents → append → второй call в это же время →
|
|
459
|
+
// дубли карточек / наезжают). Если уже рендерим — отметим что нужен
|
|
460
|
+
// follow-up render и выйдем; запустим его после текущего.
|
|
461
|
+
let _wrInFlight = false;
|
|
462
|
+
let _wrPending = false;
|
|
457
463
|
async function renderWelcomeRecents() {
|
|
464
|
+
if (_wrInFlight) { _wrPending = true; return; }
|
|
465
|
+
_wrInFlight = true;
|
|
466
|
+
try { await _renderWelcomeRecentsInner(); }
|
|
467
|
+
finally {
|
|
468
|
+
_wrInFlight = false;
|
|
469
|
+
if (_wrPending) { _wrPending = false; renderWelcomeRecents().catch(() => {}); }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async function _renderWelcomeRecentsInner() {
|
|
458
473
|
const grid = $('welcomeRecentGrid');
|
|
459
474
|
const wrap = $('welcomeRecent');
|
|
460
475
|
const titleEl = $('welcomeRecentTitle');
|
package/renderer/generate.js
CHANGED
|
@@ -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 => ({
|
package/renderer/notifyPanel.js
CHANGED
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
|
|
22
22
|
function _ensureUI() {
|
|
23
23
|
if ($('notifyBtn')) return;
|
|
24
|
-
// Кнопка-колокольчик в
|
|
24
|
+
// Кнопка-колокольчик в нижнем-левом углу — там есть свободное место,
|
|
25
|
+
// не перекрывает sidebar/canvas-toolbar/welcome-status.
|
|
25
26
|
const btn = document.createElement('button');
|
|
26
27
|
btn.id = 'notifyBtn';
|
|
27
28
|
btn.title = 'События (генерации, чат, ошибки)';
|
|
28
|
-
btn.style.cssText = 'position:fixed;
|
|
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;';
|
|
29
30
|
btn.innerHTML = '🔔';
|
|
30
31
|
document.body.appendChild(btn);
|
|
31
32
|
btn.addEventListener('click', toggle);
|
|
@@ -35,10 +36,10 @@
|
|
|
35
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;';
|
|
36
37
|
btn.appendChild(badge);
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
+
// Панель — bottom-left как и кнопка.
|
|
39
40
|
const panel = document.createElement('div');
|
|
40
41
|
panel.id = 'notifyPanel';
|
|
41
|
-
panel.style.cssText = 'position:fixed;
|
|
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;';
|
|
42
43
|
panel.innerHTML = `
|
|
43
44
|
<div style="padding:10px 12px; border-bottom:1px solid #2a2a2a; display:flex; align-items:center; gap:8px;">
|
|
44
45
|
<strong style="font-size:13px; color:#ddd;">События</strong>
|
|
@@ -87,20 +88,46 @@
|
|
|
87
88
|
const c = colors[e.kind] || colors.info;
|
|
88
89
|
const dt = new Date(e.ts);
|
|
89
90
|
const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
|
|
90
|
-
|
|
91
|
-
row.
|
|
91
|
+
const clickable = !!e.target;
|
|
92
|
+
row.style.cssText = `position:relative; padding:8px 10px 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; padding-right:18px;">
|
|
92
96
|
<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>
|
|
97
|
+
<span style="color:#777; font-size:10px; font-family:ui-monospace,monospace; flex-shrink:0;">${time}${targetHint}</span>
|
|
94
98
|
</div>`;
|
|
95
99
|
row.querySelector('div > div').textContent = e.text;
|
|
100
|
+
// Кнопка × для удаления конкретного event'a (не блокирует клик по row).
|
|
101
|
+
const delBtn = document.createElement('button');
|
|
102
|
+
delBtn.textContent = '×';
|
|
103
|
+
delBtn.title = 'Удалить';
|
|
104
|
+
delBtn.style.cssText = 'position:absolute; top:2px; right:4px; background:transparent; border:none; color:#888; cursor:pointer; font-size:14px; line-height:1; padding:2px 6px; opacity:0.5; transition:opacity 0.1s;';
|
|
105
|
+
delBtn.addEventListener('mouseenter', () => delBtn.style.opacity = '1');
|
|
106
|
+
delBtn.addEventListener('mouseleave', () => delBtn.style.opacity = '0.5');
|
|
107
|
+
delBtn.addEventListener('click', (ev) => {
|
|
108
|
+
ev.stopPropagation();
|
|
109
|
+
// Find by ts (events shifted by render, индекс i может измениться к click'у).
|
|
110
|
+
const idx = events.findIndex(x => x.ts === e.ts && x.text === e.text);
|
|
111
|
+
if (idx >= 0) events.splice(idx, 1);
|
|
112
|
+
render();
|
|
113
|
+
});
|
|
114
|
+
row.appendChild(delBtn);
|
|
115
|
+
if (clickable) {
|
|
116
|
+
row.addEventListener('mouseenter', () => row.style.background = `${c}44`);
|
|
117
|
+
row.addEventListener('mouseleave', () => row.style.background = `${c}22`);
|
|
118
|
+
row.addEventListener('click', (ev) => {
|
|
119
|
+
if (ev.target === delBtn) return;
|
|
120
|
+
navigateToTarget(e.target);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
96
123
|
list.appendChild(row);
|
|
97
124
|
}
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
// Public API.
|
|
101
|
-
function addEvent({ kind, text, raw }) {
|
|
128
|
+
function addEvent({ kind, text, raw, target }) {
|
|
102
129
|
if (!text) return;
|
|
103
|
-
events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw });
|
|
130
|
+
events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw, target: target || null });
|
|
104
131
|
if (events.length > MAX_EVENTS) events.shift();
|
|
105
132
|
if (!panelOpen) {
|
|
106
133
|
unread++;
|
|
@@ -109,6 +136,71 @@
|
|
|
109
136
|
render();
|
|
110
137
|
}
|
|
111
138
|
}
|
|
139
|
+
|
|
140
|
+
// Navigate from notification to scene/node.
|
|
141
|
+
// target: {projectKey, boardKey, nodeId?}
|
|
142
|
+
async function navigateToTarget(target) {
|
|
143
|
+
if (!target?.projectKey) return;
|
|
144
|
+
setOpen(false);
|
|
145
|
+
const [pkType, pkId] = String(target.projectKey).split(':', 2);
|
|
146
|
+
const currentKey = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
|
|
147
|
+
: state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
|
|
148
|
+
// Если уже в нужном проекте — просто переключаем доску.
|
|
149
|
+
if (currentKey === target.projectKey) {
|
|
150
|
+
if (target.boardKey) await _selectBoardByKey(target.boardKey);
|
|
151
|
+
if (target.nodeId) _highlightNode(target.nodeId);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Иначе открываем проект.
|
|
155
|
+
if (pkType === 'cloud') {
|
|
156
|
+
if (window.cloudProjects?.open) await window.cloudProjects.open(pkId);
|
|
157
|
+
else { alert('Облачный проект недоступен'); return; }
|
|
158
|
+
} else if (pkType === 'folder') {
|
|
159
|
+
// Folder-проект: ищем в recents.
|
|
160
|
+
try {
|
|
161
|
+
const recents = await getRecents();
|
|
162
|
+
const r = recents.find(x => x.name === pkId);
|
|
163
|
+
if (r?.handle) {
|
|
164
|
+
let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
|
|
165
|
+
if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
|
|
166
|
+
if (g) await openFilm(r.handle);
|
|
167
|
+
} else {
|
|
168
|
+
alert(`Проект «${pkId}» не найден в недавних. Открой его вручную.`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
} catch (e) { console.warn('navigate to folder failed:', e); return; }
|
|
172
|
+
}
|
|
173
|
+
// После openFilm/cloudProjects.open — выбираем доску.
|
|
174
|
+
if (target.boardKey) {
|
|
175
|
+
// openFilm вызывает selectBoard через lastBoard, может перебить. Wait чуть.
|
|
176
|
+
setTimeout(() => _selectBoardByKey(target.boardKey).then(() => {
|
|
177
|
+
if (target.nodeId) _highlightNode(target.nodeId);
|
|
178
|
+
}), 600);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function _selectBoardByKey(boardKey) {
|
|
182
|
+
if (!state.filmHandle) return;
|
|
183
|
+
// boardKey формат: 'episode/Name' / 'character/Name' / 'location/Name'.
|
|
184
|
+
const [kind, ...nameParts] = String(boardKey).split('/');
|
|
185
|
+
const name = nameParts.join('/');
|
|
186
|
+
if (!kind || !name) return;
|
|
187
|
+
try {
|
|
188
|
+
const list = kind === 'character' ? await listCharacters(state.filmHandle)
|
|
189
|
+
: kind === 'location' ? await listLocations(state.filmHandle)
|
|
190
|
+
: await listEpisodes(state.filmHandle);
|
|
191
|
+
const found = list.find(b => b.name === name);
|
|
192
|
+
if (found) await selectBoard({ kind, ...found });
|
|
193
|
+
} catch (e) { console.warn('select board failed:', e); }
|
|
194
|
+
}
|
|
195
|
+
function _highlightNode(nodeId) {
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
const el = document.querySelector(`.node[data-id="${nodeId}"]`);
|
|
198
|
+
if (!el) return;
|
|
199
|
+
el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
|
|
200
|
+
el.style.boxShadow = '0 0 0 3px #e33377';
|
|
201
|
+
setTimeout(() => { el.style.boxShadow = ''; }, 2500);
|
|
202
|
+
}, 200);
|
|
203
|
+
}
|
|
112
204
|
window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
|
|
113
205
|
|
|
114
206
|
// Init: ensure UI exists ASAP, hook into showToast и WS-events.
|
|
@@ -138,13 +230,23 @@
|
|
|
138
230
|
const e = m.event || {};
|
|
139
231
|
const ch = m.channel || '';
|
|
140
232
|
if (ch === 'jobs:all') {
|
|
233
|
+
// target позволяет click'нуть и перейти к ноде на доске.
|
|
234
|
+
const target = (e.projectKey && e.boardKey) ? {
|
|
235
|
+
projectKey: e.projectKey, boardKey: e.boardKey, nodeId: e.jobId,
|
|
236
|
+
} : null;
|
|
141
237
|
if (e.kind === 'done') {
|
|
142
|
-
|
|
238
|
+
const nameLabel = e.nodeName ? `«${e.nodeName}»` : `(jobId=${(e.jobId||'').slice(0,8)})`;
|
|
239
|
+
addEvent({
|
|
240
|
+
kind: 'ok',
|
|
241
|
+
text: `✓ ${e.kindOf || 'gen'} ${nameLabel} готов в ${e.projectKey || ''}`,
|
|
242
|
+
target,
|
|
243
|
+
});
|
|
143
244
|
} else if (e.kind === 'failed') {
|
|
144
|
-
addEvent({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
245
|
+
addEvent({
|
|
246
|
+
kind: 'error',
|
|
247
|
+
text: `⚠ ${e.kindOf || 'gen'} ${e.nodeName ? '«'+e.nodeName+'»' : ''} провалился: ${(e.error || '').slice(0, 100)}`,
|
|
248
|
+
target,
|
|
249
|
+
});
|
|
148
250
|
}
|
|
149
251
|
}
|
|
150
252
|
};
|
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,6 +104,7 @@ 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(() => {});
|