kingkont 0.17.1 → 0.17.3
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 +2 -0
- package/lib/jobsHub.js +28 -11
- package/lib/projectPaths.js +62 -0
- package/package.json +1 -1
- package/renderer/board.js +16 -0
- package/renderer/generate.js +3 -2
- package/renderer/notifyPanel.js +156 -0
- package/renderer/state.js +3 -2
- package/server.js +29 -0
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
|
@@ -36,16 +36,25 @@ function start({ projectKey, jobId, kind, name, type, taskId }) {
|
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
startedAt: existing.startedAt || Date.now(),
|
|
50
|
+
};
|
|
51
|
+
m.set(jobId, merged);
|
|
52
|
+
console.log('[jobsHub] start/update', { projectKey, jobId, ...merged });
|
|
43
53
|
_emit(projectKey);
|
|
44
|
-
// Если есть taskId
|
|
45
|
-
// полит
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
_startPoller({ projectKey, jobId, taskId, kind });
|
|
54
|
+
// Если есть И taskId, И kind — стартуем серверный poller. Renderer тоже
|
|
55
|
+
// полит параллельно. Если renderer закрыт — серверный продолжает.
|
|
56
|
+
if (merged.taskId && (merged.kind === 'image' || merged.kind === 'video')) {
|
|
57
|
+
_startPoller({ projectKey, jobId, taskId: merged.taskId, kind: merged.kind });
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
|
|
@@ -62,19 +71,27 @@ function end({ projectKey, jobId }) {
|
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
function _startPoller({ projectKey, jobId, taskId, kind }) {
|
|
65
|
-
if (pollers.has(jobId))
|
|
74
|
+
if (pollers.has(jobId)) {
|
|
75
|
+
console.log('[jobsHub] poller already running for', jobId);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log('[jobsHub] _startPoller', { projectKey, jobId, taskId, kind });
|
|
66
79
|
const providers = require('./providers');
|
|
67
80
|
const tick = async () => {
|
|
68
81
|
const entry = pollers.get(jobId);
|
|
69
82
|
if (!entry) return;
|
|
70
83
|
entry.lastPollAt = Date.now();
|
|
71
84
|
let r;
|
|
72
|
-
try {
|
|
85
|
+
try {
|
|
86
|
+
r = await providers.pollGeneration(taskId, _settingsGetter());
|
|
87
|
+
console.log('[jobsHub] poll', jobId, '→', r.status, r.state || '');
|
|
88
|
+
}
|
|
73
89
|
catch (e) {
|
|
74
90
|
console.warn('[jobsHub] poll failed for', jobId, e?.message);
|
|
75
91
|
return; // следующий tick попробует снова
|
|
76
92
|
}
|
|
77
93
|
if (r.status === 'done') {
|
|
94
|
+
console.log('[jobsHub] DONE', jobId, '→', r.url);
|
|
78
95
|
_stopPoller(jobId);
|
|
79
96
|
// Push WS event с completion-info. Renderer (если жив) скачает
|
|
80
97
|
// через /api/proxy и запишет в FSAH. Если renderer закрыт — event
|
|
@@ -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/generate.js
CHANGED
|
@@ -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
|
-
|
|
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,156 @@
|
|
|
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
|
+
const btn = document.createElement('button');
|
|
26
|
+
btn.id = 'notifyBtn';
|
|
27
|
+
btn.title = 'События (генерации, чат, ошибки)';
|
|
28
|
+
btn.style.cssText = 'position:fixed; top:12px; right:24px; 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
|
+
btn.innerHTML = '🔔';
|
|
30
|
+
document.body.appendChild(btn);
|
|
31
|
+
btn.addEventListener('click', toggle);
|
|
32
|
+
// Бейдж со счётчиком unread.
|
|
33
|
+
const badge = document.createElement('span');
|
|
34
|
+
badge.id = 'notifyBadge';
|
|
35
|
+
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
|
+
btn.appendChild(badge);
|
|
37
|
+
|
|
38
|
+
// Панель.
|
|
39
|
+
const panel = document.createElement('div');
|
|
40
|
+
panel.id = 'notifyPanel';
|
|
41
|
+
panel.style.cssText = 'position:fixed; top:50px; right:16px; width:380px; max-height:75vh; 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
|
+
panel.innerHTML = `
|
|
43
|
+
<div style="padding:10px 12px; border-bottom:1px solid #2a2a2a; display:flex; align-items:center; gap:8px;">
|
|
44
|
+
<strong style="font-size:13px; color:#ddd;">События</strong>
|
|
45
|
+
<span style="flex:1;"></span>
|
|
46
|
+
<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>
|
|
47
|
+
<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>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="notifyList" style="flex:1; overflow-y:auto; padding:6px;"></div>
|
|
50
|
+
`;
|
|
51
|
+
document.body.appendChild(panel);
|
|
52
|
+
$('notifyClose').addEventListener('click', () => setOpen(false));
|
|
53
|
+
$('notifyClear').addEventListener('click', () => { events.length = 0; unread = 0; render(); });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setOpen(open) {
|
|
57
|
+
_ensureUI();
|
|
58
|
+
panelOpen = open;
|
|
59
|
+
$('notifyPanel').style.display = open ? 'flex' : 'none';
|
|
60
|
+
if (open) { unread = 0; updateBadge(); render(); }
|
|
61
|
+
}
|
|
62
|
+
function toggle() { setOpen(!panelOpen); }
|
|
63
|
+
|
|
64
|
+
function updateBadge() {
|
|
65
|
+
const b = $('notifyBadge');
|
|
66
|
+
if (!b) return;
|
|
67
|
+
if (unread > 0) {
|
|
68
|
+
b.textContent = unread > 99 ? '99+' : String(unread);
|
|
69
|
+
b.style.display = '';
|
|
70
|
+
} else b.style.display = 'none';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function render() {
|
|
74
|
+
const list = $('notifyList');
|
|
75
|
+
if (!list) return;
|
|
76
|
+
list.innerHTML = '';
|
|
77
|
+
if (!events.length) {
|
|
78
|
+
list.innerHTML = '<div style="padding:24px; text-align:center; color:#555; font-size:12px;">Нет событий</div>';
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
82
|
+
const e = events[i];
|
|
83
|
+
const row = document.createElement('div');
|
|
84
|
+
const colors = {
|
|
85
|
+
ok: '#3a6a3a', error: '#8a3a3a', info: '#3a5a8a', warn: '#8a7a3a', chat: '#7a4a8a',
|
|
86
|
+
};
|
|
87
|
+
const c = colors[e.kind] || colors.info;
|
|
88
|
+
const dt = new Date(e.ts);
|
|
89
|
+
const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
|
|
90
|
+
row.style.cssText = `padding:8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:default;`;
|
|
91
|
+
row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start;">
|
|
92
|
+
<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>
|
|
94
|
+
</div>`;
|
|
95
|
+
row.querySelector('div > div').textContent = e.text;
|
|
96
|
+
list.appendChild(row);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Public API.
|
|
101
|
+
function addEvent({ kind, text, raw }) {
|
|
102
|
+
if (!text) return;
|
|
103
|
+
events.push({ ts: Date.now(), kind: kind || 'info', text: String(text).slice(0, 300), raw });
|
|
104
|
+
if (events.length > MAX_EVENTS) events.shift();
|
|
105
|
+
if (!panelOpen) {
|
|
106
|
+
unread++;
|
|
107
|
+
updateBadge();
|
|
108
|
+
} else {
|
|
109
|
+
render();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
|
|
113
|
+
|
|
114
|
+
// Init: ensure UI exists ASAP, hook into showToast и WS-events.
|
|
115
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
116
|
+
_ensureUI();
|
|
117
|
+
// Wrap showToast чтобы дублировать в панель.
|
|
118
|
+
if (typeof window.showToast === 'function') {
|
|
119
|
+
const orig = window.showToast;
|
|
120
|
+
window.showToast = function (text, kind) {
|
|
121
|
+
try { addEvent({ kind: kind || 'info', text }); } catch {}
|
|
122
|
+
return orig.apply(this, arguments);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// WS-канал jobs:all для start/end/done/failed events. Connect к /ws.
|
|
126
|
+
function _connect() {
|
|
127
|
+
let ws;
|
|
128
|
+
try {
|
|
129
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
130
|
+
ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
131
|
+
} catch { setTimeout(_connect, 5000); return; }
|
|
132
|
+
ws.onopen = () => {
|
|
133
|
+
try { ws.send(JSON.stringify({ type: 'subscribe', channel: 'jobs:all' })); } catch {}
|
|
134
|
+
};
|
|
135
|
+
ws.onmessage = (ev) => {
|
|
136
|
+
let m; try { m = JSON.parse(ev.data); } catch { return; }
|
|
137
|
+
if (m?.type !== 'event') return;
|
|
138
|
+
const e = m.event || {};
|
|
139
|
+
const ch = m.channel || '';
|
|
140
|
+
if (ch === 'jobs:all') {
|
|
141
|
+
if (e.kind === 'done') {
|
|
142
|
+
addEvent({ kind: 'ok', text: `✓ ${e.kindOf || 'gen'} готов в ${e.projectKey || ''} (jobId=${(e.jobId||'').slice(0,8)})` });
|
|
143
|
+
} else if (e.kind === 'failed') {
|
|
144
|
+
addEvent({ kind: 'error', text: `⚠ ${e.kindOf || 'gen'} провалился: ${(e.error || '').slice(0, 100)}` });
|
|
145
|
+
} else if (e.kind === 'changed' && Array.isArray(e.list)) {
|
|
146
|
+
// Только при изменении (start/end) — добавим краткую инфу.
|
|
147
|
+
// Без шума — addEvent только если что-то стало активно или закончилось.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
ws.onclose = () => setTimeout(_connect, 5000);
|
|
152
|
+
ws.onerror = () => {};
|
|
153
|
+
}
|
|
154
|
+
_connect();
|
|
155
|
+
});
|
|
156
|
+
})();
|
package/renderer/state.js
CHANGED
|
@@ -108,12 +108,13 @@ function bgJobStart(info) {
|
|
|
108
108
|
}).catch(() => {});
|
|
109
109
|
}
|
|
110
110
|
// Update taskId for an existing job (server didn't have it on initial track).
|
|
111
|
-
|
|
111
|
+
// kind важен — без него server-side merge оставит null и poller не стартует.
|
|
112
|
+
function bgJobUpdateTaskId(nodeId, taskId, projectKey, kind) {
|
|
112
113
|
const pk = projectKey || _projectKeyForCurrent();
|
|
113
114
|
if (!pk || !taskId) return;
|
|
114
115
|
fetch('/api/jobs/track', {
|
|
115
116
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
116
|
-
body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId }),
|
|
117
|
+
body: JSON.stringify({ action: 'start', projectKey: pk, jobId: nodeId, taskId, kind }),
|
|
117
118
|
}).catch(() => {});
|
|
118
119
|
}
|
|
119
120
|
window.bgJobUpdateTaskId = bgJobUpdateTaskId;
|
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);
|