kingkont 0.20.3 → 0.20.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/providers.js +64 -1
- package/package.json +1 -1
- package/renderer/cloudFs.js +12 -0
- package/renderer/cloudProjects.js +117 -12
- package/scripts/test-gpt-image-ref.js +69 -0
- package/server.js +40 -0
package/lib/providers.js
CHANGED
|
@@ -31,8 +31,13 @@ const CHATIUM_PATHS = {
|
|
|
31
31
|
// Cloud-projects (НЕ шаблоны) — редактируемые проекты юзера на сервере.
|
|
32
32
|
// Owner-only видимость, save-to-server по explicit-кнопке.
|
|
33
33
|
// Формат:
|
|
34
|
-
// { id, name, manifest: { boards: [{ kind, name, scene, files: {relPath: cdnUrl} }] }, files, coverUrl }
|
|
34
|
+
// { id, name, manifest: { boards: [{ kind, name, scene, files: {relPath: cdnUrl}, texts: {relPath: textId} }] }, files, coverUrl }
|
|
35
35
|
projects: '/app/spaces/server/api/projects',
|
|
36
|
+
// Текстовый контент (.md) проектов — отдельная таблица t_spaces_server_project_texts_K1.
|
|
37
|
+
// Manifest проекта ссылается на записи по id. Зачем: Chatium-storage даёт
|
|
38
|
+
// 502 на text/markdown; CDN-roundtrip оверхед для маленьких файлов;
|
|
39
|
+
// изменение текста = UPDATE существующей записи, без orphan'ов в storage.
|
|
40
|
+
projectTexts: '/app/spaces/server/api/project_texts',
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
const CHATIUM_CDN = 'https://fs.chatium.ru/get';
|
|
@@ -1144,6 +1149,59 @@ async function deleteProject(id, s) {
|
|
|
1144
1149
|
return true;
|
|
1145
1150
|
}
|
|
1146
1151
|
|
|
1152
|
+
// ---- Project texts (.md содержимое text-нод). ------------------------------
|
|
1153
|
+
// Mirror of project CRUD. Хранятся в отдельной таблице чтобы не раздувать
|
|
1154
|
+
// manifest и обходить 502 на text/markdown в Chatium-storage.
|
|
1155
|
+
|
|
1156
|
+
async function createProjectText(body, s) {
|
|
1157
|
+
const headers = { ...chatiumAuthHeaders(s), 'Content-Type': 'application/json' };
|
|
1158
|
+
logCall('POST', 'Chatium', chatiumBase(s) + CHATIUM_PATHS.projectTexts,
|
|
1159
|
+
`projectId=${body?.projectId} relPath=${body?.relPath} len=${body?.content?.length||0}`);
|
|
1160
|
+
const r = await fetch(chatiumBase(s) + CHATIUM_PATHS.projectTexts, {
|
|
1161
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
1162
|
+
});
|
|
1163
|
+
const text = await r.text();
|
|
1164
|
+
let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
|
|
1165
|
+
if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
|
|
1166
|
+
return d;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async function getProjectText(id, s) {
|
|
1170
|
+
const headers = chatiumAuthHeaders(s);
|
|
1171
|
+
const url = `${chatiumBase(s)}${CHATIUM_PATHS.projectTexts}~get?id=${encodeURIComponent(id)}`;
|
|
1172
|
+
logCall('GET ', 'Chatium', url);
|
|
1173
|
+
const r = await fetch(url, { headers });
|
|
1174
|
+
const text = await r.text();
|
|
1175
|
+
let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
|
|
1176
|
+
if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
|
|
1177
|
+
return d;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async function updateProjectText(id, body, s) {
|
|
1181
|
+
const headers = { ...chatiumAuthHeaders(s), 'Content-Type': 'application/json' };
|
|
1182
|
+
const url = `${chatiumBase(s)}${CHATIUM_PATHS.projectTexts}~update?id=${encodeURIComponent(id)}`;
|
|
1183
|
+
logCall('POST', 'Chatium', url, `len=${body?.content?.length||0}`);
|
|
1184
|
+
const r = await fetch(url, {
|
|
1185
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
1186
|
+
});
|
|
1187
|
+
const text = await r.text();
|
|
1188
|
+
let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
|
|
1189
|
+
if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
|
|
1190
|
+
return d;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function deleteProjectText(id, s) {
|
|
1194
|
+
const headers = chatiumAuthHeaders(s);
|
|
1195
|
+
const url = `${chatiumBase(s)}${CHATIUM_PATHS.projectTexts}~delete?id=${encodeURIComponent(id)}`;
|
|
1196
|
+
logCall('POST', 'Chatium', url);
|
|
1197
|
+
const r = await fetch(url, { method: 'POST', headers });
|
|
1198
|
+
if (!r.ok) {
|
|
1199
|
+
const text = await r.text().catch(() => '');
|
|
1200
|
+
throw new Error(text || `HTTP ${r.status}`);
|
|
1201
|
+
}
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1147
1205
|
async function listElevenVoices() {
|
|
1148
1206
|
const key = process.env.ELEVENLABS_API_KEY;
|
|
1149
1207
|
if (!key) throw new Error('ELEVENLABS_API_KEY не задан');
|
|
@@ -1190,6 +1248,11 @@ module.exports = {
|
|
|
1190
1248
|
createProject,
|
|
1191
1249
|
updateProject,
|
|
1192
1250
|
deleteProject,
|
|
1251
|
+
// Project texts (.md контент text-нод — отдельная таблица)
|
|
1252
|
+
createProjectText,
|
|
1253
|
+
getProjectText,
|
|
1254
|
+
updateProjectText,
|
|
1255
|
+
deleteProjectText,
|
|
1193
1256
|
// Constants (для server.js / тестов)
|
|
1194
1257
|
KIE_IMAGE_MODELS,
|
|
1195
1258
|
KIE_VIDEO_MODELS,
|
package/package.json
CHANGED
package/renderer/cloudFs.js
CHANGED
|
@@ -296,5 +296,17 @@
|
|
|
296
296
|
await backend.ensure();
|
|
297
297
|
return backend.write(relPath, data);
|
|
298
298
|
},
|
|
299
|
+
async readFile(projectId, relPath) {
|
|
300
|
+
const backend = makeBackend(projectId);
|
|
301
|
+
return backend.read(relPath);
|
|
302
|
+
},
|
|
303
|
+
async readTextFile(projectId, relPath) {
|
|
304
|
+
const backend = makeBackend(projectId);
|
|
305
|
+
try { return await backend.readText(relPath); } catch { return null; }
|
|
306
|
+
},
|
|
307
|
+
async existsFile(projectId, relPath) {
|
|
308
|
+
const backend = makeBackend(projectId);
|
|
309
|
+
return backend.exists(relPath);
|
|
310
|
+
},
|
|
299
311
|
};
|
|
300
312
|
})();
|
|
@@ -257,14 +257,22 @@
|
|
|
257
257
|
} catch {}
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
// 1. Проверяем синхронизирован ли уже
|
|
261
|
-
|
|
260
|
+
// 1. Проверяем синхронизирован ли уже локально.
|
|
261
|
+
// Electron: userData/cloud-projects/<id>/ через window.cloudFs (диск).
|
|
262
|
+
// Web: in-memory map в cloudFsShim — живёт пока страница не перезагружена.
|
|
263
|
+
// Без web-ветки браузер качал файлы при каждом open, хотя в памяти
|
|
264
|
+
// они уже лежали с прошлого визита в этой же сессии.
|
|
265
|
+
if (!opts.forceRefresh) {
|
|
262
266
|
try {
|
|
263
|
-
|
|
264
|
-
if (
|
|
267
|
+
let metaText = null;
|
|
268
|
+
if (window.cloudFs) {
|
|
269
|
+
metaText = await window.cloudFs.readText(projectId, '.kingkont-meta.json');
|
|
270
|
+
} else if (window.cloudFsShim?.readTextFile) {
|
|
271
|
+
metaText = await window.cloudFsShim.readTextFile(projectId, '.kingkont-meta.json');
|
|
272
|
+
}
|
|
273
|
+
if (metaText) {
|
|
265
274
|
const meta = JSON.parse(metaText);
|
|
266
275
|
if (meta.cloudProjectId === projectId) {
|
|
267
|
-
// Local copy готова — открываем без скачивания.
|
|
268
276
|
const handle = await window.cloudFsShim.makeCloudHandle(projectId, suggestedName || projectId);
|
|
269
277
|
state.cloudProjectId = projectId;
|
|
270
278
|
state.cloudDirty = false;
|
|
@@ -288,13 +296,15 @@
|
|
|
288
296
|
const handle = await window.cloudFsShim.makeCloudHandle(projectId, proj.name || suggestedName || 'Project');
|
|
289
297
|
// Скачиваем все board'ы манифеста (если они уже не лежат локально с тем же hash).
|
|
290
298
|
const boards = Array.isArray(proj.manifest?.boards) ? proj.manifest.boards : [];
|
|
291
|
-
// Сначала собираем общий список файлов для
|
|
292
|
-
const totalFiles = boards.reduce(
|
|
299
|
+
// Сначала собираем общий список файлов для прогресса (CDN-файлы + DB-тексты).
|
|
300
|
+
const totalFiles = boards.reduce(
|
|
301
|
+
(s, b) => s + Object.keys(b?.files || {}).length + Object.keys(b?.texts || {}).length, 0);
|
|
293
302
|
let done = 0;
|
|
294
|
-
// fileHashes индексируем тем же ключом что использует save:
|
|
295
|
-
//
|
|
296
|
-
// перезальёт
|
|
303
|
+
// fileHashes / textHashes индексируем тем же ключом что использует save:
|
|
304
|
+
// `${kind}/${name}/${relPath}`. Иначе при следующем save dedup не найдёт
|
|
305
|
+
// ничего и перезальёт всё заново.
|
|
297
306
|
const fileHashes = {};
|
|
307
|
+
const textHashes = {};
|
|
298
308
|
for (const board of boards) {
|
|
299
309
|
const boardDir = boardKindToDir(board.kind, board.name);
|
|
300
310
|
// 1) scene.json
|
|
@@ -302,7 +312,7 @@
|
|
|
302
312
|
await writeCloudFile(projectId, joinPath(boardDir, 'scene.json'),
|
|
303
313
|
new TextEncoder().encode(JSON.stringify(board.scene, null, 2)));
|
|
304
314
|
}
|
|
305
|
-
// 2) media-файлы
|
|
315
|
+
// 2) media-файлы — из CDN.
|
|
306
316
|
for (const [relPath, cdnUrl] of Object.entries(board.files || {})) {
|
|
307
317
|
done++;
|
|
308
318
|
PROGRESS.update(done, totalFiles, `Загрузка ${relPath} (${done}/${totalFiles})`);
|
|
@@ -324,6 +334,27 @@
|
|
|
324
334
|
url: cdnUrl, size: buf.byteLength, mtime,
|
|
325
335
|
};
|
|
326
336
|
}
|
|
337
|
+
// 3) text-файлы (.md) — из таблицы project_texts по textId.
|
|
338
|
+
for (const [relPath, textId] of Object.entries(board.texts || {})) {
|
|
339
|
+
done++;
|
|
340
|
+
PROGRESS.update(done, totalFiles, `Загрузка ${relPath} (${done}/${totalFiles})`);
|
|
341
|
+
const tr = await fetch('/api/project-texts/' + encodeURIComponent(textId));
|
|
342
|
+
if (!tr.ok) throw new Error(`Не удалось получить текст ${relPath}: ${tr.status}`);
|
|
343
|
+
const td = await tr.json();
|
|
344
|
+
const buf = new TextEncoder().encode(td.content || '');
|
|
345
|
+
const localPath = joinPath(boardDir, relPath);
|
|
346
|
+
await writeCloudFile(projectId, localPath, buf);
|
|
347
|
+
let mtime = Date.now();
|
|
348
|
+
if (window.cloudFs?.stat) {
|
|
349
|
+
try {
|
|
350
|
+
const st = await window.cloudFs.stat(projectId, localPath);
|
|
351
|
+
if (st?.mtimeMs) mtime = st.mtimeMs;
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
textHashes[`${board.kind}/${board.name}/${relPath}`] = {
|
|
355
|
+
textId, size: buf.byteLength, mtime,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
327
358
|
}
|
|
328
359
|
// 3) cover (опционально). Запоминаем size+mtime → coverHash для
|
|
329
360
|
// dedup'а при следующем save'е (иначе первый же save перезальёт
|
|
@@ -356,6 +387,7 @@
|
|
|
356
387
|
new TextEncoder().encode(JSON.stringify({
|
|
357
388
|
cloudProjectId: projectId,
|
|
358
389
|
fileHashes,
|
|
390
|
+
textHashes,
|
|
359
391
|
coverUrl: proj.coverUrl || null,
|
|
360
392
|
coverHash: openedCoverHash,
|
|
361
393
|
syncedAt: Date.now(),
|
|
@@ -432,7 +464,9 @@
|
|
|
432
464
|
? (await readProjectMeta(state.filmHandle)) || {}
|
|
433
465
|
: {};
|
|
434
466
|
const oldHashes = meta.fileHashes || {};
|
|
467
|
+
const oldTextHashes = meta.textHashes || {};
|
|
435
468
|
const newHashes = {};
|
|
469
|
+
const newTextHashes = {};
|
|
436
470
|
|
|
437
471
|
// 1) Собираем все board'ы локального проекта (та же логика что у
|
|
438
472
|
// saveCurrentProjectAsTemplate из templates.js).
|
|
@@ -451,6 +485,7 @@
|
|
|
451
485
|
try { scene = JSON.parse(text); } catch {}
|
|
452
486
|
}
|
|
453
487
|
const boardFiles = {};
|
|
488
|
+
const boardTexts = {};
|
|
454
489
|
// walkBoardFiles возвращает {relPath, fileHandle}.
|
|
455
490
|
for (const f of board.mediaFiles) {
|
|
456
491
|
if (!f.fileHandle) {
|
|
@@ -459,6 +494,73 @@
|
|
|
459
494
|
}
|
|
460
495
|
const file = await f.fileHandle.getFile();
|
|
461
496
|
const metaKey = `${board.kind}/${board.name}/${f.relPath}`;
|
|
497
|
+
const isMd = /\.md$/i.test(f.relPath);
|
|
498
|
+
|
|
499
|
+
// ---- .md → DB-таблица project_texts (не CDN-storage). ----
|
|
500
|
+
// На стороне клиента грейсфул-fallback: если эндпоинт ещё не задеплоен
|
|
501
|
+
// (HTTP 404 → "is not found in UgcPluginEntity"), .md уходит по
|
|
502
|
+
// старому пути через CDN. После деплоя сервера новый путь активируется
|
|
503
|
+
// автоматически, без обновления клиента.
|
|
504
|
+
if (isMd) {
|
|
505
|
+
const cachedText = oldTextHashes[metaKey];
|
|
506
|
+
// Dedup: size+mtime совпадают → переиспользуем textId, нет API-вызова.
|
|
507
|
+
if (cachedText && cachedText.size === file.size && cachedText.mtime === file.lastModified && cachedText.textId) {
|
|
508
|
+
boardTexts[f.relPath] = cachedText.textId;
|
|
509
|
+
newTextHashes[metaKey] = cachedText;
|
|
510
|
+
reused++;
|
|
511
|
+
uploaded++;
|
|
512
|
+
PROGRESS.update(uploaded, total,
|
|
513
|
+
`[${board.kind}/${board.name}] ${f.relPath} (cached, ${uploaded}/${total})`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
PROGRESS.update(uploaded, total,
|
|
517
|
+
`[${board.kind}/${board.name}] ${f.relPath} (text, ${uploaded + 1}/${total})`);
|
|
518
|
+
const content = await file.text();
|
|
519
|
+
let textId = cachedText?.textId || null;
|
|
520
|
+
let dbPathFailed = false;
|
|
521
|
+
if (textId) {
|
|
522
|
+
// UPDATE существующей записи — id остаётся прежним.
|
|
523
|
+
const ur = await fetch('/api/project-texts/' + encodeURIComponent(textId), {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({ content, relPath: f.relPath }),
|
|
527
|
+
});
|
|
528
|
+
if (!ur.ok) {
|
|
529
|
+
if (ur.status === 404) {
|
|
530
|
+
// Либо id протух, либо эндпоинт ещё не задеплоен — пробуем create.
|
|
531
|
+
textId = null;
|
|
532
|
+
} else {
|
|
533
|
+
throw new Error(`update text ${f.relPath} failed: ${ur.status}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (!textId) {
|
|
538
|
+
const cr = await fetch('/api/project-texts', {
|
|
539
|
+
method: 'POST',
|
|
540
|
+
headers: { 'Content-Type': 'application/json' },
|
|
541
|
+
body: JSON.stringify({ projectId, relPath: f.relPath, content }),
|
|
542
|
+
});
|
|
543
|
+
if (cr.ok) {
|
|
544
|
+
const j = await cr.json();
|
|
545
|
+
textId = j.id;
|
|
546
|
+
} else if (cr.status === 404 || cr.status === 502) {
|
|
547
|
+
// Эндпоинт ещё не задеплоен на chatium-стороне → fallback на CDN.
|
|
548
|
+
console.warn(`[cloudProjects] /api/project-texts ${cr.status} → fallback на CDN-upload для ${f.relPath}`);
|
|
549
|
+
dbPathFailed = true;
|
|
550
|
+
} else {
|
|
551
|
+
throw new Error(`create text ${f.relPath} failed: ${cr.status}`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (textId) {
|
|
555
|
+
boardTexts[f.relPath] = textId;
|
|
556
|
+
newTextHashes[metaKey] = { textId, size: file.size, mtime: file.lastModified };
|
|
557
|
+
uploaded++;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
// dbPathFailed → проваливаемся в общий CDN-upload ниже.
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ---- Бинарь → CDN через /api/upload. ----
|
|
462
564
|
const cached = oldHashes[metaKey];
|
|
463
565
|
// Dedup: size + mtime совпадают → переиспользуем CDN-URL.
|
|
464
566
|
// (Это identifier «файл не менялся с момента последней синхронизации»;
|
|
@@ -490,7 +592,9 @@
|
|
|
490
592
|
newHashes[metaKey] = { url: j.url, size: file.size, mtime: file.lastModified };
|
|
491
593
|
uploaded++;
|
|
492
594
|
}
|
|
493
|
-
|
|
595
|
+
const boardEntry = { kind: board.kind, name: board.name, scene, files: boardFiles };
|
|
596
|
+
if (Object.keys(boardTexts).length) boardEntry.texts = boardTexts;
|
|
597
|
+
manifestBoards.push(boardEntry);
|
|
494
598
|
}
|
|
495
599
|
|
|
496
600
|
// 2) Cover (dedup'нутый — не грузим если size+mtime совпадают).
|
|
@@ -547,6 +651,7 @@
|
|
|
547
651
|
...meta,
|
|
548
652
|
cloudProjectId: projectId,
|
|
549
653
|
fileHashes: newHashes,
|
|
654
|
+
textHashes: newTextHashes,
|
|
550
655
|
coverUrl: coverUrl || null,
|
|
551
656
|
coverHash: newCoverHash,
|
|
552
657
|
syncedAt: Date.now(),
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Тест: использует ли gpt-image-2-image-to-image референсное изображение?
|
|
3
|
+
//
|
|
4
|
+
// Делаем 2 генерации с одинаковым промптом:
|
|
5
|
+
// 1) text-only ("control") — gpt-image-2-text-to-image
|
|
6
|
+
// 2) image-to-image — gpt-image-2-image-to-image + reference URL
|
|
7
|
+
//
|
|
8
|
+
// Если референс реально работает — результат #2 будет иметь композицию/
|
|
9
|
+
// объект из reference. Если игнорируется — результаты будут похожи между собой.
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { loadSettings, applySettingsToEnv } = require('../lib/settings');
|
|
16
|
+
const { startGeneration, waitForGeneration, uploadFile, downloadUrl } = require('../lib/providers');
|
|
17
|
+
|
|
18
|
+
// Референс: фото конкретного полосатого котика (Wikipedia, public).
|
|
19
|
+
// Если модель его «видит» — на выходе должен быть похожий кот (та же поза,
|
|
20
|
+
// тот же фон), просто в новом цвете. Если игнорирует — выйдет
|
|
21
|
+
// generic-кот на generic-фоне.
|
|
22
|
+
const REF_URL = 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg';
|
|
23
|
+
const PROMPT = 'recolor this exact cat to be completely white, keep the same pose, background, framing, and scene, photorealistic';
|
|
24
|
+
|
|
25
|
+
(async () => {
|
|
26
|
+
const s = loadSettings();
|
|
27
|
+
applySettingsToEnv(s);
|
|
28
|
+
console.log('[test] useChatium=%s hasToken=%s base=%s', s.useChatium, !!s.chatium?.token, s.chatium?.base);
|
|
29
|
+
|
|
30
|
+
// 1) Скачать референс с Wikipedia, загрузить в Chatium-storage.
|
|
31
|
+
console.log('[test] download ref:', REF_URL);
|
|
32
|
+
const refBuf = await downloadUrl(REF_URL);
|
|
33
|
+
console.log('[test] ref size:', refBuf.length, 'bytes');
|
|
34
|
+
const up = await uploadFile({ buffer: refBuf, filename: 'cat-ref.jpg', mime: 'image/jpeg', settings: s });
|
|
35
|
+
console.log('[test] uploaded ref →', up.url);
|
|
36
|
+
|
|
37
|
+
// 2) Параллельно: text-only (control) + image-to-image (with ref).
|
|
38
|
+
console.log('[test] kick off 2 generations...');
|
|
39
|
+
const opts = { kind: 'image', modelKey: 'gpt-image-2', prompt: PROMPT, aspectRatio: '1:1', settings: s };
|
|
40
|
+
const [ctrlStart, refStart] = await Promise.all([
|
|
41
|
+
startGeneration({ ...opts }),
|
|
42
|
+
startGeneration({ ...opts, imageInputs: [up.url] }),
|
|
43
|
+
]);
|
|
44
|
+
console.log('[test] control task:', ctrlStart.taskId);
|
|
45
|
+
console.log('[test] withref task:', refStart.taskId);
|
|
46
|
+
|
|
47
|
+
const [ctrlRes, refRes] = await Promise.all([
|
|
48
|
+
waitForGeneration(ctrlStart.taskId, s, { timeoutMs: 240000, onProgress: (r) => console.log(' control:', r.state || r.status) }),
|
|
49
|
+
waitForGeneration(refStart.taskId, s, { timeoutMs: 240000, onProgress: (r) => console.log(' withref:', r.state || r.status) }),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
console.log('\n[test] === RESULTS ===');
|
|
53
|
+
console.log('CONTROL:', JSON.stringify(ctrlRes, null, 2));
|
|
54
|
+
console.log('WITH REF:', JSON.stringify(refRes, null, 2));
|
|
55
|
+
|
|
56
|
+
// Скачать оба результата, чтобы можно было визуально сравнить.
|
|
57
|
+
const outDir = '/tmp/kk-gptimage-ref-test';
|
|
58
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
59
|
+
fs.writeFileSync(path.join(outDir, 'reference.jpg'), refBuf);
|
|
60
|
+
for (const [name, res] of [['control', ctrlRes], ['withref', refRes]]) {
|
|
61
|
+
const url = res?.url || res?.result?.url || res?.imageUrl || res?.images?.[0];
|
|
62
|
+
if (!url) { console.warn('[test] no url for', name); continue; }
|
|
63
|
+
const buf = await downloadUrl(url);
|
|
64
|
+
const out = path.join(outDir, `${name}.jpg`);
|
|
65
|
+
fs.writeFileSync(out, buf);
|
|
66
|
+
console.log(`[test] saved ${name}: ${out} (${buf.length} bytes, src=${url})`);
|
|
67
|
+
}
|
|
68
|
+
console.log('\n[test] open with: open ' + outDir);
|
|
69
|
+
})().catch(e => { console.error('[test] FAILED:', e); process.exit(1); });
|
package/server.js
CHANGED
|
@@ -462,6 +462,35 @@ async function handleProjectDelete(res, id) {
|
|
|
462
462
|
} catch (e) { sendError(res, e, 502); }
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// ---- Project texts (.md контент text-нод). --------------------------------
|
|
466
|
+
// Зачем отдельный API: см. lib/providers.js → CHATIUM_PATHS.projectTexts.
|
|
467
|
+
|
|
468
|
+
async function handleProjectTextCreate(req, res) {
|
|
469
|
+
try {
|
|
470
|
+
const body = await readJson(req); // { projectId, relPath, content }
|
|
471
|
+
send(res, 200, await providers.createProjectText(body, getSettings()));
|
|
472
|
+
} catch (e) { sendError(res, e, 502); }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function handleProjectTextGet(res, id) {
|
|
476
|
+
try { send(res, 200, await providers.getProjectText(id, getSettings())); }
|
|
477
|
+
catch (e) { sendError(res, e, 502); }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async function handleProjectTextUpdate(req, res, id) {
|
|
481
|
+
try {
|
|
482
|
+
const body = await readJson(req); // { content?, relPath? }
|
|
483
|
+
send(res, 200, await providers.updateProjectText(id, body, getSettings()));
|
|
484
|
+
} catch (e) { sendError(res, e, 502); }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function handleProjectTextDelete(res, id) {
|
|
488
|
+
try {
|
|
489
|
+
await providers.deleteProjectText(id, getSettings());
|
|
490
|
+
send(res, 200, { ok: true });
|
|
491
|
+
} catch (e) { sendError(res, e, 502); }
|
|
492
|
+
}
|
|
493
|
+
|
|
465
494
|
// =============================================================================
|
|
466
495
|
// Chat sessions: server-side LLM-loop с tool-pump pattern.
|
|
467
496
|
// Клиент: POST /api/chat/send {key, text, system} → starts loop
|
|
@@ -686,6 +715,17 @@ async function _requestHandler(req, res) {
|
|
|
686
715
|
if (req.method === 'DELETE') return handleProjectDelete(res, decodeURIComponent(m[1]));
|
|
687
716
|
}
|
|
688
717
|
}
|
|
718
|
+
// Project texts (.md контент). Отдельная таблица на сервере; клиент
|
|
719
|
+
// ссылается на записи по id из manifest'а board.texts[relPath] = textId.
|
|
720
|
+
if (req.method === 'POST' && url.pathname === '/api/project-texts') return handleProjectTextCreate(req, res);
|
|
721
|
+
{
|
|
722
|
+
const m = url.pathname.match(/^\/api\/project-texts\/([^/]+)$/);
|
|
723
|
+
if (m) {
|
|
724
|
+
if (req.method === 'GET') return handleProjectTextGet(res, decodeURIComponent(m[1]));
|
|
725
|
+
if (req.method === 'POST') return handleProjectTextUpdate(req, res, decodeURIComponent(m[1]));
|
|
726
|
+
if (req.method === 'DELETE') return handleProjectTextDelete(res, decodeURIComponent(m[1]));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
689
729
|
if (req.method === 'GET') return serveStatic(res, url);
|
|
690
730
|
send(res, 404, 'not found');
|
|
691
731
|
} catch (e) {
|