Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ temp/
# Vite
.vite/

# Local toolchains / installers
.node/
node-v*-win-*.zip

# Local Netlify folder
.netlify

Expand Down
367 changes: 367 additions & 0 deletions Codex支持整改计划.md

Large diffs are not rendered by default.

30 changes: 24 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
"mime-types": "^3.0.1",
"multer": "^2.0.1",
"node-fetch": "^2.7.0",
"node-pty": "^1.1.0-beta34",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
Expand All @@ -95,7 +94,11 @@
"tailwind-merge": "^3.3.1",
"ws": "^8.14.2"
},
"optionalDependencies": {
"node-pty": "^1.1.0-beta34"
},
"devDependencies": {
"@rollup/rollup-win32-x64-msvc": "^4.55.3",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.6.0",
Expand Down
22 changes: 20 additions & 2 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import http from 'http';
import cors from 'cors';
import { promises as fsPromises } from 'fs';
import { spawn } from 'child_process';
import pty from 'node-pty';
import fetch from 'node-fetch';
import mime from 'mime-types';

Expand All @@ -77,6 +76,16 @@ import codexRoutes from './routes/codex.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';

let ptyModulePromise = null;
async function getPtyModule() {
if (!ptyModulePromise) {
ptyModulePromise = import('node-pty')
.then((mod) => mod?.default || mod)
.catch(() => null);
}
return ptyModulePromise;
}

// File system watcher for projects folder
let projectsWatcher = null;
const connectedClients = new Set();
Expand Down Expand Up @@ -1051,7 +1060,16 @@ function handleShellConnection(ws) {
const termRows = data.rows || 24;
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);

shellProcess = pty.spawn(shell, shellArgs, {
const ptyModule = await getPtyModule();
if (!ptyModule) {
ws.send(JSON.stringify({
type: 'output',
data: '[ERROR] node-pty 不可用,无法启动交互终端。请使用 Node 20 并在具备 C++ Build Tools 的环境重新安装依赖,或跳过终端功能。'
}));
return;
Comment on lines +1063 to +1069
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize the user-facing fallback message.
The terminal error text is hard-coded in one language; consider making it bilingual or routing through i18n for consistency.

🤖 Prompt for AI Agents
In `@server/index.js` around lines 1063 - 1069, The fallback terminal error
message sent by ws.send when getPtyModule() returns null is hard-coded in
Chinese; update the handler in the getPtyModule() null branch to produce a
localized message (e.g., via the existing i18n/localization helper or by
including both English and Chinese) instead of a single-language string—modify
the payload created for ws.send (type: 'output', data: ...) so it uses the
project's localization method or returns a bilingual string; ensure you update
the same branch where getPtyModule() is awaited and the early return occurs.

}

shellProcess = ptyModule.spawn(shell, shellArgs, {
name: 'xterm-256color',
cols: termCols,
rows: termRows,
Expand Down
70 changes: 66 additions & 4 deletions server/openai-codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,52 @@ function mapPermissionModeToCodexOptions(permissionMode) {
default:
return {
sandboxMode: 'workspace-write',
approvalPolicy: 'untrusted'
// UI 暂未实现 Codex 的审批交互,默认避免进入“等待审批/卡住”的状态
approvalPolicy: 'never'
};
}
}

function codexErrorToPayload(error) {
const rawMessage = String(error?.message || 'Unknown error');
const status = error?.status || error?.response?.status;

const lower = rawMessage.toLowerCase();
const looksLikeNotFound =
status === 404 ||
lower.includes('not found') ||
lower.includes('thread') && lower.includes('missing');

if (looksLikeNotFound) {
return {
code: 'CODEX_THREAD_NOT_FOUND',
message: 'Codex 会话不存在或已失效:请在侧边栏选择一个有效会话,或新建会话后重试。',
details: rawMessage
};
}

const looksLikeAuth =
status === 401 ||
status === 403 ||
lower.includes('api key') ||
lower.includes('unauthorized') ||
lower.includes('forbidden');

if (looksLikeAuth) {
return {
code: 'CODEX_AUTH_FAILED',
message: 'Codex 认证失败:请检查环境变量/配置(如 OPENAI_API_KEY),或确认本机已完成 Codex 登录。',
details: rawMessage
};
}

return {
code: 'CODEX_ERROR',
message: rawMessage,
details: rawMessage
};
}
Comment on lines +186 to +224
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve pre‑classified error codes to keep UI hints.

Errors rethrown from resumeThread carry err.code/err.details, but codexErrorToPayload discards them and re‑classifies based on message text. If the message is localized, it will fall back to CODEX_ERROR, so the frontend loses CODEX_THREAD_NOT_FOUND / CODEX_AUTH_FAILED hints.

✅ Suggested fix
 function codexErrorToPayload(error) {
+  if (error?.code) {
+    const msg = String(error.message || 'Unknown error');
+    return {
+      code: error.code,
+      message: msg,
+      details: error.details ?? msg
+    };
+  }
   const rawMessage = String(error?.message || 'Unknown error');
   const status = error?.status || error?.response?.status;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function codexErrorToPayload(error) {
const rawMessage = String(error?.message || 'Unknown error');
const status = error?.status || error?.response?.status;
const lower = rawMessage.toLowerCase();
const looksLikeNotFound =
status === 404 ||
lower.includes('not found') ||
lower.includes('thread') && lower.includes('missing');
if (looksLikeNotFound) {
return {
code: 'CODEX_THREAD_NOT_FOUND',
message: 'Codex 会话不存在或已失效:请在侧边栏选择一个有效会话,或新建会话后重试。',
details: rawMessage
};
}
const looksLikeAuth =
status === 401 ||
status === 403 ||
lower.includes('api key') ||
lower.includes('unauthorized') ||
lower.includes('forbidden');
if (looksLikeAuth) {
return {
code: 'CODEX_AUTH_FAILED',
message: 'Codex 认证失败:请检查环境变量/配置(如 OPENAI_API_KEY),或确认本机已完成 Codex 登录。',
details: rawMessage
};
}
return {
code: 'CODEX_ERROR',
message: rawMessage,
details: rawMessage
};
}
function codexErrorToPayload(error) {
if (error?.code) {
const msg = String(error.message || 'Unknown error');
return {
code: error.code,
message: msg,
details: error.details ?? msg
};
}
const rawMessage = String(error?.message || 'Unknown error');
const status = error?.status || error?.response?.status;
const lower = rawMessage.toLowerCase();
const looksLikeNotFound =
status === 404 ||
lower.includes('not found') ||
lower.includes('thread') && lower.includes('missing');
if (looksLikeNotFound) {
return {
code: 'CODEX_THREAD_NOT_FOUND',
message: 'Codex 会话不存在或已失效:请在侧边栏选择一个有效会话,或新建会话后重试。',
details: rawMessage
};
}
const looksLikeAuth =
status === 401 ||
status === 403 ||
lower.includes('api key') ||
lower.includes('unauthorized') ||
lower.includes('forbidden');
if (looksLikeAuth) {
return {
code: 'CODEX_AUTH_FAILED',
message: 'Codex 认证失败:请检查环境变量/配置(如 OPENAI_API_KEY),或确认本机已完成 Codex 登录。',
details: rawMessage
};
}
return {
code: 'CODEX_ERROR',
message: rawMessage,
details: rawMessage
};
}
🤖 Prompt for AI Agents
In `@server/openai-codex.js` around lines 186 - 224, codexErrorToPayload currently
re-classifies every error from resumeThread by parsing the message and drops any
pre-set err.code/err.details; update codexErrorToPayload to first check for an
existing error.code on the incoming error and, if present, return that code with
message = error.message || rawMessage and details = error.details || rawMessage
(preserving any localized message and original details), otherwise fall back to
the existing status/message-based classification logic; reference the function
codexErrorToPayload and the incoming properties error.code and error.details
when making the change.


/**
* Execute a Codex query with streaming
* @param {string} command - The prompt to send
Expand All @@ -205,6 +246,16 @@ export async function queryCodex(command, options = {}, ws) {
let currentSessionId = sessionId;

try {
console.info('[Codex] query', {
projectPath,
cwd,
sessionId,
model,
permissionMode,
mapped: { sandboxMode, approvalPolicy },
workingDirectory
});

// Initialize Codex SDK
codex = new Codex();

Expand All @@ -219,7 +270,15 @@ export async function queryCodex(command, options = {}, ws) {

// Start or resume thread
if (sessionId) {
thread = codex.resumeThread(sessionId, threadOptions);
try {
thread = codex.resumeThread(sessionId, threadOptions);
} catch (resumeError) {
const payload = codexErrorToPayload(resumeError);
const err = new Error(payload.message);
err.code = payload.code;
err.details = payload.details;
throw err;
}
} else {
thread = codex.startThread(threadOptions);
}
Expand Down Expand Up @@ -285,11 +344,14 @@ export async function queryCodex(command, options = {}, ws) {
});

} catch (error) {
console.error('[Codex] Error:', error);
const payload = codexErrorToPayload(error);
console.error('[Codex] Error:', { code: payload.code, message: payload.message, details: payload.details });

sendMessage(ws, {
type: 'codex-error',
error: error.message,
error: payload.message,
code: payload.code,
details: payload.details,
sessionId: currentSessionId
});

Expand Down
Loading