feat: add CentOS7 single executable build

This commit is contained in:
shiyue
2026-06-24 10:36:03 +08:00
parent a794607817
commit 67914ba10f
8 changed files with 365 additions and 26 deletions

View File

@@ -119,6 +119,29 @@ Codex App 原生协作工具会被转成页面上的子代理状态卡片:
- npm - npm
- 已安装并登录需要使用的 Agent CLI - 已安装并登录需要使用的 Agent CLI
源码方式运行需要 Node.js >= 18。Node.js 版本过低时,可先执行:
```bash
# 已安装 nvm 时,使用当前 LTS 版本
nvm install --lts
nvm use --lts
node -v
# Ubuntu / Debian / WSL可固定安装 Node.js 22root 用户可去掉 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 22root 用户可去掉 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 ```bash
npm install -g @anthropic-ai/claude-code npm install -g @anthropic-ai/claude-code
npm install -g @openai/codex npm install -g @openai/codex
@@ -162,6 +185,44 @@ npm start
http://localhost:8002 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 ### Windows
```cmd ```cmd

View File

@@ -9,7 +9,7 @@ function createAgentRuntime(deps) {
getDefaultCodexModel, getDefaultCodexModel,
loadCodexConfig, loadCodexConfig,
prepareCodexCustomRuntime, prepareCodexCustomRuntime,
ccwebMcpServerPath, ccwebMcpServerArg,
internalMcpUrl, internalMcpUrl,
internalMcpToken, internalMcpToken,
nodePath, nodePath,
@@ -31,7 +31,7 @@ function createAgentRuntime(deps) {
} }
function createCcwebMcpEnv(session, options = {}) { 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 rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0; const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
return { return {
@@ -48,7 +48,7 @@ function createAgentRuntime(deps) {
args.push( args.push(
'-c', 'mcp_servers.ccweb.type="stdio"', '-c', 'mcp_servers.ccweb.type="stdio"',
'-c', `mcp_servers.ccweb.command=${tomlString(nodePath || 'node')}`, '-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.env_vars=${tomlStringArray(envVars)}`,
'-c', 'mcp_servers.ccweb.startup_timeout_sec=10', '-c', 'mcp_servers.ccweb.startup_timeout_sec=10',
'-c', 'mcp_servers.ccweb.tool_timeout_sec=60' '-c', 'mcp_servers.ccweb.tool_timeout_sec=60'

View File

@@ -306,9 +306,7 @@ async function handleRequest(message) {
} }
} }
module.exports = { TOOLS }; function runStdioServer() {
if (require.main === module) {
let lineBuffer = ''; let lineBuffer = '';
process.stdin.setEncoding('utf8'); process.stdin.setEncoding('utf8');
@@ -334,3 +332,9 @@ if (require.main === module) {
process.exit(0); process.exit(0);
}); });
} }
module.exports = { TOOLS, runStdioServer };
if (require.main === module) {
runStdioServer();
}

View File

@@ -1,10 +1,12 @@
'use strict'; 'use strict';
const path = require('path'); const path = require('path');
const { fork } = require('child_process'); const { fork, spawn } = require('child_process');
function createCodexAppWorkerClient(options = {}) { function createCodexAppWorkerClient(options = {}) {
const workerPath = options.workerPath || path.join(__dirname, 'codex-app-worker.js'); 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 onNotification = typeof options.onNotification === 'function' ? options.onNotification : () => {};
const onServerRequest = typeof options.onServerRequest === 'function' ? options.onServerRequest : null; const onServerRequest = typeof options.onServerRequest === 'function' ? options.onServerRequest : null;
const onExit = typeof options.onExit === 'function' ? options.onExit : () => {}; const onExit = typeof options.onExit === 'function' ? options.onExit : () => {};
@@ -40,11 +42,14 @@ function createCodexAppWorkerClient(options = {}) {
workerExited = false; workerExited = false;
configured = false; configured = false;
appServerRunning = false; appServerRunning = false;
worker = fork(workerPath, [], { const spawnOptions = {
cwd: options.cwd || process.cwd(), cwd: options.cwd || process.cwd(),
env: options.env || process.env, env: options.env || process.env,
stdio: ['ignore', 'ignore', 'ignore', 'ipc'], stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
}); };
worker = workerCommand
? spawn(workerCommand, workerArgs, spawnOptions)
: fork(workerPath, [], spawnOptions);
worker.on('message', (message = {}) => { worker.on('message', (message = {}) => {
if (Object.prototype.hasOwnProperty.call(message, 'id')) { if (Object.prototype.hasOwnProperty.call(message, 'id')) {

View File

@@ -4,6 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"build:single-exe": "node scripts/build-single-exe.js",
"regression": "node scripts/regression.js" "regression": "node scripts/regression.js"
}, },
"dependencies": { "dependencies": {

203
scripts/build-single-exe.js Normal file
View 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);
}

View File

@@ -12,8 +12,37 @@ const { createCodexAppRuntime } = require('./lib/codex-app-runtime');
const { createCodexRolloutStore } = require('./lib/codex-rollouts'); const { createCodexRolloutStore } = require('./lib/codex-rollouts');
const { TOOLS: CCWEB_MCP_TOOLS } = require('./lib/ccweb-mcp-server'); 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 // Load .env
const envPath = path.join(__dirname, '.env'); const envPath = path.join(APP_DIR, '.env');
if (fs.existsSync(envPath)) { if (fs.existsSync(envPath)) {
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) { for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
const m = line.match(/^([^#=]+)=(.*)$/); const m = line.match(/^([^#=]+)=(.*)$/);
@@ -33,11 +62,10 @@ const PORT = parseInt(process.env.PORT) || 8002;
const CLAUDE_PATH = process.env.CLAUDE_PATH || 'claude'; const CLAUDE_PATH = process.env.CLAUDE_PATH || 'claude';
const CODEX_PATH = process.env.CODEX_PATH || 'codex'; 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 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 CONFIG_DIR = process.env.CC_WEB_CONFIG_DIR || path.join(APP_DIR, 'config');
const SESSIONS_DIR = process.env.CC_WEB_SESSIONS_DIR || path.join(__dirname, 'sessions'); 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(__dirname, 'public'); 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(__dirname, 'logs'); const LOGS_DIR = process.env.CC_WEB_LOGS_DIR || path.join(APP_DIR, 'logs');
const CCWEB_MCP_SERVER_PATH = path.join(__dirname, 'lib', 'ccweb-mcp-server.js');
const ATTACHMENTS_DIR = path.join(SESSIONS_DIR, '_attachments'); const ATTACHMENTS_DIR = path.join(SESSIONS_DIR, '_attachments');
const ATTACHMENT_TTL_MS = 7 * 24 * 60 * 60 * 1000; const ATTACHMENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
@@ -1789,8 +1817,8 @@ function composerSkillRoots(options = {}) {
if (codexHome) roots.push(path.join(codexHome, 'skills')); if (codexHome) roots.push(path.join(codexHome, 'skills'));
const homeDir = normalizeExistingDirPath(process.env.HOME || process.env.USERPROFILE || ''); const homeDir = normalizeExistingDirPath(process.env.HOME || process.env.USERPROFILE || '');
if (homeDir) roots.push(path.join(homeDir, '.agents', 'skills')); if (homeDir) roots.push(path.join(homeDir, '.agents', 'skills'));
roots.push(path.join(__dirname, '.codex', 'skills')); roots.push(path.join(APP_DIR, '.codex', 'skills'));
roots.push(path.join(__dirname, '.agents', 'skills')); roots.push(path.join(APP_DIR, '.agents', 'skills'));
roots.push('/etc/codex/skills'); roots.push('/etc/codex/skills');
const seen = new Set(); const seen = new Set();
return roots.filter((root) => { return roots.filter((root) => {
@@ -1899,8 +1927,8 @@ function composerPromptRoots() {
const roots = []; const roots = [];
const codexHome = getCodexHomeDir(); const codexHome = getCodexHomeDir();
if (codexHome) roots.push(path.join(codexHome, 'prompts')); if (codexHome) roots.push(path.join(codexHome, 'prompts'));
roots.push(path.join(__dirname, '.codex', 'prompts')); roots.push(path.join(APP_DIR, '.codex', 'prompts'));
roots.push(path.join(__dirname, '.agents', 'prompts')); roots.push(path.join(APP_DIR, '.agents', 'prompts'));
return roots; return roots;
} }
@@ -6613,7 +6641,7 @@ const {
getDefaultCodexModel, getDefaultCodexModel,
loadCodexConfig, loadCodexConfig,
prepareCodexCustomRuntime, prepareCodexCustomRuntime,
ccwebMcpServerPath: CCWEB_MCP_SERVER_PATH, ccwebMcpServerArg: CCWEB_MCP_SERVER_ARG,
internalMcpUrl: `http://127.0.0.1:${PORT}/api/internal/mcp`, internalMcpUrl: `http://127.0.0.1:${PORT}/api/internal/mcp`,
internalMcpToken: INTERNAL_MCP_TOKEN, internalMcpToken: INTERNAL_MCP_TOKEN,
nodePath: process.execPath, nodePath: process.execPath,
@@ -7577,7 +7605,7 @@ function codexAppTurnPermissionParams(session) {
} }
function codexAppCcwebMcpEnv(session, options = {}) { 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 rawHopCount = Number.parseInt(String(options.mcpContext?.hopCount || 0), 10);
const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0; const hopCount = Number.isFinite(rawHopCount) ? Math.max(0, rawHopCount) : 0;
return { return {
@@ -7594,7 +7622,7 @@ function codexAppThreadConfig(session, options = {}) {
return { return {
'mcp_servers.ccweb': { 'mcp_servers.ccweb': {
command: process.execPath, command: process.execPath,
args: [CCWEB_MCP_SERVER_PATH], args: [CCWEB_MCP_SERVER_ARG],
env: ccwebMcpEnv, env: ccwebMcpEnv,
startup_timeout_sec: 10, startup_timeout_sec: 10,
tool_timeout_sec: 60, tool_timeout_sec: 60,
@@ -7707,12 +7735,17 @@ function getCodexAppClient() {
onLog: (level, event, data) => plog(level, event, data), onLog: (level, event, data) => plog(level, event, data),
postInitialize: codexAppPostInitialize, postInitialize: codexAppPostInitialize,
}; };
if (IS_BUN_SINGLE_EXECUTABLE) {
clientOptions.workerCommand = process.execPath;
clientOptions.workerArgs = [CODEX_APP_WORKER_ARG];
}
codexAppClient = CODEX_APP_WORKER_ENABLED codexAppClient = CODEX_APP_WORKER_ENABLED
? createCodexAppWorkerClient(clientOptions) ? createCodexAppWorkerClient(clientOptions)
: createCodexAppServerClient(clientOptions); : createCodexAppServerClient(clientOptions);
codexAppClientSignature = signature; codexAppClientSignature = signature;
plog('INFO', 'codex_app_client_created', { plog('INFO', 'codex_app_client_created', {
worker: CODEX_APP_WORKER_ENABLED, worker: CODEX_APP_WORKER_ENABLED,
workerLauncher: IS_BUN_SINGLE_EXECUTABLE ? 'single-executable' : 'source-file',
command: path.basename(spec.command || ''), command: path.basename(spec.command || ''),
strippedEnvKeys: spec.strippedEnvKeys || [], strippedEnvKeys: spec.strippedEnvKeys || [],
}); });
@@ -8059,11 +8092,11 @@ function handleCodexAppAbortSession(sessionId, ws = null) {
function handleCheckUpdate(ws) { function handleCheckUpdate(ws) {
const localVersion = (() => { const localVersion = (() => {
try { 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.]+)\*\*/); const m = cl.match(/##\s*v([\d.]+)/) || cl.match(/\*\*v([\d.]+)\*\*/);
if (m) return m[1]; if (m) return m[1];
} catch {} } 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'; return 'unknown';
})(); })();

View File

@@ -23,6 +23,34 @@ warn() {
printf '[cc-web pm2] WARNING: %s\n' "$*" >&2 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 22root 用户可去掉 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 22root 用户可去掉 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() { ensure_command() {
local cmd="$1" local cmd="$1"
local hint="$2" local hint="$2"
@@ -277,13 +305,17 @@ main() {
[[ -f "${ENTRY_PATH}" ]] || fail "找不到入口文件: ${ENTRY_PATH}" [[ -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。" ensure_command npm "请先安装 npm。"
local node_major local node_major
node_major="$(node -p "Number(process.versions.node.split('.')[0])")" node_major="$(node -p "Number(process.versions.node.split('.')[0])")"
if [[ "${node_major}" -lt 18 ]]; then if [[ "${node_major}" -lt 18 ]]; then
fail "Node.js 版本过低,当前为 $(node -v),要求 >= 18。" fail "Node.js 版本过低,当前为 $(node -v),要求 >= 18。
$(node_upgrade_hint)"
fi fi
check_runtime_config check_runtime_config