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); });