kingkont 0.17.3 → 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/lib/jobsHub.js +17 -16
- package/package.json +1 -1
- package/renderer/generate.js +1 -1
- package/renderer/notifyPanel.js +97 -13
- 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/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,28 @@
|
|
|
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
|
+
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>` : '';
|
|
91
95
|
row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start;">
|
|
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
|
+
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
|
+
}
|
|
96
105
|
list.appendChild(row);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
// Public API.
|
|
101
|
-
function addEvent({ kind, text, raw }) {
|
|
110
|
+
function addEvent({ kind, text, raw, target }) {
|
|
102
111
|
if (!text) return;
|
|
103
|
-
events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw });
|
|
112
|
+
events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw, target: target || null });
|
|
104
113
|
if (events.length > MAX_EVENTS) events.shift();
|
|
105
114
|
if (!panelOpen) {
|
|
106
115
|
unread++;
|
|
@@ -109,6 +118,71 @@
|
|
|
109
118
|
render();
|
|
110
119
|
}
|
|
111
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
|
+
}
|
|
112
186
|
window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
|
|
113
187
|
|
|
114
188
|
// Init: ensure UI exists ASAP, hook into showToast и WS-events.
|
|
@@ -138,13 +212,23 @@
|
|
|
138
212
|
const e = m.event || {};
|
|
139
213
|
const ch = m.channel || '';
|
|
140
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;
|
|
141
219
|
if (e.kind === 'done') {
|
|
142
|
-
|
|
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
|
+
});
|
|
143
226
|
} else if (e.kind === 'failed') {
|
|
144
|
-
addEvent({
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
227
|
+
addEvent({
|
|
228
|
+
kind: 'error',
|
|
229
|
+
text: `⚠ ${e.kindOf || 'gen'} ${e.nodeName ? '«'+e.nodeName+'»' : ''} провалился: ${(e.error || '').slice(0, 100)}`,
|
|
230
|
+
target,
|
|
231
|
+
});
|
|
148
232
|
}
|
|
149
233
|
}
|
|
150
234
|
};
|
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(() => {});
|