feat: add CentOS7 single executable build
This commit is contained in:
61
README.md
61
README.md
@@ -119,6 +119,29 @@ Codex App 原生协作工具会被转成页面上的子代理状态卡片:
|
||||
- npm
|
||||
- 已安装并登录需要使用的 Agent CLI:
|
||||
|
||||
源码方式运行需要 Node.js >= 18。Node.js 版本过低时,可先执行:
|
||||
|
||||
```bash
|
||||
# 已安装 nvm 时,使用当前 LTS 版本
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
node -v
|
||||
|
||||
# Ubuntu / Debian / WSL,可固定安装 Node.js 22;root 用户可去掉 sudo
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
node -v
|
||||
|
||||
# RHEL / Rocky / AlmaLinux 8/9,可固定安装 Node.js 22;root 用户可去掉 sudo
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo dnf install -y nodejs || sudo yum install -y nodejs
|
||||
node -v
|
||||
```
|
||||
|
||||
CentOS 7 这类老系统通常停在 `glibc 2.17`,NodeSource Node.js 22 RPM 会要求
|
||||
`glibc >= 2.28`,不要在这台机器上硬装。老系统部署请使用下面的
|
||||
**Bun single executable** 方式。
|
||||
|
||||
```bash
|
||||
npm install -g @anthropic-ai/claude-code
|
||||
npm install -g @openai/codex
|
||||
@@ -162,6 +185,44 @@ npm start
|
||||
http://localhost:8002
|
||||
```
|
||||
|
||||
### CentOS 7 / 老 glibc
|
||||
|
||||
hapi 能在 CentOS 7 上运行,不是因为它在 CentOS 7 上安装了新版 Node,而是因为
|
||||
它在构建机用 `bun build --compile --target=bun-linux-x64-baseline` 产出 baseline
|
||||
单文件二进制。cc-web 也按这个思路提供发布包构建。
|
||||
|
||||
在较新的 Linux 构建机或 CI 上执行:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:single-exe
|
||||
```
|
||||
|
||||
默认会生成:
|
||||
|
||||
```text
|
||||
dist-exe/bun-linux-x64-baseline/
|
||||
```
|
||||
|
||||
把这个目录整体拷贝到 CentOS 7 后直接运行:
|
||||
|
||||
```bash
|
||||
cd /opt/cc-web
|
||||
chmod +x cc-web
|
||||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web
|
||||
```
|
||||
|
||||
这个发布包只包含 cc-web 服务本体和前端资源,**不会把 Claude/Codex CLI 打进包里**。
|
||||
运行时仍调用宿主机上的 CLI:
|
||||
|
||||
```bash
|
||||
export CLAUDE_PATH=/usr/local/bin/claude
|
||||
export CODEX_PATH=/usr/local/bin/codex
|
||||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web
|
||||
```
|
||||
|
||||
如果不设置 `CLAUDE_PATH` / `CODEX_PATH`,默认仍从宿主机 `PATH` 查找 `claude` 和 `codex`。
|
||||
|
||||
### Windows
|
||||
|
||||
```cmd
|
||||
|
||||
@@ -9,7 +9,7 @@ function createAgentRuntime(deps) {
|
||||
getDefaultCodexModel,
|
||||
loadCodexConfig,
|
||||
prepareCodexCustomRuntime,
|
||||
ccwebMcpServerPath,
|
||||
ccwebMcpServerArg,
|
||||
internalMcpUrl,
|
||||
internalMcpToken,
|
||||
nodePath,
|
||||
@@ -31,7 +31,7 @@ function createAgentRuntime(deps) {
|
||||
}
|
||||
|
||||
function createCcwebMcpEnv(session, options = {}) {
|
||||
if (!ccwebMcpServerPath || !internalMcpUrl || !internalMcpToken || !session?.id) return null;
|
||||
if (!ccwebMcpServerArg || !internalMcpUrl || !internalMcpToken || !session?.id) return null;
|
||||
const rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
|
||||
const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
|
||||
return {
|
||||
@@ -48,7 +48,7 @@ function createAgentRuntime(deps) {
|
||||
args.push(
|
||||
'-c', 'mcp_servers.ccweb.type="stdio"',
|
||||
'-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`,
|
||||
'-c', `mcp_servers.ccweb.args=${tomlStringArray([ccwebMcpServerPath])}`,
|
||||
'-c', `mcp_servers.ccweb.args=${tomlStringArray([ccwebMcpServerArg])}`,
|
||||
'-c', `mcp_servers.ccweb.env_vars=${tomlStringArray(envVars)}`,
|
||||
'-c', 'mcp_servers.ccweb.startup_timeout_sec=10',
|
||||
'-c', 'mcp_servers.ccweb.tool_timeout_sec=60'
|
||||
|
||||
@@ -306,9 +306,7 @@ async function handleRequest(message) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TOOLS };
|
||||
|
||||
if (require.main === module) {
|
||||
function runStdioServer() {
|
||||
let lineBuffer = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
@@ -334,3 +332,9 @@ if (require.main === module) {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { TOOLS, runStdioServer };
|
||||
|
||||
if (require.main === module) {
|
||||
runStdioServer();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { fork } = require('child_process');
|
||||
const { fork, spawn } = require('child_process');
|
||||
|
||||
function createCodexAppWorkerClient(options = {}) {
|
||||
const workerPath = options.workerPath || path.join(__dirname, 'codex-app-worker.js');
|
||||
const workerCommand = String(options.workerCommand || '').trim();
|
||||
const workerArgs = Array.isArray(options.workerArgs) ? options.workerArgs : [];
|
||||
const onNotification = typeof options.onNotification === 'function' ? options.onNotification : () => {};
|
||||
const onServerRequest = typeof options.onServerRequest === 'function' ? options.onServerRequest : null;
|
||||
const onExit = typeof options.onExit === 'function' ? options.onExit : () => {};
|
||||
@@ -40,11 +42,14 @@ function createCodexAppWorkerClient(options = {}) {
|
||||
workerExited = false;
|
||||
configured = false;
|
||||
appServerRunning = false;
|
||||
worker = fork(workerPath, [], {
|
||||
const spawnOptions = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
||||
});
|
||||
};
|
||||
worker = workerCommand
|
||||
? spawn(workerCommand, workerArgs, spawnOptions)
|
||||
: fork(workerPath, [], spawnOptions);
|
||||
|
||||
worker.on('message', (message = {}) => {
|
||||
if (Object.prototype.hasOwnProperty.call(message, 'id')) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"build:single-exe": "node scripts/build-single-exe.js",
|
||||
"regression": "node scripts/regression.js"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
203
scripts/build-single-exe.js
Normal file
203
scripts/build-single-exe.js
Normal file
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_TARGET = 'bun-linux-x64-baseline';
|
||||
const DEFAULT_OUTDIR = 'dist-exe';
|
||||
const DEFAULT_NAME = 'cc-web';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
target: DEFAULT_TARGET,
|
||||
outdir: DEFAULT_OUTDIR,
|
||||
name: DEFAULT_NAME,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
const readValue = (name) => {
|
||||
const inlinePrefix = `${name}=`;
|
||||
if (arg.startsWith(inlinePrefix)) return arg.slice(inlinePrefix.length);
|
||||
if (arg === name && index + 1 < argv.length) {
|
||||
index += 1;
|
||||
return argv[index];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const target = readValue('--target');
|
||||
if (target !== null) {
|
||||
options.target = target;
|
||||
continue;
|
||||
}
|
||||
|
||||
const outdir = readValue('--outdir');
|
||||
if (outdir !== null) {
|
||||
options.outdir = outdir;
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = readValue('--name');
|
||||
if (name !== null) {
|
||||
options.name = name;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '-h' || arg === '--help') {
|
||||
options.help = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`未知参数: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`用法:
|
||||
npm run build:single-exe
|
||||
node scripts/build-single-exe.js --target bun-linux-x64-baseline --outdir dist-exe --name cc-web
|
||||
|
||||
说明:
|
||||
默认 target 是 ${DEFAULT_TARGET},用于兼容 CentOS 7 这类老 glibc Linux x64 系统。
|
||||
该命令只打包 cc-web 服务本体;Claude/Codex CLI 仍在运行时从宿主机 PATH 或 CLAUDE_PATH/CODEX_PATH 调用。`);
|
||||
}
|
||||
|
||||
function isWindowsTarget(target) {
|
||||
return /^bun-windows(?:-|$)/.test(String(target || ''));
|
||||
}
|
||||
|
||||
function resolveBinaryName(target, name) {
|
||||
return isWindowsTarget(target) && !name.endsWith('.exe') ? `${name}.exe` : name;
|
||||
}
|
||||
|
||||
function copyFileIfExists(source, destination) {
|
||||
if (!fs.existsSync(source)) return false;
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.copyFileSync(source, destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyDirIfExists(source, destination) {
|
||||
if (!fs.existsSync(source)) return false;
|
||||
fs.cpSync(source, destination, { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
function writeRunningGuide(releaseDir, target, binaryName) {
|
||||
const runCommand = isWindowsTarget(target) ? binaryName : `./${binaryName}`;
|
||||
const content = `# cc-web single executable 运行说明
|
||||
|
||||
这个目录是 Bun single executable 发布包,面向 CentOS 7 / 老 glibc Linux 运行。
|
||||
|
||||
## 直接运行
|
||||
|
||||
\`\`\`bash
|
||||
chmod +x ${binaryName}
|
||||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ${runCommand}
|
||||
\`\`\`
|
||||
|
||||
## 调用宿主机 Claude/Codex CLI
|
||||
|
||||
cc-web 不内置 Claude/Codex CLI。运行时会继续从宿主机 PATH 查找:
|
||||
|
||||
\`\`\`bash
|
||||
export CLAUDE_PATH=/usr/local/bin/claude
|
||||
export CODEX_PATH=/usr/local/bin/codex
|
||||
${runCommand}
|
||||
\`\`\`
|
||||
|
||||
如果不设置上述变量,默认命令名仍是 \`claude\` 和 \`codex\`。
|
||||
|
||||
## 目录说明
|
||||
|
||||
- \`public/\`:前端静态资源
|
||||
- \`config/\`:运行时配置,首次启动会写入登录和通知配置
|
||||
- \`sessions/\`:会话数据
|
||||
- \`logs/\`:运行日志
|
||||
|
||||
不要把已有生产环境的 \`config/\`、\`sessions/\` 误删或覆盖。
|
||||
`;
|
||||
fs.writeFileSync(path.join(releaseDir, 'RUNNING.md'), content, 'utf8');
|
||||
}
|
||||
|
||||
function copyRuntimeAssets(projectRoot, releaseDir, target, binaryName) {
|
||||
copyDirIfExists(path.join(projectRoot, 'public'), path.join(releaseDir, 'public'));
|
||||
|
||||
for (const file of ['.env.example', 'README.md', 'README.en.md', 'CHANGELOG.md', 'package.json']) {
|
||||
copyFileIfExists(path.join(projectRoot, file), path.join(releaseDir, file));
|
||||
}
|
||||
|
||||
for (const dir of ['.codex/skills', '.codex/prompts', '.agents/skills', '.agents/prompts']) {
|
||||
copyDirIfExists(path.join(projectRoot, dir), path.join(releaseDir, dir));
|
||||
}
|
||||
|
||||
for (const dir of ['config', 'sessions', 'logs']) {
|
||||
fs.mkdirSync(path.join(releaseDir, dir), { recursive: true });
|
||||
}
|
||||
|
||||
writeRunningGuide(releaseDir, target, binaryName);
|
||||
}
|
||||
|
||||
function runBunBuild(projectRoot, target, outfile) {
|
||||
const bunCommand = process.versions.bun ? process.execPath : (process.env.BUN_BIN || 'bun');
|
||||
const args = [
|
||||
'build',
|
||||
'--compile',
|
||||
'--no-compile-autoload-dotenv',
|
||||
`--target=${target}`,
|
||||
`--outfile=${outfile}`,
|
||||
path.join(projectRoot, 'server.js'),
|
||||
];
|
||||
|
||||
console.log(`[build:single-exe] ${bunCommand} ${args.join(' ')}`);
|
||||
const result = spawnSync(bunCommand, args, {
|
||||
cwd: projectRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
if (result.error && result.error.code === 'ENOENT') {
|
||||
throw new Error('未找到 bun。请先在构建机安装 Bun,或通过 BUN_BIN 指定 bun 可执行文件。');
|
||||
}
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Bun 编译失败,退出码: ${result.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const target = options.target || DEFAULT_TARGET;
|
||||
const binaryName = resolveBinaryName(target, options.name || DEFAULT_NAME);
|
||||
const outdir = path.resolve(projectRoot, options.outdir || DEFAULT_OUTDIR);
|
||||
const releaseDir = path.join(outdir, target);
|
||||
const outfile = path.join(releaseDir, binaryName);
|
||||
|
||||
fs.rmSync(releaseDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(releaseDir, { recursive: true });
|
||||
|
||||
runBunBuild(projectRoot, target, outfile);
|
||||
if (!isWindowsTarget(target)) fs.chmodSync(outfile, 0o755);
|
||||
copyRuntimeAssets(projectRoot, releaseDir, target, binaryName);
|
||||
|
||||
console.log(`[build:single-exe] 发布目录: ${releaseDir}`);
|
||||
console.log(`[build:single-exe] 可执行文件: ${outfile}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(`[build:single-exe] ERROR: ${err.message || err}`);
|
||||
process.exit(1);
|
||||
}
|
||||
63
server.js
63
server.js
@@ -12,8 +12,37 @@ const { createCodexAppRuntime } = require('./lib/codex-app-runtime');
|
||||
const { createCodexRolloutStore } = require('./lib/codex-rollouts');
|
||||
const { TOOLS: CCWEB_MCP_TOOLS } = require('./lib/ccweb-mcp-server');
|
||||
|
||||
if (process.argv.includes('--ccweb-mcp-server')) {
|
||||
require('./lib/ccweb-mcp-server').runStdioServer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.argv.includes('--codex-app-worker')) {
|
||||
require('./lib/codex-app-worker');
|
||||
return;
|
||||
}
|
||||
|
||||
function resolveAppDir() {
|
||||
const explicit = String(process.env.CC_WEB_APP_DIR || '').trim();
|
||||
if (explicit) return path.resolve(explicit);
|
||||
|
||||
const execDir = process.execPath ? path.dirname(process.execPath) : '';
|
||||
if (process.versions?.bun && execDir && fs.existsSync(path.join(execDir, 'public'))) {
|
||||
return execDir;
|
||||
}
|
||||
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
const APP_DIR = resolveAppDir();
|
||||
const IS_BUN_SINGLE_EXECUTABLE = !!process.versions?.bun
|
||||
&& process.execPath
|
||||
&& !/^bun(?:\.exe)?$/i.test(path.basename(process.execPath));
|
||||
const CCWEB_MCP_SERVER_ARG = '--ccweb-mcp-server';
|
||||
const CODEX_APP_WORKER_ARG = '--codex-app-worker';
|
||||
|
||||
// Load .env
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
const envPath = path.join(APP_DIR, '.env');
|
||||
if (fs.existsSync(envPath)) {
|
||||
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
||||
const m = line.match(/^([^#=]+)=(.*)$/);
|
||||
@@ -33,11 +62,10 @@ const PORT = parseInt(process.env.PORT) || 8002;
|
||||
const CLAUDE_PATH = process.env.CLAUDE_PATH || 'claude';
|
||||
const CODEX_PATH = process.env.CODEX_PATH || 'codex';
|
||||
const INTERNAL_MCP_TOKEN = process.env.CC_WEB_INTERNAL_MCP_TOKEN || crypto.randomBytes(32).toString('hex');
|
||||
const CONFIG_DIR = process.env.CC_WEB_CONFIG_DIR || path.join(__dirname, 'config');
|
||||
const SESSIONS_DIR = process.env.CC_WEB_SESSIONS_DIR || path.join(__dirname, 'sessions');
|
||||
const PUBLIC_DIR = process.env.CC_WEB_PUBLIC_DIR || path.join(__dirname, 'public');
|
||||
const LOGS_DIR = process.env.CC_WEB_LOGS_DIR || path.join(__dirname, 'logs');
|
||||
const CCWEB_MCP_SERVER_PATH = path.join(__dirname, 'lib', 'ccweb-mcp-server.js');
|
||||
const CONFIG_DIR = process.env.CC_WEB_CONFIG_DIR || path.join(APP_DIR, 'config');
|
||||
const SESSIONS_DIR = process.env.CC_WEB_SESSIONS_DIR || path.join(APP_DIR, 'sessions');
|
||||
const PUBLIC_DIR = process.env.CC_WEB_PUBLIC_DIR || path.join(APP_DIR, 'public');
|
||||
const LOGS_DIR = process.env.CC_WEB_LOGS_DIR || path.join(APP_DIR, 'logs');
|
||||
const ATTACHMENTS_DIR = path.join(SESSIONS_DIR, '_attachments');
|
||||
const ATTACHMENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
|
||||
@@ -1789,8 +1817,8 @@ function composerSkillRoots(options = {}) {
|
||||
if (codexHome) roots.push(path.join(codexHome, 'skills'));
|
||||
const homeDir = normalizeExistingDirPath(process.env.HOME || process.env.USERPROFILE || '');
|
||||
if (homeDir) roots.push(path.join(homeDir, '.agents', 'skills'));
|
||||
roots.push(path.join(__dirname, '.codex', 'skills'));
|
||||
roots.push(path.join(__dirname, '.agents', 'skills'));
|
||||
roots.push(path.join(APP_DIR, '.codex', 'skills'));
|
||||
roots.push(path.join(APP_DIR, '.agents', 'skills'));
|
||||
roots.push('/etc/codex/skills');
|
||||
const seen = new Set();
|
||||
return roots.filter((root) => {
|
||||
@@ -1899,8 +1927,8 @@ function composerPromptRoots() {
|
||||
const roots = [];
|
||||
const codexHome = getCodexHomeDir();
|
||||
if (codexHome) roots.push(path.join(codexHome, 'prompts'));
|
||||
roots.push(path.join(__dirname, '.codex', 'prompts'));
|
||||
roots.push(path.join(__dirname, '.agents', 'prompts'));
|
||||
roots.push(path.join(APP_DIR, '.codex', 'prompts'));
|
||||
roots.push(path.join(APP_DIR, '.agents', 'prompts'));
|
||||
return roots;
|
||||
}
|
||||
|
||||
@@ -6613,7 +6641,7 @@ const {
|
||||
getDefaultCodexModel,
|
||||
loadCodexConfig,
|
||||
prepareCodexCustomRuntime,
|
||||
ccwebMcpServerPath: CCWEB_MCP_SERVER_PATH,
|
||||
ccwebMcpServerArg: CCWEB_MCP_SERVER_ARG,
|
||||
internalMcpUrl: `http://127.0.0.1:${PORT}/api/internal/mcp`,
|
||||
internalMcpToken: INTERNAL_MCP_TOKEN,
|
||||
nodePath: process.execPath,
|
||||
@@ -7577,7 +7605,7 @@ function codexAppTurnPermissionParams(session) {
|
||||
}
|
||||
|
||||
function codexAppCcwebMcpEnv(session, options = {}) {
|
||||
if (!session?.id || !INTERNAL_MCP_TOKEN || !CCWEB_MCP_SERVER_PATH) return null;
|
||||
if (!session?.id || !INTERNAL_MCP_TOKEN) return null;
|
||||
const rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
|
||||
const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
|
||||
return {
|
||||
@@ -7594,7 +7622,7 @@ function codexAppThreadConfig(session, options = {}) {
|
||||
return {
|
||||
'mcp_servers.ccweb': {
|
||||
command: process.execPath,
|
||||
args: [CCWEB_MCP_SERVER_PATH],
|
||||
args: [CCWEB_MCP_SERVER_ARG],
|
||||
env: ccwebMcpEnv,
|
||||
startup_timeout_sec: 10,
|
||||
tool_timeout_sec: 60,
|
||||
@@ -7707,12 +7735,17 @@ function getCodexAppClient() {
|
||||
onLog: (level, event, data) => plog(level, event, data),
|
||||
postInitialize: codexAppPostInitialize,
|
||||
};
|
||||
if (IS_BUN_SINGLE_EXECUTABLE) {
|
||||
clientOptions.workerCommand = process.execPath;
|
||||
clientOptions.workerArgs = [CODEX_APP_WORKER_ARG];
|
||||
}
|
||||
codexAppClient = CODEX_APP_WORKER_ENABLED
|
||||
? createCodexAppWorkerClient(clientOptions)
|
||||
: createCodexAppServerClient(clientOptions);
|
||||
codexAppClientSignature = signature;
|
||||
plog('INFO', 'codex_app_client_created', {
|
||||
worker: CODEX_APP_WORKER_ENABLED,
|
||||
workerLauncher: IS_BUN_SINGLE_EXECUTABLE ? 'single-executable' : 'source-file',
|
||||
command: path.basename(spec.command || ''),
|
||||
strippedEnvKeys: spec.strippedEnvKeys || [],
|
||||
});
|
||||
@@ -8059,11 +8092,11 @@ function handleCodexAppAbortSession(sessionId, ws = null) {
|
||||
function handleCheckUpdate(ws) {
|
||||
const localVersion = (() => {
|
||||
try {
|
||||
const cl = fs.readFileSync(path.join(__dirname, 'CHANGELOG.md'), 'utf8');
|
||||
const cl = fs.readFileSync(path.join(APP_DIR, 'CHANGELOG.md'), 'utf8');
|
||||
const m = cl.match(/##\s*v([\d.]+)/) || cl.match(/\*\*v([\d.]+)\*\*/);
|
||||
if (m) return m[1];
|
||||
} catch {}
|
||||
try { return JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version || 'unknown'; } catch {}
|
||||
try { return JSON.parse(fs.readFileSync(path.join(APP_DIR, 'package.json'), 'utf8')).version || 'unknown'; } catch {}
|
||||
return 'unknown';
|
||||
})();
|
||||
|
||||
|
||||
36
start.sh
36
start.sh
@@ -23,6 +23,34 @@ warn() {
|
||||
printf '[cc-web pm2] WARNING: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
node_upgrade_hint() {
|
||||
cat <<'EOF'
|
||||
可选处理方式:
|
||||
# 已安装 nvm 时,使用当前 LTS 版本
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
node -v
|
||||
|
||||
# Ubuntu / Debian / WSL,可固定安装 Node.js 22;root 用户可去掉 sudo
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
node -v
|
||||
|
||||
# RHEL / Rocky / AlmaLinux 8/9,可固定安装 Node.js 22;root 用户可去掉 sudo
|
||||
curl -fsSL https://rpm.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo dnf install -y nodejs || sudo yum install -y nodejs
|
||||
node -v
|
||||
|
||||
# CentOS 7 / glibc 2.17 不适合安装 NodeSource Node.js 22。
|
||||
# 推荐在较新的构建机生成 Bun baseline 单文件发布包,再拷贝到 CentOS 7 直接运行:
|
||||
npm install
|
||||
npm run build:single-exe
|
||||
# 拷贝 dist-exe/bun-linux-x64-baseline/ 到 CentOS 7 后:
|
||||
cd dist-exe/bun-linux-x64-baseline
|
||||
PORT=8002 CC_WEB_PASSWORD='请改成强密码' ./cc-web
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_command() {
|
||||
local cmd="$1"
|
||||
local hint="$2"
|
||||
@@ -277,13 +305,17 @@ main() {
|
||||
|
||||
[[ -f "${ENTRY_PATH}" ]] || fail "找不到入口文件: ${ENTRY_PATH}"
|
||||
|
||||
ensure_command node "请先安装 Node.js 18 或更高版本。"
|
||||
ensure_command node "请先安装 Node.js 18 或更高版本。
|
||||
|
||||
$(node_upgrade_hint)"
|
||||
ensure_command npm "请先安装 npm。"
|
||||
|
||||
local node_major
|
||||
node_major="$(node -p "Number(process.versions.node.split('.')[0])")"
|
||||
if [[ "${node_major}" -lt 18 ]]; then
|
||||
fail "Node.js 版本过低,当前为 $(node -v),要求 >= 18。"
|
||||
fail "Node.js 版本过低,当前为 $(node -v),要求 >= 18。
|
||||
|
||||
$(node_upgrade_hint)"
|
||||
fi
|
||||
|
||||
check_runtime_config
|
||||
|
||||
Reference in New Issue
Block a user