Initial commit
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const {
|
||||
getAuthStatus,
|
||||
getLoginState,
|
||||
logoutCodex,
|
||||
runCodexOnce,
|
||||
runCodexStream,
|
||||
startLogin
|
||||
} = require('./codexClient.cjs');
|
||||
|
||||
const app = express();
|
||||
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
|
||||
function isAllowedOrigin(origin) {
|
||||
if (!origin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
return ['localhost', '127.0.0.1', '::1'].includes(url.hostname);
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
app.use(cors({
|
||||
origin: function (origin, callback) {
|
||||
callback(null, isAllowedOrigin(origin));
|
||||
}
|
||||
}));
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
|
||||
function writeSse(res, eventName, data) {
|
||||
res.write('event: ' + eventName + '\n');
|
||||
res.write('data: ' + JSON.stringify(data) + '\n\n');
|
||||
}
|
||||
|
||||
function toHttpStatus(error) {
|
||||
const status = Number(error && (error.status || error.statusCode));
|
||||
if (Number.isFinite(status) && status >= 400 && status < 600) {
|
||||
return status;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
function toErrorMessage(error) {
|
||||
if (!error) {
|
||||
return '未知错误';
|
||||
}
|
||||
|
||||
if (typeof error.message === 'string' && error.message.trim()) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return String(error);
|
||||
}
|
||||
|
||||
app.get('/api/health', function (_req, res) {
|
||||
res.json({ ok: true, provider: 'codex-sdk' });
|
||||
});
|
||||
|
||||
app.get('/api/codex/status', async function (_req, res) {
|
||||
try {
|
||||
const status = await getAuthStatus({ force: true });
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
res.status(toHttpStatus(error)).json({ error: toErrorMessage(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/codex/login-log', function (_req, res) {
|
||||
res.json(getLoginState());
|
||||
});
|
||||
|
||||
app.post('/api/codex/login', function (req, res) {
|
||||
const state = startLogin({
|
||||
deviceAuth: Boolean(req.body && req.body.deviceAuth)
|
||||
});
|
||||
|
||||
res.status(202).json(state);
|
||||
});
|
||||
|
||||
app.post('/api/codex/logout', async function (_req, res) {
|
||||
try {
|
||||
const result = await logoutCodex();
|
||||
res.status(result.ok ? 200 : 500).json(result);
|
||||
} catch (error) {
|
||||
res.status(toHttpStatus(error)).json({ error: toErrorMessage(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/chat', async function (req, res) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// 只有客户端真正断开时才中止 Codex 进程,避免普通请求结束误判为取消。
|
||||
res.on('close', function () {
|
||||
if (!res.writableEnded) {
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runCodexOnce(req.body || {}, controller.signal);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(toHttpStatus(error)).json({ error: toErrorMessage(error) });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/chat/stream', async function (req, res) {
|
||||
const controller = new AbortController();
|
||||
let closed = false;
|
||||
|
||||
// 流式响应中,浏览器停止请求时同步中止后端 Codex 运行。
|
||||
res.on('close', function () {
|
||||
if (!res.writableEnded) {
|
||||
closed = true;
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.flushHeaders && res.flushHeaders();
|
||||
|
||||
try {
|
||||
const result = await runCodexStream(req.body || {}, controller.signal, {
|
||||
onDelta: function (delta) {
|
||||
if (!closed) {
|
||||
writeSse(res, 'delta', { delta: delta });
|
||||
}
|
||||
},
|
||||
onError: function (message) {
|
||||
if (!closed) {
|
||||
writeSse(res, 'error', { error: message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!closed) {
|
||||
writeSse(res, 'done', result);
|
||||
res.end();
|
||||
}
|
||||
} catch (error) {
|
||||
if (!closed) {
|
||||
writeSse(res, 'error', { error: toErrorMessage(error) });
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, HOST, function () {
|
||||
console.log('Local Codex SDK proxy running at http://' + HOST + ':' + PORT);
|
||||
});
|
||||
Reference in New Issue
Block a user