ima2-gen 1.0.7 → 1.0.9
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/README.md +1 -1
- package/config.js +2 -2
- package/lib/assetLifecycle.js +5 -3
- package/lib/historyList.js +1 -0
- package/lib/nodeStore.js +16 -14
- package/lib/oauthProxy.js +43 -3
- package/lib/storageMigration.js +91 -0
- package/package.json +1 -1
- package/routes/nodes.js +89 -15
- package/routes/sessions.js +3 -1
- package/server.js +2 -0
- package/ui/dist/assets/{index-Jcs5Q1sj.css → index-CBrmEeD7.css} +1 -1
- package/ui/dist/assets/index-DRST1V_0.js +22 -0
- package/ui/dist/assets/index-DRST1V_0.js.map +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-D1MUxZaB.js +0 -16
- package/ui/dist/assets/index-D1MUxZaB.js.map +0 -1
package/README.md
CHANGED
|
@@ -214,7 +214,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
214
214
|
| `IMA2_OAUTH_PROXY_PORT` / `OAUTH_PORT` | `10531` | OAuth proxy port |
|
|
215
215
|
| `IMA2_SERVER` | — | CLI target override |
|
|
216
216
|
| `IMA2_CONFIG_DIR` | `~/.ima2` | Config and SQLite location |
|
|
217
|
-
| `IMA2_GENERATED_DIR` | `
|
|
217
|
+
| `IMA2_GENERATED_DIR` | `~/.ima2/generated` | Generated image directory |
|
|
218
218
|
| `IMA2_NO_OAUTH_PROXY` | — | Set `1` to disable auto-starting the OAuth proxy |
|
|
219
219
|
| `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | Retention for opt-in terminal in-flight debug jobs |
|
|
220
220
|
| `VITE_IMA2_NODE_MODE` | enabled | Set `0` at UI build time to hide node mode |
|
package/config.js
CHANGED
|
@@ -108,8 +108,8 @@ export const config = {
|
|
|
108
108
|
storage: {
|
|
109
109
|
configDir,
|
|
110
110
|
packageRoot,
|
|
111
|
-
generatedDir: pickStr(env.IMA2_GENERATED_DIR, fileCfg.storage?.generatedDir, join(
|
|
112
|
-
trashDir: pickStr(env.IMA2_TRASH_DIR, fileCfg.storage?.trashDir, join(
|
|
111
|
+
generatedDir: pickStr(env.IMA2_GENERATED_DIR, fileCfg.storage?.generatedDir, join(configDir, "generated")),
|
|
112
|
+
trashDir: pickStr(env.IMA2_TRASH_DIR, fileCfg.storage?.trashDir, join(configDir, "generated", ".trash")),
|
|
113
113
|
generatedDirName: pickStr(env.IMA2_GENERATED_DIRNAME, fileCfg.storage?.generatedDirName, "generated"),
|
|
114
114
|
trashDirName: pickStr(env.IMA2_TRASH_DIRNAME, fileCfg.storage?.trashDirName, ".trash"),
|
|
115
115
|
dbPath: pickStr(env.IMA2_DB_PATH, fileCfg.storage?.dbPath, join(configDir, "sessions.db")),
|
package/lib/assetLifecycle.js
CHANGED
|
@@ -8,6 +8,7 @@ const TRASH = config.storage.trashDirName;
|
|
|
8
8
|
const TRASH_TTL_MS = config.trash.ttlMs;
|
|
9
9
|
|
|
10
10
|
function resolveInGenerated(rootDir, relPath) {
|
|
11
|
+
void rootDir;
|
|
11
12
|
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
12
13
|
const err = new Error("filename required");
|
|
13
14
|
err.status = 400;
|
|
@@ -20,7 +21,7 @@ function resolveInGenerated(rootDir, relPath) {
|
|
|
20
21
|
err.code = "INVALID_FILENAME";
|
|
21
22
|
throw err;
|
|
22
23
|
}
|
|
23
|
-
const baseDir = resolve(
|
|
24
|
+
const baseDir = resolve(config.storage.generatedDir);
|
|
24
25
|
const target = resolve(baseDir, relPath);
|
|
25
26
|
if (target !== baseDir && !target.startsWith(baseDir + sep)) {
|
|
26
27
|
const err = new Error("filename escapes generated/");
|
|
@@ -78,7 +79,7 @@ export async function trashAsset(rootDir, filename) {
|
|
|
78
79
|
err.code = "ASSET_NOT_FOUND";
|
|
79
80
|
throw err;
|
|
80
81
|
}
|
|
81
|
-
const trashDir = resolve(
|
|
82
|
+
const trashDir = resolve(config.storage.trashDir);
|
|
82
83
|
await mkdir(trashDir, { recursive: true });
|
|
83
84
|
// Flatten filename (subdir separators -> __) so trash is flat & easy to restore
|
|
84
85
|
const flat = filename.replace(/[\\/]+/g, "__");
|
|
@@ -107,7 +108,8 @@ export async function trashAsset(rootDir, filename) {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
export async function restoreAsset(rootDir, trashId, originalFilename) {
|
|
110
|
-
|
|
111
|
+
void rootDir;
|
|
112
|
+
const trashDir = resolve(config.storage.trashDir);
|
|
111
113
|
const src = resolve(trashDir, trashId);
|
|
112
114
|
if (!src.startsWith(trashDir + sep) && src !== trashDir) {
|
|
113
115
|
const err = new Error("invalid trashId");
|
package/lib/historyList.js
CHANGED
|
@@ -52,6 +52,7 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
52
52
|
nodeId: meta?.nodeId || null,
|
|
53
53
|
parentNodeId: meta?.parentNodeId || null,
|
|
54
54
|
clientNodeId: meta?.clientNodeId || null,
|
|
55
|
+
requestId: meta?.requestId || null,
|
|
55
56
|
kind: meta?.kind || null,
|
|
56
57
|
refsCount: Number.isFinite(meta?.refsCount) ? meta.refsCount : 0,
|
|
57
58
|
};
|
package/lib/nodeStore.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { writeFile, readFile, access } from "fs/promises";
|
|
1
|
+
import { writeFile, readFile, access, mkdir } from "fs/promises";
|
|
2
2
|
import { join, resolve, sep } from "path";
|
|
3
3
|
import { randomBytes } from "crypto";
|
|
4
4
|
import { config } from "../config.js";
|
|
5
5
|
|
|
6
|
-
const DIR = config.storage.generatedDirName;
|
|
7
|
-
|
|
8
6
|
export function newNodeId() {
|
|
9
7
|
return "n_" + randomBytes(config.ids.nodeHexBytes).toString("hex");
|
|
10
8
|
}
|
|
11
9
|
|
|
12
|
-
export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png" }) {
|
|
10
|
+
export async function saveNode(rootDir, { nodeId, b64, meta, ext = "png", generatedDir = config.storage.generatedDir }) {
|
|
11
|
+
void rootDir;
|
|
13
12
|
const filename = `${nodeId}.${ext}`;
|
|
14
|
-
await
|
|
15
|
-
await writeFile(join(
|
|
13
|
+
await mkdir(generatedDir, { recursive: true });
|
|
14
|
+
await writeFile(join(generatedDir, filename), Buffer.from(b64, "base64"));
|
|
15
|
+
await writeFile(join(generatedDir, filename + ".json"), JSON.stringify(meta, null, 2));
|
|
16
16
|
return { filename };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function loadNodeB64(rootDir, filename) {
|
|
20
|
-
const p = resolveGeneratedPath(rootDir, filename);
|
|
19
|
+
export async function loadNodeB64(rootDir, filename, generatedDir = config.storage.generatedDir) {
|
|
20
|
+
const p = resolveGeneratedPath(rootDir, filename, generatedDir);
|
|
21
21
|
try { await access(p); } catch {
|
|
22
22
|
const err = new Error(`Node file not found: ${filename}`);
|
|
23
23
|
err.code = "NODE_NOT_FOUND";
|
|
@@ -28,16 +28,17 @@ export async function loadNodeB64(rootDir, filename) {
|
|
|
28
28
|
return buf.toString("base64");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export async function loadNodeMeta(rootDir, nodeId, ext = "png") {
|
|
31
|
+
export async function loadNodeMeta(rootDir, nodeId, ext = "png", generatedDir = config.storage.generatedDir) {
|
|
32
|
+
void rootDir;
|
|
32
33
|
try {
|
|
33
|
-
return JSON.parse(await readFile(join(
|
|
34
|
+
return JSON.parse(await readFile(join(generatedDir, `${nodeId}.${ext}.json`), "utf-8"));
|
|
34
35
|
} catch {
|
|
35
36
|
return null;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export async function loadAssetB64(rootDir, externalSrc) {
|
|
40
|
-
const p = resolveGeneratedPath(rootDir, externalSrc);
|
|
40
|
+
export async function loadAssetB64(rootDir, externalSrc, generatedDir = config.storage.generatedDir) {
|
|
41
|
+
const p = resolveGeneratedPath(rootDir, externalSrc, generatedDir);
|
|
41
42
|
try { await access(p); } catch {
|
|
42
43
|
const err = new Error(`Asset file not found: ${externalSrc}`);
|
|
43
44
|
err.code = "NODE_NOT_FOUND";
|
|
@@ -48,14 +49,15 @@ export async function loadAssetB64(rootDir, externalSrc) {
|
|
|
48
49
|
return buf.toString("base64");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function resolveGeneratedPath(rootDir, relPath) {
|
|
52
|
+
function resolveGeneratedPath(rootDir, relPath, generatedDir = config.storage.generatedDir) {
|
|
53
|
+
void rootDir;
|
|
52
54
|
if (typeof relPath !== "string" || relPath.length === 0) {
|
|
53
55
|
const err = new Error("Asset path is required");
|
|
54
56
|
err.code = "NODE_SOURCE_INVALID";
|
|
55
57
|
err.status = 400;
|
|
56
58
|
throw err;
|
|
57
59
|
}
|
|
58
|
-
const baseDir = resolve(
|
|
60
|
+
const baseDir = resolve(generatedDir);
|
|
59
61
|
const target = resolve(baseDir, relPath);
|
|
60
62
|
if (target !== baseDir && !target.startsWith(baseDir + sep)) {
|
|
61
63
|
const err = new Error(`Asset path escapes generated/: ${relPath}`);
|
package/lib/oauthProxy.js
CHANGED
|
@@ -46,6 +46,24 @@ function extractSseData(block) {
|
|
|
46
46
|
return eventData;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function extractPartialImage(data) {
|
|
50
|
+
if (typeof data?.type !== "string" || !data.type.includes("partial")) return null;
|
|
51
|
+
const item = data.item || {};
|
|
52
|
+
const b64 =
|
|
53
|
+
data.partial_image ||
|
|
54
|
+
data.image ||
|
|
55
|
+
data.result ||
|
|
56
|
+
item.partial_image ||
|
|
57
|
+
item.image ||
|
|
58
|
+
item.result;
|
|
59
|
+
if (typeof b64 !== "string" || b64.length === 0) return null;
|
|
60
|
+
const index =
|
|
61
|
+
Number.isFinite(data.index) ? data.index :
|
|
62
|
+
Number.isFinite(item.index) ? item.index :
|
|
63
|
+
null;
|
|
64
|
+
return { b64, index, eventType: data.type };
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstreamBodyChars, eventType } = {}) {
|
|
50
68
|
const err = new Error(message);
|
|
51
69
|
err.code = code;
|
|
@@ -55,7 +73,7 @@ function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstre
|
|
|
55
73
|
return err;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
|
-
async function readImageStream(res, { requestId = null, scope = "oauth" } = {}) {
|
|
76
|
+
async function readImageStream(res, { requestId = null, scope = "oauth", onPartialImage = null } = {}) {
|
|
59
77
|
const reader = res.body.getReader();
|
|
60
78
|
const decoder = new TextDecoder();
|
|
61
79
|
let buffer = "";
|
|
@@ -81,6 +99,17 @@ async function readImageStream(res, { requestId = null, scope = "oauth" } = {})
|
|
|
81
99
|
const data = JSON.parse(eventData);
|
|
82
100
|
eventCount++;
|
|
83
101
|
|
|
102
|
+
const partial = extractPartialImage(data);
|
|
103
|
+
if (partial) {
|
|
104
|
+
logEvent(scope, "partial", {
|
|
105
|
+
requestId,
|
|
106
|
+
index: partial.index,
|
|
107
|
+
imageChars: partial.b64.length,
|
|
108
|
+
eventType: partial.eventType,
|
|
109
|
+
});
|
|
110
|
+
if (requestId) setJobPhase(requestId, "partial");
|
|
111
|
+
if (typeof onPartialImage === "function") onPartialImage(partial);
|
|
112
|
+
}
|
|
84
113
|
if (data.type === "response.output_item.done" && data.item?.type === "image_generation_call") {
|
|
85
114
|
if (data.item.result) {
|
|
86
115
|
imageB64 = data.item.result;
|
|
@@ -123,11 +152,18 @@ export async function generateViaOAuth(
|
|
|
123
152
|
requestId = null,
|
|
124
153
|
mode = "auto",
|
|
125
154
|
ctx = {},
|
|
155
|
+
options = {},
|
|
126
156
|
) {
|
|
127
157
|
const oauthUrl = getOAuthUrl(ctx);
|
|
128
158
|
const tools = [
|
|
129
159
|
{ type: "web_search" },
|
|
130
|
-
{
|
|
160
|
+
{
|
|
161
|
+
type: "image_generation",
|
|
162
|
+
quality,
|
|
163
|
+
size,
|
|
164
|
+
moderation,
|
|
165
|
+
...(options.partialImages ? { partial_images: options.partialImages } : {}),
|
|
166
|
+
},
|
|
131
167
|
];
|
|
132
168
|
|
|
133
169
|
const textPrompt = buildUserTextPrompt(prompt, mode);
|
|
@@ -187,7 +223,11 @@ export async function generateViaOAuth(
|
|
|
187
223
|
throw new Error("No image data in response (non-stream mode)");
|
|
188
224
|
}
|
|
189
225
|
|
|
190
|
-
const { imageB64, usage, webSearchCalls, revisedPrompt, eventCount } = await readImageStream(res, {
|
|
226
|
+
const { imageB64, usage, webSearchCalls, revisedPrompt, eventCount } = await readImageStream(res, {
|
|
227
|
+
requestId,
|
|
228
|
+
scope: "oauth",
|
|
229
|
+
onPartialImage: options.onPartialImage,
|
|
230
|
+
});
|
|
191
231
|
logEvent("oauth", "stream_end", { requestId, events: eventCount, hasImage: !!imageB64 });
|
|
192
232
|
|
|
193
233
|
if (!imageB64) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readdir, copyFile, stat } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_NAME = "ima2-gen";
|
|
7
|
+
|
|
8
|
+
function addStats(a, b) {
|
|
9
|
+
return {
|
|
10
|
+
copied: a.copied + b.copied,
|
|
11
|
+
skippedExisting: a.skippedExisting + b.skippedExisting,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function copyMissingTree(srcDir, dstDir) {
|
|
16
|
+
await mkdir(dstDir, { recursive: true });
|
|
17
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
18
|
+
let stats = { copied: 0, skippedExisting: 0 };
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const src = join(srcDir, entry.name);
|
|
21
|
+
const dst = join(dstDir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
stats = addStats(stats, await copyMissingTree(src, dst));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isFile()) continue;
|
|
27
|
+
if (existsSync(dst)) {
|
|
28
|
+
stats.skippedExisting += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
await copyFile(src, dst);
|
|
32
|
+
stats.copied += 1;
|
|
33
|
+
}
|
|
34
|
+
return stats;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSameOrInside(child, parent) {
|
|
38
|
+
const a = resolve(child);
|
|
39
|
+
const b = resolve(parent);
|
|
40
|
+
return a === b || a.startsWith(b + sep);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function migrateGeneratedStorage(ctx, options = {}) {
|
|
44
|
+
const targetDir = ctx.config.storage.generatedDir;
|
|
45
|
+
const candidates = options.legacyDirs || getLegacyGeneratedCandidates(ctx, options.env);
|
|
46
|
+
const result = {
|
|
47
|
+
copied: 0,
|
|
48
|
+
skippedExisting: 0,
|
|
49
|
+
sourcesScanned: 0,
|
|
50
|
+
sourcesSkipped: 0,
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
for (const legacyDir of candidates) {
|
|
54
|
+
if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
|
|
55
|
+
result.sourcesSkipped += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const legacyStat = await stat(legacyDir);
|
|
60
|
+
if (!legacyStat.isDirectory()) continue;
|
|
61
|
+
result.sourcesScanned += 1;
|
|
62
|
+
const copyStats = await copyMissingTree(legacyDir, targetDir);
|
|
63
|
+
result.copied += copyStats.copied;
|
|
64
|
+
result.skippedExisting += copyStats.skippedExisting;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err?.code !== "ENOENT") {
|
|
67
|
+
console.warn("[storage] generated asset migration source skipped:", legacyDir, err.message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn("[storage] generated asset migration skipped:", err.message);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getLegacyGeneratedCandidates(ctx, env = process.env) {
|
|
79
|
+
const home = homedir();
|
|
80
|
+
const nodePrefix = dirname(dirname(process.execPath));
|
|
81
|
+
const npmPrefix = env.npm_config_prefix || nodePrefix;
|
|
82
|
+
const appData = env.APPDATA || join(home, "AppData", "Roaming");
|
|
83
|
+
return Array.from(new Set([
|
|
84
|
+
join(ctx.rootDir, "generated"),
|
|
85
|
+
join(npmPrefix, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
86
|
+
join(npmPrefix, "node_modules", PACKAGE_NAME, "generated"),
|
|
87
|
+
join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
|
|
88
|
+
join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
89
|
+
join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
90
|
+
].map((p) => resolve(p))));
|
|
91
|
+
}
|
package/package.json
CHANGED
package/routes/nodes.js
CHANGED
|
@@ -22,9 +22,40 @@ function validateModeration(ctx, moderation) {
|
|
|
22
22
|
return { moderation };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function wantsSse(req) {
|
|
26
|
+
const accept = typeof req.headers.accept === "string" ? req.headers.accept : "";
|
|
27
|
+
return accept.includes("text/event-stream");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeSse(res, event, data) {
|
|
31
|
+
res.write(`event: ${event}\n`);
|
|
32
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeNodeError(res, status, code, message, parentNodeId) {
|
|
36
|
+
if (res.headersSent) {
|
|
37
|
+
writeSse(res, "error", {
|
|
38
|
+
error: { code, message },
|
|
39
|
+
parentNodeId,
|
|
40
|
+
status,
|
|
41
|
+
});
|
|
42
|
+
res.end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.status(status).json({
|
|
46
|
+
error: { code, message },
|
|
47
|
+
parentNodeId,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function dataUrlFromB64(format, b64) {
|
|
52
|
+
return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
export function registerNodeRoutes(app, ctx) {
|
|
26
56
|
app.post("/api/node/generate", async (req, res) => {
|
|
27
57
|
const body = req.body || {};
|
|
58
|
+
const streamResponse = wantsSse(req);
|
|
28
59
|
const parentNodeId = body.parentNodeId ?? null;
|
|
29
60
|
const requestId = typeof body.requestId === "string" ? body.requestId : null;
|
|
30
61
|
const sessionId = typeof body.sessionId === "string" ? body.sessionId : null;
|
|
@@ -125,9 +156,9 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
125
156
|
const startTime = Date.now();
|
|
126
157
|
let parentB64 = null;
|
|
127
158
|
if (parentNodeId) {
|
|
128
|
-
parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png
|
|
159
|
+
parentB64 = await loadNodeB64(ctx.rootDir, `${parentNodeId}.png`, ctx.config.storage.generatedDir);
|
|
129
160
|
} else if (typeof externalSrc === "string" && externalSrc.length > 0) {
|
|
130
|
-
parentB64 = await loadAssetB64(ctx.rootDir, externalSrc);
|
|
161
|
+
parentB64 = await loadAssetB64(ctx.rootDir, externalSrc, ctx.config.storage.generatedDir);
|
|
131
162
|
}
|
|
132
163
|
logEvent("node", "request", {
|
|
133
164
|
requestId,
|
|
@@ -144,6 +175,15 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
144
175
|
styleSheetApplied: !!styleSheetApplied,
|
|
145
176
|
});
|
|
146
177
|
|
|
178
|
+
if (streamResponse) {
|
|
179
|
+
res.writeHead(200, {
|
|
180
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
181
|
+
"Cache-Control": "no-cache, no-transform",
|
|
182
|
+
Connection: "keep-alive",
|
|
183
|
+
});
|
|
184
|
+
writeSse(res, "phase", { requestId, phase: "streaming" });
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
let b64, usage, webSearchCalls = 0, revisedPrompt = null;
|
|
148
188
|
const MAX_RETRIES = 1;
|
|
149
189
|
let lastErr;
|
|
@@ -151,7 +191,27 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
151
191
|
try {
|
|
152
192
|
const r = parentB64
|
|
153
193
|
? await editViaOAuth(effectivePrompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId)
|
|
154
|
-
: await generateViaOAuth(
|
|
194
|
+
: await generateViaOAuth(
|
|
195
|
+
effectivePrompt,
|
|
196
|
+
quality,
|
|
197
|
+
size,
|
|
198
|
+
moderation,
|
|
199
|
+
refCheck.refs,
|
|
200
|
+
requestId,
|
|
201
|
+
normalizedPromptMode,
|
|
202
|
+
ctx,
|
|
203
|
+
{
|
|
204
|
+
partialImages: streamResponse ? 2 : 0,
|
|
205
|
+
onPartialImage: streamResponse
|
|
206
|
+
? (partial) =>
|
|
207
|
+
writeSse(res, "partial", {
|
|
208
|
+
requestId,
|
|
209
|
+
image: dataUrlFromB64(format, partial.b64),
|
|
210
|
+
index: partial.index,
|
|
211
|
+
})
|
|
212
|
+
: null,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
155
215
|
if (r.b64) {
|
|
156
216
|
b64 = r.b64;
|
|
157
217
|
usage = r.usage;
|
|
@@ -172,10 +232,13 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
172
232
|
finishStatus = "error";
|
|
173
233
|
finishHttpStatus = 422;
|
|
174
234
|
finishErrorCode = "SAFETY_REFUSAL";
|
|
175
|
-
return
|
|
176
|
-
|
|
235
|
+
return writeNodeError(
|
|
236
|
+
res,
|
|
237
|
+
422,
|
|
238
|
+
"SAFETY_REFUSAL",
|
|
239
|
+
lastErr?.message || "Empty response after retry",
|
|
177
240
|
parentNodeId,
|
|
178
|
-
|
|
241
|
+
);
|
|
179
242
|
}
|
|
180
243
|
|
|
181
244
|
const nodeId = newNodeId();
|
|
@@ -199,6 +262,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
199
262
|
webSearchCalls,
|
|
200
263
|
provider: "oauth",
|
|
201
264
|
kind: parentB64 ? "edit" : "generate",
|
|
265
|
+
requestId,
|
|
202
266
|
refsCount: refCheck.refs.length,
|
|
203
267
|
quality,
|
|
204
268
|
size,
|
|
@@ -206,7 +270,13 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
206
270
|
moderation,
|
|
207
271
|
};
|
|
208
272
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
209
|
-
const { filename } = await saveNode(ctx.rootDir, {
|
|
273
|
+
const { filename } = await saveNode(ctx.rootDir, {
|
|
274
|
+
nodeId,
|
|
275
|
+
b64,
|
|
276
|
+
meta,
|
|
277
|
+
ext: format,
|
|
278
|
+
generatedDir: ctx.config.storage.generatedDir,
|
|
279
|
+
});
|
|
210
280
|
finishMeta = { nodeId, filename, imageChars: b64.length };
|
|
211
281
|
finishHttpStatus = 200;
|
|
212
282
|
logEvent("node", "saved", {
|
|
@@ -217,11 +287,11 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
217
287
|
elapsedMs: Date.now() - startTime,
|
|
218
288
|
});
|
|
219
289
|
|
|
220
|
-
|
|
290
|
+
const payload = {
|
|
221
291
|
nodeId,
|
|
222
292
|
parentNodeId,
|
|
223
293
|
requestId,
|
|
224
|
-
image:
|
|
294
|
+
image: dataUrlFromB64(format, b64),
|
|
225
295
|
filename,
|
|
226
296
|
url: `/generated/${filename}`,
|
|
227
297
|
elapsed,
|
|
@@ -233,17 +303,21 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
233
303
|
warnings: qualityWarnings,
|
|
234
304
|
revisedPrompt,
|
|
235
305
|
promptMode: normalizedPromptMode,
|
|
236
|
-
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (streamResponse) {
|
|
309
|
+
writeSse(res, "done", payload);
|
|
310
|
+
res.end();
|
|
311
|
+
} else {
|
|
312
|
+
res.json(payload);
|
|
313
|
+
}
|
|
237
314
|
} catch (err) {
|
|
238
315
|
const code = err.code || classifyUpstreamError(err.message) || "NODE_GEN_FAILED";
|
|
239
316
|
finishStatus = "error";
|
|
240
317
|
finishHttpStatus = err.status || 500;
|
|
241
318
|
finishErrorCode = code;
|
|
242
319
|
logError("node", "error", err, { requestId, code, parentNodeId, sessionId, clientNodeId });
|
|
243
|
-
res
|
|
244
|
-
error: { code, message: err.message },
|
|
245
|
-
parentNodeId,
|
|
246
|
-
});
|
|
320
|
+
writeNodeError(res, err.status || 500, code, err.message, parentNodeId);
|
|
247
321
|
} finally {
|
|
248
322
|
finishJob(requestId, {
|
|
249
323
|
status: finishStatus,
|
|
@@ -257,7 +331,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
257
331
|
app.get("/api/node/:nodeId", async (req, res) => {
|
|
258
332
|
try {
|
|
259
333
|
const { nodeId } = req.params;
|
|
260
|
-
const meta = await loadNodeMeta(ctx.rootDir, nodeId);
|
|
334
|
+
const meta = await loadNodeMeta(ctx.rootDir, nodeId, "png", ctx.config.storage.generatedDir);
|
|
261
335
|
if (!meta) {
|
|
262
336
|
return res.status(404).json({ error: { code: "NODE_NOT_FOUND", message: "Node metadata missing" } });
|
|
263
337
|
}
|
package/routes/sessions.js
CHANGED
|
@@ -266,7 +266,9 @@ export function registerSessionRoutes(app, ctx) {
|
|
|
266
266
|
const code = err.code || "DB_ERROR";
|
|
267
267
|
const payload = { error: { code, message: err.message } };
|
|
268
268
|
if (typeof err.currentVersion === "number") payload.currentVersion = err.currentVersion;
|
|
269
|
-
|
|
269
|
+
if (code !== "GRAPH_VERSION_CONFLICT") {
|
|
270
|
+
logError("session", "graph_error", err, { sessionId: req.params.id, code });
|
|
271
|
+
}
|
|
270
272
|
res.status(err.status || 500).json(payload);
|
|
271
273
|
}
|
|
272
274
|
});
|
package/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath, pathToFileURL } from "url";
|
|
|
13
13
|
import { onShutdown } from "./bin/lib/platform.js";
|
|
14
14
|
import { ensureDefaultSession } from "./lib/sessionStore.js";
|
|
15
15
|
import { startOAuthProxy } from "./lib/oauthLauncher.js";
|
|
16
|
+
import { migrateGeneratedStorage } from "./lib/storageMigration.js";
|
|
16
17
|
import { configureRoutes } from "./routes/index.js";
|
|
17
18
|
import { config } from "./config.js";
|
|
18
19
|
|
|
@@ -114,6 +115,7 @@ export async function createRuntimeContext(overrides = {}) {
|
|
|
114
115
|
|
|
115
116
|
export async function startServer(overrides = {}) {
|
|
116
117
|
const ctx = await createRuntimeContext(overrides);
|
|
118
|
+
await migrateGeneratedStorage(ctx);
|
|
117
119
|
const app = buildApp(ctx);
|
|
118
120
|
const oauthChild =
|
|
119
121
|
overrides.oauthChild !== undefined
|