feat: add CentOS7 single executable build
This commit is contained in:
61
README.md
61
README.md
@@ -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 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
|
```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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -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
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 { 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';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
36
start.sh
36
start.sh
@@ -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 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() {
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user