fix(app): 強化 Codex 執行與 auth 暫存處理
This commit is contained in:
+58
-41
@@ -39,11 +39,11 @@ class TempFileRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
for (const filePath of Array.from(this.files).reverse()) {
|
for (const filePath of this.files) {
|
||||||
this.removeFile(filePath);
|
this.removeFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const dirPath of Array.from(this.dirs).reverse()) {
|
for (const dirPath of this.dirs) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(dirPath, { force: true, recursive: true });
|
fs.rmSync(dirPath, { force: true, recursive: true });
|
||||||
this.dirs.delete(dirPath);
|
this.dirs.delete(dirPath);
|
||||||
@@ -56,6 +56,37 @@ class TempFileRegistry {
|
|||||||
|
|
||||||
const tempFiles = new TempFileRegistry();
|
const tempFiles = new TempFileRegistry();
|
||||||
|
|
||||||
|
class OutputCollector {
|
||||||
|
constructor(maxBytes) {
|
||||||
|
this.maxBytes = maxBytes;
|
||||||
|
this.chunks = [];
|
||||||
|
this.size = 0;
|
||||||
|
this.truncated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
append(chunk) {
|
||||||
|
const available = this.maxBytes - this.size;
|
||||||
|
|
||||||
|
if (available <= 0) {
|
||||||
|
this.truncated = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedChunk = chunk.length > available ? chunk.subarray(0, available) : chunk;
|
||||||
|
this.chunks.push(storedChunk);
|
||||||
|
this.size += storedChunk.length;
|
||||||
|
|
||||||
|
if (storedChunk.length < chunk.length) {
|
||||||
|
this.truncated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const truncationMessage = this.truncated ? "\n[Output truncated]\n" : "";
|
||||||
|
return `${Buffer.concat(this.chunks, this.size).toString()}${truncationMessage}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
tempFiles.cleanup();
|
tempFiles.cleanup();
|
||||||
}
|
}
|
||||||
@@ -101,13 +132,16 @@ function fail(message, code = 1) {
|
|||||||
process.exit(code);
|
process.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAuth(encodedAuth, authFile) {
|
function normalizeBase64(value) {
|
||||||
const normalizedAuth = encodedAuth.replace(/\s+/g, "");
|
return value.replace(/\s+/g, "").replace(/=+$/, "");
|
||||||
const decoded = Buffer.from(encodedAuth, "base64");
|
}
|
||||||
const normalizedDecoded = decoded.toString("base64").replace(/=+$/, "");
|
|
||||||
const normalizedInput = normalizedAuth.replace(/=+$/, "");
|
|
||||||
|
|
||||||
if (decoded.length === 0 && normalizedAuth.length > 0) {
|
function validateAuth(encodedAuth, authFile) {
|
||||||
|
const decoded = Buffer.from(encodedAuth, "base64");
|
||||||
|
const normalizedDecoded = normalizeBase64(decoded.toString("base64"));
|
||||||
|
const normalizedInput = normalizeBase64(encodedAuth);
|
||||||
|
|
||||||
|
if (decoded.length === 0 && normalizedInput.length > 0) {
|
||||||
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
fail("OAUTH must be valid base64 encoded Codex auth.json.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,18 +168,6 @@ function parsePositiveInteger(value, fallback) {
|
|||||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncateOutput(chunks, nextChunk, maxBytes) {
|
|
||||||
const currentSize = chunks.reduce((total, chunk) => total + chunk.length, 0);
|
|
||||||
const available = maxBytes - currentSize;
|
|
||||||
|
|
||||||
if (available <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks.push(nextChunk.length > available ? nextChunk.subarray(0, available) : nextChunk);
|
|
||||||
return nextChunk.length <= available;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runCodex(model, prompt) {
|
function runCodex(model, prompt) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS);
|
const timeoutMs = parsePositiveInteger(process.env.CODEX_TIMEOUT_MS, DEFAULT_CODEX_TIMEOUT_MS);
|
||||||
@@ -167,46 +189,39 @@ function runCodex(model, prompt) {
|
|||||||
{ cwd: workspace, stdio: ["ignore", "pipe", "pipe"] },
|
{ cwd: workspace, stdio: ["ignore", "pipe", "pipe"] },
|
||||||
);
|
);
|
||||||
|
|
||||||
const outputChunks = [];
|
const output = new OutputCollector(outputLimitBytes);
|
||||||
let outputTruncated = false;
|
|
||||||
const appendOutput = (chunk) => {
|
|
||||||
if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) {
|
|
||||||
outputTruncated = true;
|
|
||||||
}
|
|
||||||
return Buffer.concat(outputChunks).toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
child.kill("SIGTERM");
|
child.kill("SIGTERM");
|
||||||
appendOutput(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`));
|
output.append(Buffer.from(`Codex execution timed out after ${timeoutMs} ms.\n`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
child.stdout.pipe(process.stdout);
|
child.stdout.pipe(process.stdout);
|
||||||
child.stderr.pipe(process.stdout);
|
child.stderr.pipe(process.stdout);
|
||||||
|
|
||||||
child.stdout.on("data", (chunk) => {
|
child.stdout.on("data", (chunk) => {
|
||||||
if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) {
|
output.append(chunk);
|
||||||
outputTruncated = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr.on("data", (chunk) => {
|
child.stderr.on("data", (chunk) => {
|
||||||
if (!truncateOutput(outputChunks, chunk, outputLimitBytes)) {
|
output.append(chunk);
|
||||||
outputTruncated = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", (error) => {
|
child.on("error", (error) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const output = appendOutput(Buffer.from(`${error.message}\n`));
|
const message = error.code === "ENOENT" ? "Unable to find codex command.\n" : `${error.message}\n`;
|
||||||
resolve({ status: 1, output });
|
output.append(Buffer.from(message));
|
||||||
|
resolve({ status: 1, output: output.toString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code, signal) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const truncationMessage = outputTruncated ? "\n[Output truncated]\n" : "";
|
|
||||||
const output = `${Buffer.concat(outputChunks).toString()}${truncationMessage}`;
|
if (code === null && signal) {
|
||||||
resolve({ status: code ?? 1, output });
|
output.append(Buffer.from(`Codex process terminated by signal ${signal}.\n`));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ status: code ?? 1, output: output.toString() });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -243,6 +258,8 @@ function readConfig() {
|
|||||||
function setupAuth(oauth, codexHome) {
|
function setupAuth(oauth, codexHome) {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
fs.mkdirSync(codexHome, { recursive: true, mode: DIR_MODE_PRIVATE });
|
||||||
|
fs.chmodSync(codexHome, DIR_MODE_PRIVATE);
|
||||||
|
fs.accessSync(codexHome, fs.constants.R_OK | fs.constants.W_OK | fs.constants.X_OK);
|
||||||
} catch {
|
} catch {
|
||||||
fail("Unable to create CODEX_HOME.");
|
fail("Unable to create CODEX_HOME.");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user