kingkont 0.17.0 → 0.17.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/chatSession.js +29 -1
- package/lib/projectPaths.js +62 -0
- package/package.json +1 -1
- package/renderer/board.js +16 -0
- package/renderer/chat.js +17 -0
- package/server.js +29 -0
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
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// lib/projectPaths.js — server-side registry абсолютных путей проектов.
|
|
2
|
+
//
|
|
3
|
+
// FSAH (File System Access API) — это browser-only. Сервер (Node.js) не
|
|
4
|
+
// может работать с его DirectoryHandle. Но в Electron мы можем извлечь
|
|
5
|
+
// абс-путь любого файла внутри папки через webUtils.getPathForFile и
|
|
6
|
+
// зарегистрировать здесь — после чего сервер напрямую читает/пишет через
|
|
7
|
+
// node:fs.
|
|
8
|
+
//
|
|
9
|
+
// Для cloud-проектов путь однозначно: userData/cloud-projects/<id>/
|
|
10
|
+
// (см. main.js cloudResolve). Их регистрировать не нужно — путь
|
|
11
|
+
// derivable.
|
|
12
|
+
//
|
|
13
|
+
// Для folder-проектов (showDirectoryPicker) путь регистрируется
|
|
14
|
+
// renderer'ом на openFilm через POST /api/project/register
|
|
15
|
+
// {projectKey, absPath}.
|
|
16
|
+
//
|
|
17
|
+
// API:
|
|
18
|
+
// register(projectKey, absPath)
|
|
19
|
+
// resolve(projectKey) → absPath или null
|
|
20
|
+
// resolveBoardPath(projectKey, kind, name) → абс-путь board-folder'a
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
// Map<projectKey, absPath>.
|
|
27
|
+
// projectKey формат: 'cloud:<id>' | 'folder:<name>'.
|
|
28
|
+
const paths = new Map();
|
|
29
|
+
let _userDataDir = null;
|
|
30
|
+
|
|
31
|
+
function init({ userDataDir }) {
|
|
32
|
+
_userDataDir = userDataDir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function register(projectKey, absPath) {
|
|
36
|
+
if (!projectKey || !absPath) return;
|
|
37
|
+
paths.set(projectKey, absPath);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolve(projectKey) {
|
|
41
|
+
if (!projectKey) return null;
|
|
42
|
+
// Cloud — derivable из userDataDir.
|
|
43
|
+
if (projectKey.startsWith('cloud:')) {
|
|
44
|
+
if (!_userDataDir) return null;
|
|
45
|
+
const id = projectKey.slice('cloud:'.length);
|
|
46
|
+
const safe = String(id).replace(/[\\/]/g, '_');
|
|
47
|
+
return path.join(_userDataDir, 'cloud-projects', safe);
|
|
48
|
+
}
|
|
49
|
+
// Folder — из registry.
|
|
50
|
+
return paths.get(projectKey) || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Полный путь к папке доски (scene) внутри проекта. kind: episode|character|location.
|
|
54
|
+
function resolveBoardPath(projectKey, kind, name) {
|
|
55
|
+
const root = resolve(projectKey);
|
|
56
|
+
if (!root) return null;
|
|
57
|
+
if (kind === 'character') return path.join(root, '_characters', name);
|
|
58
|
+
if (kind === 'location') return path.join(root, '_locations', name);
|
|
59
|
+
return path.join(root, name); // episode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { init, register, resolve, resolveBoardPath };
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -1182,6 +1182,22 @@ async function openFilm(handle) {
|
|
|
1182
1182
|
// Запоминаем что юзер сейчас в проекте → Cmd+R откроет его снова.
|
|
1183
1183
|
try { localStorage.setItem('lastLocation', 'project'); } catch {}
|
|
1184
1184
|
window.appProject?.notifyState(true);
|
|
1185
|
+
// Register абс-путь folder-проекта на сервере — так server-side задачи
|
|
1186
|
+
// (jobsHub poller, future server-tools) могут писать файлы напрямую без
|
|
1187
|
+
// FSAH. Cloud не регистрируем (path derivable из cloudProjectId).
|
|
1188
|
+
// Не блокируем дальнейший openFilm — сервер просто не получит путь и
|
|
1189
|
+
// server-side ops для этого проекта будут недоступны.
|
|
1190
|
+
if (handle && !window.cloudFsShim?.isCloudHandle?.(handle)) {
|
|
1191
|
+
try {
|
|
1192
|
+
const absPath = await deriveFolderAbsPath(handle);
|
|
1193
|
+
if (absPath) {
|
|
1194
|
+
await fetch('/api/project/register', {
|
|
1195
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1196
|
+
body: JSON.stringify({ projectKey: 'folder:' + handle.name, absPath }),
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
} catch (e) { vlog('warn', 'project register failed: ' + e?.message); }
|
|
1200
|
+
}
|
|
1185
1201
|
ensureClaudeMd(handle).catch(() => {});
|
|
1186
1202
|
// Подзаголовок шапки = имя открытого проекта (вместо «Видео-редактор»).
|
|
1187
1203
|
const sub = $('brandSub');
|
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/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const providers = require('./lib/providers');
|
|
|
16
16
|
const chatSession = require('./lib/chatSession');
|
|
17
17
|
const jobsHub = require('./lib/jobsHub');
|
|
18
18
|
const wsHub = require('./lib/wsHub');
|
|
19
|
+
const projectPaths = require('./lib/projectPaths');
|
|
19
20
|
|
|
20
21
|
// ---------- .env loader (без зависимостей) ----------
|
|
21
22
|
function loadEnv() {
|
|
@@ -371,6 +372,30 @@ async function handleJobsList(res, url) {
|
|
|
371
372
|
} catch (e) { sendError(res, e, 500); }
|
|
372
373
|
}
|
|
373
374
|
|
|
375
|
+
// =============================================================================
|
|
376
|
+
// Projects: regission абсолютного пути folder-проекта (renderer достаёт через
|
|
377
|
+
// webUtils.getPathForFile). Cloud-проекты регистрировать не нужно — путь
|
|
378
|
+
// derivable (userData/cloud-projects/<id>/).
|
|
379
|
+
// =============================================================================
|
|
380
|
+
async function handleProjectRegister(req, res) {
|
|
381
|
+
try {
|
|
382
|
+
const body = await readJson(req);
|
|
383
|
+
const { projectKey, absPath } = body || {};
|
|
384
|
+
if (!projectKey) return send(res, 400, { error: 'projectKey обязателен' });
|
|
385
|
+
if (!absPath || typeof absPath !== 'string') return send(res, 400, { error: 'absPath обязателен' });
|
|
386
|
+
projectPaths.register(projectKey, absPath);
|
|
387
|
+
send(res, 200, { ok: true, registered: { projectKey, absPath } });
|
|
388
|
+
} catch (e) { sendError(res, e, 500); }
|
|
389
|
+
}
|
|
390
|
+
async function handleProjectResolve(res, url) {
|
|
391
|
+
try {
|
|
392
|
+
const projectKey = url.searchParams.get('projectKey');
|
|
393
|
+
if (!projectKey) return send(res, 400, { error: 'projectKey обязателен' });
|
|
394
|
+
const absPath = projectPaths.resolve(projectKey);
|
|
395
|
+
send(res, 200, { projectKey, absPath, registered: !!absPath });
|
|
396
|
+
} catch (e) { sendError(res, e, 500); }
|
|
397
|
+
}
|
|
398
|
+
|
|
374
399
|
// =============================================================================
|
|
375
400
|
// Static files (renderer assets).
|
|
376
401
|
// =============================================================================
|
|
@@ -428,6 +453,9 @@ const server = createServer(async (req, res) => {
|
|
|
428
453
|
// Jobs hub.
|
|
429
454
|
if (req.method === 'POST' && url.pathname === '/api/jobs/track') return handleJobsTrack(req, res);
|
|
430
455
|
if (req.method === 'GET' && url.pathname === '/api/jobs') return handleJobsList(res, url);
|
|
456
|
+
// Projects: register абс-путя для server-side fs-доступа.
|
|
457
|
+
if (req.method === 'POST' && url.pathname === '/api/project/register') return handleProjectRegister(req, res);
|
|
458
|
+
if (req.method === 'GET' && url.pathname === '/api/project/resolve') return handleProjectResolve(res, url);
|
|
431
459
|
// Cloud-projects routes — зеркало templates, но для редактируемых проектов.
|
|
432
460
|
if (req.method === 'GET' && url.pathname === '/api/projects') return handleProjectsList(res);
|
|
433
461
|
if (req.method === 'POST' && url.pathname === '/api/projects') return handleProjectCreate(req, res);
|
|
@@ -455,6 +483,7 @@ function start(port = PORT, opts = {}) {
|
|
|
455
483
|
// Init chatSession с userDataDir для персистентности историй.
|
|
456
484
|
// Без opts.userDataDir чат живёт только in-memory (CLI-режим).
|
|
457
485
|
chatSession.init({ userDataDir: opts.userDataDir || null });
|
|
486
|
+
projectPaths.init({ userDataDir: opts.userDataDir || null });
|
|
458
487
|
// jobsHub нужен settingsGetter чтобы поллить провайдеров (Chatium token,
|
|
459
488
|
// KIE_API_KEY и т.п.).
|
|
460
489
|
jobsHub.setSettingsGetter(getSettings);
|