kingkont 0.19.1 → 0.19.2

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/eventStore.js CHANGED
@@ -41,6 +41,8 @@ function init({ userDataDir }) {
41
41
  if (_userDataDir) {
42
42
  const dir = path.join(_userDataDir, 'events');
43
43
  try { fs.mkdirSync(dir, { recursive: true }); } catch {}
44
+ // Lazy migration на первом list/listAll — не блокируем init.
45
+ setImmediate(_migrateGlobalEvents);
44
46
  }
45
47
  }
46
48
 
@@ -68,6 +70,65 @@ function _ensureLoaded(projectKey) {
68
70
  return list;
69
71
  }
70
72
 
73
+ // Одноразовая миграция: legacy events в `_global` без target.projectKey,
74
+ // но с target.nodeId — пытаемся найти их nodeId в других bucket'ах и
75
+ // переместить event в правильный bucket. Без неё клик по таким events
76
+ // делает navigateToTarget early-return (`if (!target.projectKey) return`).
77
+ let _migrationDone = false;
78
+ function _migrateGlobalEvents() {
79
+ if (_migrationDone || !_userDataDir) return;
80
+ _migrationDone = true;
81
+ try {
82
+ // Загрузим все bucket'ы с диска для поиска nodeId.
83
+ const dir = path.join(_userDataDir, 'events');
84
+ let names;
85
+ try { names = fs.readdirSync(dir); } catch { return; }
86
+ const allNodeMap = new Map(); // nodeId → projectKey
87
+ for (const n of names) {
88
+ if (!n.endsWith('.json')) continue;
89
+ const pk = decodeURIComponent(n.replace(/\.json$/, ''));
90
+ if (pk === '_global') continue;
91
+ const evts = _ensureLoaded(pk);
92
+ for (const e of evts) {
93
+ const nid = e.target?.nodeId;
94
+ if (nid && !allNodeMap.has(nid)) allNodeMap.set(nid, pk);
95
+ }
96
+ }
97
+ const global = _ensureLoaded('_global');
98
+ if (!global.length) return;
99
+ const stays = [];
100
+ const moveBuckets = new Map(); // pk → events[]
101
+ for (const e of global) {
102
+ const nid = e.target?.nodeId;
103
+ const pk = nid ? allNodeMap.get(nid) : null;
104
+ if (pk) {
105
+ if (!moveBuckets.has(pk)) moveBuckets.set(pk, []);
106
+ moveBuckets.get(pk).push({ ...e, target: { ...e.target, projectKey: pk } });
107
+ } else {
108
+ stays.push(e);
109
+ }
110
+ }
111
+ if (moveBuckets.size === 0) return;
112
+ // Применяем перемещение.
113
+ for (const [pk, evts] of moveBuckets.entries()) {
114
+ const target = _ensureLoaded(pk);
115
+ // Дедуп по (nodeId, ts) — может уже был добавлен.
116
+ for (const e of evts) {
117
+ if (!target.some(t => t.target?.nodeId === e.target.nodeId && t.ts === e.ts)) {
118
+ target.push(e);
119
+ }
120
+ }
121
+ target.sort((a, b) => a.ts - b.ts);
122
+ _schedulePersist(pk);
123
+ }
124
+ _cache.set('_global', stays);
125
+ _schedulePersist('_global');
126
+ console.log(`[eventStore] migrated ${global.length - stays.length} events из _global в правильные bucket'ы`);
127
+ } catch (e) {
128
+ console.warn('[eventStore] migration failed:', e?.message);
129
+ }
130
+ }
131
+
71
132
  function _schedulePersist(projectKey) {
72
133
  if (!_userDataDir) return;
73
134
  if (_persistTimers.has(projectKey)) clearTimeout(_persistTimers.get(projectKey));
@@ -89,22 +150,26 @@ function append(projectKey, event) {
89
150
  const pk = projectKey || '_global';
90
151
  const list = _ensureLoaded(pk);
91
152
  const ts = event.ts || Date.now();
92
- // Дедуп (mirror'ит клиентскую логику в notifyPanel.js):
93
- // - completion-event с nodeId ключ `node:<nodeId>` (gen завершён)
94
- // - chat-final event ключ `chat:<first 60 chars text>` (chat-final
95
- // приходит И от server-side chatSession, И от client showToast)
96
- // - всё остальное ключ `text:<first 80 chars>` (короткое окно)
97
- // Окно дедупа 30 секунд.
153
+ // Дедуп:
154
+ // - node:<id> completion (ok/error/info/warn)окно 24h (gen
155
+ // каждой ноды завершается ОДИН раз; повторное событие точно
156
+ // дубль, разнос времени бывает 20+ мин если client поллит после
157
+ // долгой паузы и повторно генерит showToast).
158
+ // - chat-finalокно 24h тоже.
159
+ // - всё остальное — 30s.
98
160
  let dk = null;
161
+ let windowMs = 30 * 1000;
99
162
  if (event.target?.nodeId && (event.kind === 'ok' || event.kind === 'error' || event.kind === 'info' || event.kind === 'warn')) {
100
163
  dk = `node:${event.target.nodeId}`;
164
+ windowMs = 24 * 3600 * 1000;
101
165
  } else if (event.target?.chat) {
102
166
  dk = `chat:${String(event.text).slice(0, 60)}`;
167
+ windowMs = 24 * 3600 * 1000;
103
168
  } else {
104
169
  dk = `text:${String(event.text).slice(0, 80)}`;
105
170
  }
106
171
  if (dk) {
107
- const cutoff = ts - 30 * 1000;
172
+ const cutoff = ts - windowMs;
108
173
  for (let i = list.length - 1; i >= 0; i--) {
109
174
  if (list[i].ts < cutoff) break;
110
175
  if (list[i]._dk === dk) return; // already have it
@@ -124,9 +189,23 @@ function append(projectKey, event) {
124
189
  _schedulePersist(pk);
125
190
  }
126
191
 
192
+ // Backward-compat helper: гарантируем что у каждого event'а в target есть
193
+ // projectKey (равный bucket-key где он лежит). Старые client-persisted
194
+ // события писались БЕЗ target.projectKey — `navigateToTarget` для них
195
+ // молча возвращал ('if (!target.projectKey) return') → клик не работал.
196
+ function _injectProjectKey(events, bucketKey) {
197
+ if (bucketKey === '_global') return events;
198
+ return events.map(e => {
199
+ if (e.target && !e.target.projectKey) {
200
+ return { ...e, target: { ...e.target, projectKey: bucketKey } };
201
+ }
202
+ return e;
203
+ });
204
+ }
205
+
127
206
  function list(projectKey, { limit = MAX_PER_PROJECT } = {}) {
128
207
  const pk = projectKey || '_global';
129
- const own = _ensureLoaded(pk).slice();
208
+ let own = _injectProjectKey(_ensureLoaded(pk).slice(), pk);
130
209
  // Backward-compat: события могли быть mis-filed в _global (когда юзер
131
210
  // на welcome генерил, _persistEvent.projectKey был null). Извлекаем
132
211
  // те у которых target.projectKey совпадает с запрошенным.
@@ -155,7 +234,14 @@ function listAll({ limit = 200 } = {}) {
155
234
  }
156
235
  const merged = [];
157
236
  for (const [pk, evts] of _cache.entries()) {
158
- for (const e of evts) merged.push({ ...e, projectKey: pk });
237
+ // Inject projectKey в target если отсутствует (backward-compat).
238
+ for (const e of evts) {
239
+ const ev = { ...e, projectKey: pk };
240
+ if (e.target && !e.target.projectKey && pk !== '_global') {
241
+ ev.target = { ...e.target, projectKey: pk };
242
+ }
243
+ merged.push(ev);
244
+ }
159
245
  }
160
246
  merged.sort((a, b) => a.ts - b.ts);
161
247
  return merged.slice(-limit);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -1969,7 +1969,12 @@ async function resumeJob(node, bKey, boardHandle) {
1969
1969
 
1970
1970
  if (node.generated.taskId) {
1971
1971
  // Уже зарегистрировано в KIE — просто опрашиваем.
1972
- const job = { boardKey: bKey, boardHandle, kind, taskId: node.generated.taskId, nodeId: node.id };
1972
+ // КРИТИЧНО: projectKey обязателен без него на completion showToast
1973
+ // создаст event с target БЕЗ projectKey → клик по уведомлению ведёт
1974
+ // в никуда (navigateToTarget рано return'ит). Берём из текущего state.
1975
+ const projectKey = state.cloudProjectId ? 'cloud:' + state.cloudProjectId
1976
+ : state.filmHandle?.name ? 'folder:' + state.filmHandle.name : null;
1977
+ const job = { boardKey: bKey, boardHandle, kind, taskId: node.generated.taskId, nodeId: node.id, projectKey };
1973
1978
  state.jobs.set(node.id, job);
1974
1979
  updateJobsBadge();
1975
1980
  try {
@@ -251,7 +251,8 @@
251
251
  // не ловился (юзер видел два одинаковых «✓ image «name» готов»,
252
252
  // один синий, второй зелёный). Сейчас одинаковый nodeId-completion
253
253
  // в окне 30s — один event.
254
- const DEDUP_WINDOW_MS = 30 * 1000;
254
+ const DEDUP_DEFAULT_WINDOW_MS = 30 * 1000;
255
+ const DEDUP_NODE_WINDOW_MS = 24 * 3600 * 1000; // node-completion: дубль через 20+ мин — всё равно дубль
255
256
  // События с этими kind считаются «completion» — для них дедупим по nodeId.
256
257
  const _COMPLETION_KINDS = new Set(['ok', 'error', 'info', 'warn']);
257
258
  function _dedupKey(kind, text, target) {
@@ -265,7 +266,10 @@
265
266
  const k = kind || 'info';
266
267
  const dk = _dedupKey(k, text, target);
267
268
  if (dk) {
268
- const cutoff = Date.now() - DEDUP_WINDOW_MS;
269
+ // Для node-events окно длинное (gen завершается ОДИН раз), для всего
270
+ // остального — короткое (могут быть несколько похожих info-toast'ов).
271
+ const windowMs = dk.startsWith('node:') ? DEDUP_NODE_WINDOW_MS : DEDUP_DEFAULT_WINDOW_MS;
272
+ const cutoff = Date.now() - windowMs;
269
273
  for (let i = events.length - 1; i >= 0; i--) {
270
274
  if (events[i].ts < cutoff) break;
271
275
  if (events[i]._dk === dk) {