ima2-gen 1.1.0 → 1.1.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/README.md +47 -7
- package/assets/card-news/templates/academy-lesson-square/base.png +0 -0
- package/assets/card-news/templates/academy-lesson-square/preview.png +0 -0
- package/assets/card-news/templates/academy-lesson-square/template.json +20 -0
- package/assets/card-news/templates/clean-report-square/base.png +0 -0
- package/assets/card-news/templates/clean-report-square/preview.png +0 -0
- package/assets/card-news/templates/clean-report-square/template.json +20 -0
- package/bin/commands/cancel.js +45 -0
- package/bin/commands/edit.js +33 -4
- package/bin/commands/gen.js +26 -3
- package/bin/commands/ps.js +48 -16
- package/bin/ima2.js +56 -12
- package/bin/lib/client.js +4 -1
- package/bin/lib/error-hints.js +23 -0
- package/bin/lib/output.js +10 -0
- package/config.js +19 -1
- package/docs/API.md +67 -0
- package/docs/FAQ.ko.md +248 -0
- package/docs/FAQ.md +256 -0
- package/docs/README.ja.md +4 -0
- package/docs/README.ko.md +14 -1
- package/docs/README.zh-CN.md +4 -0
- package/docs/RECOVER_OLD_IMAGES.md +2 -0
- package/lib/cardNewsGenerator.js +162 -0
- package/lib/cardNewsJobStore.js +107 -0
- package/lib/cardNewsManifestStore.js +112 -0
- package/lib/cardNewsPlanner.js +180 -0
- package/lib/cardNewsPlannerClient.js +112 -0
- package/lib/cardNewsPlannerPrompt.js +60 -0
- package/lib/cardNewsPlannerSchema.js +259 -0
- package/lib/cardNewsRoleTemplateStore.js +47 -0
- package/lib/cardNewsTemplateStore.js +210 -0
- package/lib/db.js +20 -3
- package/lib/errorClassify.js +2 -2
- package/lib/generationErrors.js +51 -0
- package/lib/historyList.js +82 -8
- package/lib/inflight.js +117 -34
- package/lib/logger.js +37 -3
- package/lib/oauthLauncher.js +52 -19
- package/lib/oauthProxy.js +81 -14
- package/lib/requestLogger.js +48 -0
- package/lib/runtimePorts.js +93 -0
- package/lib/sessionStore.js +48 -7
- package/package.json +3 -2
- package/routes/cardNews.js +183 -0
- package/routes/edit.js +1 -1
- package/routes/generate.js +10 -10
- package/routes/health.js +27 -3
- package/routes/index.js +2 -0
- package/routes/nodes.js +93 -26
- package/server.js +91 -18
- package/ui/dist/assets/index-BjX_nzuK.js +23 -0
- package/ui/dist/assets/index-BjX_nzuK.js.map +1 -0
- package/ui/dist/assets/index-DHyUax4_.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CqpVoXpZ.css +0 -1
- package/ui/dist/assets/index-IHSd1z1a.js +0 -22
- package/ui/dist/assets/index-IHSd1z1a.js.map +0 -1
package/lib/historyList.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, stat } from "fs/promises";
|
|
2
|
-
import { join } from "path";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
3
|
import { config } from "../config.js";
|
|
4
4
|
|
|
5
5
|
async function listImageFiles(baseDir) {
|
|
@@ -25,15 +25,10 @@ async function listImageFiles(baseDir) {
|
|
|
25
25
|
export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
26
26
|
await mkdir(baseDir, { recursive: true });
|
|
27
27
|
const imgs = await listImageFiles(baseDir);
|
|
28
|
+
const setRows = await listCardNewsSetRows(baseDir);
|
|
28
29
|
const rows = await Promise.all(imgs.map(async ({ full, rel, name }) => {
|
|
29
30
|
const st = await stat(full).catch(() => null);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const raw = await readFile(full + ".json", "utf-8");
|
|
33
|
-
meta = JSON.parse(raw);
|
|
34
|
-
} catch (e) {
|
|
35
|
-
if (e.code !== "ENOENT") console.warn("[history] sidecar parse fail:", rel, e.message);
|
|
36
|
-
}
|
|
31
|
+
const meta = await readImageSidecar(full, rel);
|
|
37
32
|
return {
|
|
38
33
|
filename: rel,
|
|
39
34
|
url: `/generated/${rel.split("/").map(encodeURIComponent).join("/")}`,
|
|
@@ -55,10 +50,17 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
55
50
|
clientNodeId: meta?.clientNodeId || null,
|
|
56
51
|
requestId: meta?.requestId || null,
|
|
57
52
|
kind: meta?.kind || null,
|
|
53
|
+
setId: meta?.setId || null,
|
|
54
|
+
cardId: meta?.cardId || null,
|
|
55
|
+
cardOrder: Number.isFinite(meta?.cardOrder) ? meta.cardOrder : null,
|
|
56
|
+
headline: meta?.headline || null,
|
|
57
|
+
body: meta?.body || null,
|
|
58
|
+
cards: meta?.cards || null,
|
|
58
59
|
refsCount: Number.isFinite(meta?.refsCount) ? meta.refsCount : 0,
|
|
59
60
|
};
|
|
60
61
|
}));
|
|
61
62
|
|
|
63
|
+
rows.push(...setRows);
|
|
62
64
|
rows.sort((a, b) => {
|
|
63
65
|
if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
|
|
64
66
|
return b.filename < a.filename ? -1 : b.filename > a.filename ? 1 : 0;
|
|
@@ -66,3 +68,75 @@ export async function listHistoryRows(baseDir = config.storage.generatedDir) {
|
|
|
66
68
|
|
|
67
69
|
return rows;
|
|
68
70
|
}
|
|
71
|
+
|
|
72
|
+
async function readImageSidecar(full, rel) {
|
|
73
|
+
const sibling = full.replace(/\.(png|jpe?g|webp)$/i, ".json");
|
|
74
|
+
for (const candidate of [`${full}.json`, sibling]) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(await readFile(candidate, "utf-8"));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code !== "ENOENT") console.warn("[history] sidecar parse fail:", rel, e.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function listCardNewsSetRows(baseDir) {
|
|
85
|
+
const root = join(baseDir, "cardnews");
|
|
86
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
87
|
+
const rows = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory()) continue;
|
|
90
|
+
try {
|
|
91
|
+
const manifestPath = join(root, entry.name, "manifest.json");
|
|
92
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
|
|
93
|
+
const first = (manifest.cards || []).find((card) => card.imageFilename);
|
|
94
|
+
const filename = `cardnews/${entry.name}/manifest.json`;
|
|
95
|
+
rows.push({
|
|
96
|
+
filename,
|
|
97
|
+
url: first?.imageFilename
|
|
98
|
+
? `/generated/cardnews/${encodeURIComponent(entry.name)}/${encodeURIComponent(first.imageFilename)}`
|
|
99
|
+
: "",
|
|
100
|
+
createdAt: manifest.createdAt || 0,
|
|
101
|
+
prompt: null,
|
|
102
|
+
userPrompt: null,
|
|
103
|
+
revisedPrompt: null,
|
|
104
|
+
promptMode: null,
|
|
105
|
+
quality: null,
|
|
106
|
+
size: manifest.size || null,
|
|
107
|
+
format: "card-news-set",
|
|
108
|
+
model: null,
|
|
109
|
+
provider: "oauth",
|
|
110
|
+
usage: null,
|
|
111
|
+
webSearchCalls: 0,
|
|
112
|
+
sessionId: manifest.sessionId || null,
|
|
113
|
+
nodeId: null,
|
|
114
|
+
parentNodeId: null,
|
|
115
|
+
clientNodeId: null,
|
|
116
|
+
requestId: manifest.requestId || null,
|
|
117
|
+
kind: "card-news-set",
|
|
118
|
+
setId: manifest.setId || entry.name,
|
|
119
|
+
cardId: null,
|
|
120
|
+
cardOrder: null,
|
|
121
|
+
title: manifest.title || "Untitled card news",
|
|
122
|
+
headline: manifest.title || "Untitled card news",
|
|
123
|
+
body: null,
|
|
124
|
+
cards: (manifest.cards || []).map((card) => ({
|
|
125
|
+
url: card.imageFilename
|
|
126
|
+
? `/generated/cardnews/${encodeURIComponent(entry.name)}/${encodeURIComponent(card.imageFilename)}`
|
|
127
|
+
: "",
|
|
128
|
+
headline: card.headline,
|
|
129
|
+
body: card.body,
|
|
130
|
+
cardOrder: card.cardOrder,
|
|
131
|
+
imageFilename: card.imageFilename,
|
|
132
|
+
status: card.status || "generated",
|
|
133
|
+
})),
|
|
134
|
+
refsCount: 0,
|
|
135
|
+
dir: dirname(manifestPath),
|
|
136
|
+
});
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (e.code !== "ENOENT") console.warn("[history] card-news manifest parse fail:", entry.name, e.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return rows;
|
|
142
|
+
}
|
package/lib/inflight.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
|
+
import { getDb } from "./db.js";
|
|
2
3
|
import { logEvent } from "./logger.js";
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
+
// SQLite-backed inflight job registry.
|
|
5
6
|
// Tracks generation requests that are currently running on the server so clients
|
|
6
7
|
// can reconcile optimistic UI state after a reload or across tabs.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
9
|
+
// A restarted process cannot continue the original upstream fetch, but keeping
|
|
10
|
+
// metadata durable lets the UI reconcile requestIds and eventually prune stale
|
|
11
|
+
// work without losing the recovery breadcrumb.
|
|
10
12
|
|
|
11
|
-
const jobs = new Map(); // requestId -> { requestId, kind, prompt, meta, startedAt, phase, phaseAt }
|
|
12
13
|
const terminalJobs = new Map(); // requestId -> terminal snapshot, active-only API stays default
|
|
13
14
|
|
|
14
15
|
// Phases: "queued" → "streaming" (upstream connection open, waiting for image)
|
|
@@ -16,38 +17,60 @@ const terminalJobs = new Map(); // requestId -> terminal snapshot, active-only A
|
|
|
16
17
|
export function startJob({ requestId, kind, prompt, meta = {} }) {
|
|
17
18
|
if (!requestId) return;
|
|
18
19
|
const startedAt = Date.now();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
const normalizedPrompt = typeof prompt === "string" ? prompt.slice(0, 500) : "";
|
|
21
|
+
const normalizedMeta = normalizeMeta(meta);
|
|
22
|
+
getDb()
|
|
23
|
+
.prepare(`
|
|
24
|
+
INSERT OR REPLACE INTO inflight (
|
|
25
|
+
request_id,
|
|
26
|
+
kind,
|
|
27
|
+
prompt,
|
|
28
|
+
meta,
|
|
29
|
+
session_id,
|
|
30
|
+
parent_node_id,
|
|
31
|
+
client_node_id,
|
|
32
|
+
started_at,
|
|
33
|
+
phase,
|
|
34
|
+
phase_at
|
|
35
|
+
)
|
|
36
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
37
|
+
`)
|
|
38
|
+
.run(
|
|
39
|
+
requestId,
|
|
40
|
+
kind,
|
|
41
|
+
normalizedPrompt,
|
|
42
|
+
JSON.stringify(normalizedMeta),
|
|
43
|
+
stringOrNull(normalizedMeta.sessionId),
|
|
44
|
+
stringOrNull(normalizedMeta.parentNodeId),
|
|
45
|
+
stringOrNull(normalizedMeta.clientNodeId),
|
|
46
|
+
startedAt,
|
|
47
|
+
"queued",
|
|
48
|
+
startedAt,
|
|
49
|
+
);
|
|
28
50
|
terminalJobs.delete(requestId);
|
|
29
51
|
logEvent("inflight", "start", {
|
|
30
52
|
requestId,
|
|
31
53
|
kind,
|
|
32
|
-
sessionId:
|
|
33
|
-
parentNodeId:
|
|
34
|
-
clientNodeId:
|
|
54
|
+
sessionId: normalizedMeta.sessionId || null,
|
|
55
|
+
parentNodeId: normalizedMeta.parentNodeId || null,
|
|
56
|
+
clientNodeId: normalizedMeta.clientNodeId || null,
|
|
35
57
|
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
36
58
|
});
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
export function setJobPhase(requestId, phase) {
|
|
40
62
|
if (!requestId) return;
|
|
41
|
-
const j =
|
|
63
|
+
const j = getJob(requestId);
|
|
42
64
|
if (!j) return;
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
getDb()
|
|
66
|
+
.prepare("UPDATE inflight SET phase = ?, phase_at = ? WHERE request_id = ?")
|
|
67
|
+
.run(phase, Date.now(), requestId);
|
|
45
68
|
logEvent("inflight", "phase", { requestId, kind: j.kind, phase });
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
export function finishJob(requestId, options = {}) {
|
|
49
72
|
if (!requestId) return;
|
|
50
|
-
const j =
|
|
73
|
+
const j = getJob(requestId);
|
|
51
74
|
if (j) {
|
|
52
75
|
const finishedAt = Date.now();
|
|
53
76
|
const status = options.canceled ? "canceled" : options.status || "completed";
|
|
@@ -76,7 +99,7 @@ export function finishJob(requestId, options = {}) {
|
|
|
76
99
|
errorCode: options.errorCode,
|
|
77
100
|
});
|
|
78
101
|
}
|
|
79
|
-
|
|
102
|
+
getDb().prepare("DELETE FROM inflight WHERE request_id = ?").run(requestId);
|
|
80
103
|
reapTerminalJobs();
|
|
81
104
|
}
|
|
82
105
|
|
|
@@ -88,19 +111,23 @@ function reapTerminalJobs() {
|
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
export function listJobs(filters = {}) {
|
|
91
|
-
|
|
92
|
-
const now = Date.now();
|
|
93
|
-
for (const [id, j] of jobs) {
|
|
94
|
-
if (now - j.startedAt > config.inflight.ttlMs) jobs.delete(id);
|
|
95
|
-
}
|
|
114
|
+
purgeStaleJobs();
|
|
96
115
|
const { kind, sessionId } = filters;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
const clauses = [];
|
|
117
|
+
const params = [];
|
|
118
|
+
if (kind) {
|
|
119
|
+
clauses.push("kind = ?");
|
|
120
|
+
params.push(kind);
|
|
121
|
+
}
|
|
122
|
+
if (sessionId) {
|
|
123
|
+
clauses.push("session_id = ?");
|
|
124
|
+
params.push(sessionId);
|
|
125
|
+
}
|
|
126
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
127
|
+
return getDb()
|
|
128
|
+
.prepare(`SELECT * FROM inflight${where} ORDER BY started_at ASC`)
|
|
129
|
+
.all(...params)
|
|
130
|
+
.map(rowToJob);
|
|
104
131
|
}
|
|
105
132
|
|
|
106
133
|
export function listTerminalJobs(filters = {}) {
|
|
@@ -116,6 +143,62 @@ export function listTerminalJobs(filters = {}) {
|
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
export function _resetForTests() {
|
|
119
|
-
|
|
146
|
+
getDb().prepare("DELETE FROM inflight").run();
|
|
120
147
|
terminalJobs.clear();
|
|
121
148
|
}
|
|
149
|
+
|
|
150
|
+
export function purgeStaleJobs(now = Date.now()) {
|
|
151
|
+
getDb()
|
|
152
|
+
.prepare("DELETE FROM inflight WHERE started_at < ?")
|
|
153
|
+
.run(now - config.inflight.ttlMs);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getJob(requestId) {
|
|
157
|
+
const row = getDb()
|
|
158
|
+
.prepare("SELECT * FROM inflight WHERE request_id = ?")
|
|
159
|
+
.get(requestId);
|
|
160
|
+
return row ? rowToJob(row) : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rowToJob(row) {
|
|
164
|
+
const meta = normalizeMeta(parseMeta(row.meta));
|
|
165
|
+
const sessionId = stringOrNull(row.session_id) ?? stringOrNull(meta.sessionId);
|
|
166
|
+
const parentNodeId =
|
|
167
|
+
stringOrNull(row.parent_node_id) ?? stringOrNull(meta.parentNodeId);
|
|
168
|
+
const clientNodeId =
|
|
169
|
+
stringOrNull(row.client_node_id) ?? stringOrNull(meta.clientNodeId);
|
|
170
|
+
return {
|
|
171
|
+
requestId: row.request_id,
|
|
172
|
+
kind: row.kind,
|
|
173
|
+
prompt: row.prompt || "",
|
|
174
|
+
meta: {
|
|
175
|
+
...meta,
|
|
176
|
+
...(sessionId ? { sessionId } : {}),
|
|
177
|
+
...(parentNodeId ? { parentNodeId } : {}),
|
|
178
|
+
...(clientNodeId ? { clientNodeId } : {}),
|
|
179
|
+
},
|
|
180
|
+
startedAt: Number(row.started_at),
|
|
181
|
+
phase: row.phase || "queued",
|
|
182
|
+
phaseAt: Number(row.phase_at || row.started_at),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseMeta(raw) {
|
|
187
|
+
if (typeof raw !== "string" || !raw) return {};
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
191
|
+
? parsed
|
|
192
|
+
: {};
|
|
193
|
+
} catch {
|
|
194
|
+
return {};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeMeta(meta) {
|
|
199
|
+
return meta && typeof meta === "object" && !Array.isArray(meta) ? meta : {};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stringOrNull(value) {
|
|
203
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
204
|
+
}
|
package/lib/logger.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const REDACTED = "[redacted]";
|
|
2
2
|
const MAX_VALUE_LEN = 240;
|
|
3
|
+
const LOG_LEVELS = {
|
|
4
|
+
debug: 10,
|
|
5
|
+
info: 20,
|
|
6
|
+
warn: 30,
|
|
7
|
+
error: 40,
|
|
8
|
+
silent: 50,
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
const SECRET_KEYS = new Set([
|
|
5
12
|
"authorization",
|
|
@@ -26,6 +33,9 @@ const SECRET_KEYS = new Set([
|
|
|
26
33
|
|
|
27
34
|
const ALLOWED_PROMPT_METRICS = new Set(["promptChars", "promptMode"]);
|
|
28
35
|
|
|
36
|
+
let activeLevel = "info";
|
|
37
|
+
let activeSink = console;
|
|
38
|
+
|
|
29
39
|
function shouldRedactKey(key) {
|
|
30
40
|
if (ALLOWED_PROMPT_METRICS.has(key)) return false;
|
|
31
41
|
if (SECRET_KEYS.has(key)) return true;
|
|
@@ -96,17 +106,41 @@ export function formatLog(scope, event, fields = {}) {
|
|
|
96
106
|
return `[${scope}.${event}]${parts.length ? ` ${parts.join(" ")}` : ""}`;
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
export function normalizeLogLevel(level) {
|
|
110
|
+
return typeof level === "string" && Object.hasOwn(LOG_LEVELS, level) ? level : "info";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function configureLogger(options = {}) {
|
|
114
|
+
activeLevel = normalizeLogLevel(options.level);
|
|
115
|
+
activeSink = options.sink || console;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function shouldLog(level) {
|
|
119
|
+
const normalized = normalizeLogLevel(level);
|
|
120
|
+
return LOG_LEVELS[normalized] >= LOG_LEVELS[activeLevel] && activeLevel !== "silent";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function writeLog(level, line) {
|
|
124
|
+
if (!shouldLog(level)) return;
|
|
125
|
+
const writer = activeSink[level] || activeSink.log || console.log;
|
|
126
|
+
writer.call(activeSink, line);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function logDebug(scope, event, fields = {}) {
|
|
130
|
+
writeLog("debug", formatLog(scope, event, fields));
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
export function logEvent(scope, event, fields = {}) {
|
|
100
|
-
|
|
134
|
+
writeLog("info", formatLog(scope, event, fields));
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
export function logWarn(scope, event, fields = {}) {
|
|
104
|
-
|
|
138
|
+
writeLog("warn", formatLog(scope, event, fields));
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
export function logError(scope, event, err, fields = {}) {
|
|
108
142
|
const safe = sanitizeError(err);
|
|
109
|
-
|
|
143
|
+
writeLog("error", formatLog(scope, event, {
|
|
110
144
|
...fields,
|
|
111
145
|
errorName: safe.name,
|
|
112
146
|
errorCode: safe.code,
|
package/lib/oauthLauncher.js
CHANGED
|
@@ -1,31 +1,64 @@
|
|
|
1
1
|
import { spawnBin } from "../bin/lib/platform.js";
|
|
2
2
|
import { config } from "../config.js";
|
|
3
|
+
import { parseLocalhostPortFromUrl, parseOAuthReadyUrl } from "./runtimePorts.js";
|
|
3
4
|
|
|
4
5
|
export function startOAuthProxy(options = {}) {
|
|
5
6
|
const oauthPort = options.oauthPort ?? config.oauth.proxyPort;
|
|
6
7
|
const restartDelayMs = options.restartDelayMs ?? config.oauth.restartDelayMs;
|
|
8
|
+
let currentChild = null;
|
|
9
|
+
let stopping = false;
|
|
10
|
+
let restartTimer = null;
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const spawnProxy = () => {
|
|
13
|
+
console.log(`Starting openai-oauth on port ${oauthPort}...`);
|
|
14
|
+
const child = spawnBin("npx", ["openai-oauth", "--port", String(oauthPort)], {
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
16
|
+
env: { ...process.env },
|
|
17
|
+
});
|
|
18
|
+
currentChild = child;
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
child.stdout.on("data", (d) => {
|
|
21
|
+
const msg = d.toString().trim();
|
|
22
|
+
if (!msg) return;
|
|
23
|
+
console.log(`[oauth] ${msg}`);
|
|
24
|
+
for (const line of msg.split(/\r?\n/)) {
|
|
25
|
+
const url = parseOAuthReadyUrl(line);
|
|
26
|
+
if (!url) continue;
|
|
27
|
+
const port = parseLocalhostPortFromUrl(url);
|
|
28
|
+
if (port && port !== oauthPort) {
|
|
29
|
+
console.log(`[oauth] requested port ${oauthPort}, actual port ${port}`);
|
|
30
|
+
}
|
|
31
|
+
options.onReady?.({ url, port: port || oauthPort, requestedPort: oauthPort });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
child.stderr.on("data", (d) => {
|
|
36
|
+
const msg = d.toString().trim();
|
|
37
|
+
if (msg && !msg.includes("npm warn")) console.error(`[oauth] ${msg}`);
|
|
38
|
+
});
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
child.on("exit", (code) => {
|
|
41
|
+
if (currentChild === child) currentChild = null;
|
|
42
|
+
if (stopping) return;
|
|
43
|
+
options.onExit?.({ code });
|
|
44
|
+
console.log(`[oauth] exited with code ${code}, restarting in ${Math.round(restartDelayMs / 1000)}s...`);
|
|
45
|
+
restartTimer = setTimeout(spawnProxy, restartDelayMs);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
28
48
|
|
|
29
|
-
|
|
30
|
-
}
|
|
49
|
+
spawnProxy();
|
|
31
50
|
|
|
51
|
+
return {
|
|
52
|
+
get child() {
|
|
53
|
+
return currentChild;
|
|
54
|
+
},
|
|
55
|
+
kill(signal = "SIGTERM") {
|
|
56
|
+
this.stop(signal);
|
|
57
|
+
},
|
|
58
|
+
stop(signal = "SIGTERM") {
|
|
59
|
+
stopping = true;
|
|
60
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
61
|
+
try { currentChild?.kill(signal); } catch {}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/lib/oauthProxy.js
CHANGED
|
@@ -34,10 +34,33 @@ export function buildEditTextPrompt(userPrompt, mode) {
|
|
|
34
34
|
return `Edit this image: ${userPrompt}${AUTO_PROMPT_FIDELITY_SUFFIX}`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function buildEditResearchTextPrompt(userPrompt, mode) {
|
|
38
|
+
if (mode === "direct") return buildEditTextPrompt(userPrompt, mode);
|
|
39
|
+
return `Edit this image: ${userPrompt}${RESEARCH_SUFFIX}${AUTO_PROMPT_FIDELITY_SUFFIX}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
function getOAuthUrl(ctx = {}) {
|
|
38
43
|
return ctx.oauthUrl || `http://127.0.0.1:${config.oauth.proxyPort}`;
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
export async function waitForOAuthReady(ctx = {}) {
|
|
47
|
+
if (!ctx || !Object.prototype.hasOwnProperty.call(ctx, "oauthReadyState")) return;
|
|
48
|
+
if (ctx.oauthReadyState === "ready" || ctx.oauthReadyState === "disabled") return;
|
|
49
|
+
if (ctx.oauthReadyState === "failed") {
|
|
50
|
+
throw makeOAuthError("OAuth proxy is unavailable", { code: "OAUTH_UNAVAILABLE", status: 503 });
|
|
51
|
+
}
|
|
52
|
+
const timeoutMs = ctx.config?.oauth?.statusTimeoutMs ?? config.oauth.statusTimeoutMs;
|
|
53
|
+
if (ctx.oauthReadyPromise) {
|
|
54
|
+
await Promise.race([
|
|
55
|
+
ctx.oauthReadyPromise,
|
|
56
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
if (ctx.oauthReadyState !== "ready" && ctx.oauthReadyState !== "disabled") {
|
|
60
|
+
throw makeOAuthError("OAuth proxy is not ready yet", { code: "OAUTH_UNAVAILABLE", status: 503 });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
function extractSseData(block) {
|
|
42
65
|
let eventData = "";
|
|
43
66
|
for (const line of block.split("\n")) {
|
|
@@ -64,15 +87,30 @@ function extractPartialImage(data) {
|
|
|
64
87
|
return { b64, index, eventType: data.type };
|
|
65
88
|
}
|
|
66
89
|
|
|
67
|
-
function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstreamBodyChars, eventType } = {}) {
|
|
90
|
+
function makeOAuthError(message, { status, code = "OAUTH_UPSTREAM_ERROR", upstreamBodyChars, eventType, eventCount, cause } = {}) {
|
|
68
91
|
const err = new Error(message);
|
|
69
92
|
err.code = code;
|
|
70
93
|
if (status) err.status = status;
|
|
71
94
|
if (typeof upstreamBodyChars === "number") err.upstreamBodyChars = upstreamBodyChars;
|
|
72
95
|
if (eventType) err.eventType = eventType;
|
|
96
|
+
if (typeof eventCount === "number") err.eventCount = eventCount;
|
|
97
|
+
if (cause) err.cause = cause;
|
|
73
98
|
return err;
|
|
74
99
|
}
|
|
75
100
|
|
|
101
|
+
async function fetchOAuth(url, init, { requestId, scope } = {}) {
|
|
102
|
+
try {
|
|
103
|
+
return await fetch(url, init);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logEvent(scope || "oauth", "proxy_unavailable", { requestId, message: err?.message });
|
|
106
|
+
throw makeOAuthError("OAuth proxy is unavailable", {
|
|
107
|
+
code: "OAUTH_UNAVAILABLE",
|
|
108
|
+
status: 503,
|
|
109
|
+
cause: err,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
76
114
|
async function readImageStream(res, { requestId = null, scope = "oauth", onPartialImage = null } = {}) {
|
|
77
115
|
const reader = res.body.getReader();
|
|
78
116
|
const decoder = new TextDecoder();
|
|
@@ -129,9 +167,12 @@ async function readImageStream(res, { requestId = null, scope = "oauth", onParti
|
|
|
129
167
|
if (typeof wsNum === "number" && wsNum > webSearchCalls) webSearchCalls = wsNum;
|
|
130
168
|
}
|
|
131
169
|
if (data.type === "error") {
|
|
170
|
+
const code = data.error?.code || "OAUTH_STREAM_ERROR";
|
|
171
|
+
logEvent(scope, "stream_error", { requestId, code, eventType: data.type, eventCount });
|
|
132
172
|
throw makeOAuthError("OAuth stream returned an error", {
|
|
133
|
-
code
|
|
173
|
+
code,
|
|
134
174
|
eventType: data.type,
|
|
175
|
+
eventCount,
|
|
135
176
|
});
|
|
136
177
|
}
|
|
137
178
|
} catch (e) {
|
|
@@ -154,6 +195,7 @@ export async function generateViaOAuth(
|
|
|
154
195
|
ctx = {},
|
|
155
196
|
options = {},
|
|
156
197
|
) {
|
|
198
|
+
await waitForOAuthReady(ctx);
|
|
157
199
|
const oauthUrl = getOAuthUrl(ctx);
|
|
158
200
|
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
159
201
|
const tools = [
|
|
@@ -178,7 +220,7 @@ export async function generateViaOAuth(
|
|
|
178
220
|
]
|
|
179
221
|
: textPrompt;
|
|
180
222
|
|
|
181
|
-
const res = await
|
|
223
|
+
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
182
224
|
method: "POST",
|
|
183
225
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
184
226
|
body: JSON.stringify({
|
|
@@ -191,7 +233,7 @@ export async function generateViaOAuth(
|
|
|
191
233
|
tool_choice: "auto",
|
|
192
234
|
stream: true,
|
|
193
235
|
}),
|
|
194
|
-
});
|
|
236
|
+
}, { requestId, scope: "oauth" });
|
|
195
237
|
|
|
196
238
|
logEvent("oauth", "response", {
|
|
197
239
|
requestId,
|
|
@@ -199,7 +241,6 @@ export async function generateViaOAuth(
|
|
|
199
241
|
status: res.status,
|
|
200
242
|
contentType: res.headers.get("content-type"),
|
|
201
243
|
});
|
|
202
|
-
if (requestId) setJobPhase(requestId, "streaming");
|
|
203
244
|
|
|
204
245
|
if (!res.ok) {
|
|
205
246
|
const text = await res.text();
|
|
@@ -210,6 +251,8 @@ export async function generateViaOAuth(
|
|
|
210
251
|
});
|
|
211
252
|
}
|
|
212
253
|
|
|
254
|
+
if (requestId) setJobPhase(requestId, "streaming");
|
|
255
|
+
|
|
213
256
|
const contentType = res.headers.get("content-type") || "";
|
|
214
257
|
if (!contentType.includes("text/event-stream")) {
|
|
215
258
|
logEvent("oauth", "json_response", { requestId });
|
|
@@ -234,7 +277,7 @@ export async function generateViaOAuth(
|
|
|
234
277
|
|
|
235
278
|
if (!imageB64) {
|
|
236
279
|
logEvent("oauth", "retry_json", { requestId });
|
|
237
|
-
const retryRes = await
|
|
280
|
+
const retryRes = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
238
281
|
method: "POST",
|
|
239
282
|
headers: { "Content-Type": "application/json" },
|
|
240
283
|
body: JSON.stringify({
|
|
@@ -243,7 +286,7 @@ export async function generateViaOAuth(
|
|
|
243
286
|
tools: [{ type: "image_generation", quality, size, moderation }],
|
|
244
287
|
stream: false,
|
|
245
288
|
}),
|
|
246
|
-
});
|
|
289
|
+
}, { requestId, scope: "oauth" });
|
|
247
290
|
|
|
248
291
|
if (retryRes.ok) {
|
|
249
292
|
const json = await retryRes.json();
|
|
@@ -263,11 +306,33 @@ export async function generateViaOAuth(
|
|
|
263
306
|
}
|
|
264
307
|
|
|
265
308
|
export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
|
|
309
|
+
await waitForOAuthReady(ctx);
|
|
266
310
|
const oauthUrl = getOAuthUrl(ctx);
|
|
267
311
|
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
268
|
-
const
|
|
312
|
+
const searchMode = options.searchMode === "on" ? "on" : "off";
|
|
313
|
+
const textPrompt = searchMode === "on"
|
|
314
|
+
? buildEditResearchTextPrompt(prompt, mode)
|
|
315
|
+
: buildEditTextPrompt(prompt, mode);
|
|
316
|
+
const references = Array.isArray(options.references) ? options.references : [];
|
|
317
|
+
const referenceContent = references.map((b64) => ({
|
|
318
|
+
type: "input_image",
|
|
319
|
+
image_url: `data:image/png;base64,${b64}`,
|
|
320
|
+
}));
|
|
321
|
+
const tools = [
|
|
322
|
+
...(searchMode === "on" ? [{ type: "web_search" }] : []),
|
|
323
|
+
{ type: "image_generation", quality, size, moderation },
|
|
324
|
+
];
|
|
269
325
|
|
|
270
|
-
|
|
326
|
+
logEvent("oauth-edit", "request", {
|
|
327
|
+
requestId,
|
|
328
|
+
model,
|
|
329
|
+
refsCount: references.length,
|
|
330
|
+
inputImageCount: 1 + references.length,
|
|
331
|
+
parentImagePresent: true,
|
|
332
|
+
webSearchEnabled: searchMode === "on",
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
271
336
|
method: "POST",
|
|
272
337
|
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
273
338
|
body: JSON.stringify({
|
|
@@ -278,15 +343,16 @@ export async function editViaOAuth(prompt, imageB64, quality, size, moderation =
|
|
|
278
343
|
role: "user",
|
|
279
344
|
content: [
|
|
280
345
|
{ type: "input_image", image_url: `data:image/png;base64,${imageB64}` },
|
|
346
|
+
...referenceContent,
|
|
281
347
|
{ type: "input_text", text: textPrompt },
|
|
282
348
|
],
|
|
283
349
|
},
|
|
284
350
|
],
|
|
285
|
-
tools
|
|
351
|
+
tools,
|
|
286
352
|
tool_choice: "required",
|
|
287
353
|
stream: true,
|
|
288
354
|
}),
|
|
289
|
-
});
|
|
355
|
+
}, { requestId, scope: "oauth-edit" });
|
|
290
356
|
|
|
291
357
|
logEvent("oauth-edit", "response", {
|
|
292
358
|
requestId,
|
|
@@ -294,7 +360,6 @@ export async function editViaOAuth(prompt, imageB64, quality, size, moderation =
|
|
|
294
360
|
status: res.status,
|
|
295
361
|
contentType: res.headers.get("content-type"),
|
|
296
362
|
});
|
|
297
|
-
if (requestId) setJobPhase(requestId, "streaming");
|
|
298
363
|
|
|
299
364
|
if (!res.ok) {
|
|
300
365
|
const text = await res.text();
|
|
@@ -305,11 +370,13 @@ export async function editViaOAuth(prompt, imageB64, quality, size, moderation =
|
|
|
305
370
|
});
|
|
306
371
|
}
|
|
307
372
|
|
|
308
|
-
|
|
373
|
+
if (requestId) setJobPhase(requestId, "streaming");
|
|
374
|
+
|
|
375
|
+
const { imageB64: resultB64, usage, revisedPrompt, webSearchCalls } = await readImageStream(res, {
|
|
309
376
|
scope: "oauth-edit",
|
|
310
377
|
requestId,
|
|
311
378
|
});
|
|
312
379
|
logEvent("oauth-edit", "stream_end", { requestId, hasImage: !!resultB64 });
|
|
313
|
-
if (resultB64) return { b64: resultB64, usage, revisedPrompt };
|
|
380
|
+
if (resultB64) return { b64: resultB64, usage, revisedPrompt, webSearchCalls };
|
|
314
381
|
throw new Error("No image data received from OAuth edit");
|
|
315
382
|
}
|