#!/usr/bin/env bash set -Eeuo pipefail APP_NAME="${CC_WEB_PM2_NAME:-ccweb}" ENTRY_FILE="${CC_WEB_ENTRY:-server.js}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP_DIR="${SCRIPT_DIR}" ENTRY_PATH="${APP_DIR}/${ENTRY_FILE}" ENV_FILE="${APP_DIR}/.env" ENV_EXAMPLE_FILE="${APP_DIR}/.env.example" log() { printf '[cc-web pm2] %s\n' "$*" } fail() { printf '[cc-web pm2] ERROR: %s\n' "$*" >&2 exit 1 } 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" if ! command -v "${cmd}" >/dev/null 2>&1; then fail "${cmd} 未安装。${hint}" fi } get_env_value() { local key="$1" [[ -f "${ENV_FILE}" ]] || return 0 awk -v key="${key}" ' $0 ~ /^[[:space:]]*#/ { next } { line = $0 sub(/^[[:space:]]*/, "", line) if (index(line, key "=") == 1) { sub(/^[^=]*=/, "", line) print line exit } } ' "${ENV_FILE}" } set_env_value() { local key="$1" local value="$2" local tmp_file tmp_file="$(mktemp)" if [[ -f "${ENV_FILE}" ]]; then awk -v key="${key}" -v value="${value}" ' BEGIN { updated = 0 } { line = $0 sub(/^[[:space:]]*/, "", line) if (index(line, key "=") == 1) { print key "=" value updated = 1 next } print } END { if (!updated) print key "=" value } ' "${ENV_FILE}" > "${tmp_file}" else printf '%s=%s\n' "${key}" "${value}" > "${tmp_file}" fi mv "${tmp_file}" "${ENV_FILE}" chmod 600 "${ENV_FILE}" } validate_password() { local password="$1" local types=0 if [[ "${#password}" -lt 8 ]]; then printf '密码长度至少 8 位\n' return 1 fi [[ "${password}" =~ [a-z] ]] && types=$((types + 1)) [[ "${password}" =~ [A-Z] ]] && types=$((types + 1)) [[ "${password}" =~ [0-9] ]] && types=$((types + 1)) [[ "${password}" =~ [^a-zA-Z0-9] ]] && types=$((types + 1)) if [[ "${types}" -lt 2 ]]; then printf '密码需包含至少 2 种字符类型(大写/小写/数字/特殊字符)\n' return 1 fi return 0 } resolve_config_dir() { local configured configured="${CC_WEB_CONFIG_DIR:-$(get_env_value CC_WEB_CONFIG_DIR)}" if [[ -n "${configured}" ]]; then if [[ "${configured}" = /* ]]; then printf '%s\n' "${configured}" else printf '%s/%s\n' "${APP_DIR}" "${configured}" fi return fi printf '%s/config\n' "${APP_DIR}" } ensure_env_file() { if [[ -f "${ENV_FILE}" ]]; then log "检测到 .env 配置文件" chmod 600 "${ENV_FILE}" return fi if [[ -f "${ENV_EXAMPLE_FILE}" ]]; then cp "${ENV_EXAMPLE_FILE}" "${ENV_FILE}" chmod 600 "${ENV_FILE}" log "已从 .env.example 创建 .env" else : > "${ENV_FILE}" chmod 600 "${ENV_FILE}" log "已创建空 .env" fi } ensure_port_config() { local port port="$(get_env_value PORT)" if [[ -z "${port}" ]]; then set_env_value PORT "8002" log "未配置 PORT,已写入默认端口 8002" return fi log "服务端口: ${port}" } ensure_initial_password() { local config_dir auth_file env_password password confirm message config_dir="$(resolve_config_dir)" auth_file="${config_dir}/auth.json" if [[ -f "${auth_file}" ]]; then log "检测到已有登录配置: ${auth_file},跳过初始密码设置" return fi env_password="$(get_env_value CC_WEB_PASSWORD)" if [[ -n "${env_password}" && "${env_password}" != "changeme" ]]; then log "检测到 .env 已配置 CC_WEB_PASSWORD,首次启动时会迁移到 auth.json" return fi if [[ ! -t 0 ]]; then fail "首次启动缺少初始密码。请在 .env 中设置 CC_WEB_PASSWORD,或在交互终端执行 ./start.sh。" fi log "首次启动需要设置 Web 登录初始密码" while true; do read -r -s -p "请输入初始密码: " password printf '\n' read -r -s -p "请再次输入初始密码: " confirm printf '\n' if [[ "${password}" != "${confirm}" ]]; then log "两次输入不一致,请重新输入" continue fi if ! message="$(validate_password "${password}")"; then log "${message}" continue fi set_env_value CC_WEB_PASSWORD "${password}" log "初始密码已写入 .env;服务首次启动后会迁移到 config/auth.json" break done } check_agent_cli() { local key="$1" local default_cmd="$2" local label="$3" local configured configured="$(get_env_value "${key}")" configured="${configured:-${default_cmd}}" if command -v "${configured}" >/dev/null 2>&1; then log "${label} CLI: $(command -v "${configured}")" return fi log "未检测到 ${label} CLI (${configured});服务可启动,但对应 Agent 功能需要安装并登录后才能使用" } check_runtime_config() { ensure_env_file ensure_port_config ensure_initial_password check_agent_cli CLAUDE_PATH claude "Claude" check_agent_cli CODEX_PATH codex "Codex" } install_pm2() { if command -v pm2 >/dev/null 2>&1; then log "PM2 已安装: $(command -v pm2)" return fi log "未检测到 PM2,开始安装..." if npm install -g pm2; then log "PM2 安装完成" return fi if command -v sudo >/dev/null 2>&1; then log "普通 npm 全局安装失败,尝试使用 sudo 安装 PM2..." sudo npm install -g pm2 log "PM2 安装完成" return fi fail "PM2 自动安装失败,且当前环境没有 sudo。请先手动安装: npm install -g pm2" } install_dependencies() { cd "${APP_DIR}" if [[ -f package-lock.json ]]; then log "安装项目依赖: npm ci" npm ci else log "安装项目依赖: npm install" npm install fi } start_or_restart_app() { cd "${APP_DIR}" if pm2 describe "${APP_NAME}" >/dev/null 2>&1; then log "PM2 应用已存在,执行重启: ${APP_NAME}" pm2 restart "${APP_NAME}" --update-env else log "启动 PM2 应用: ${APP_NAME}" pm2 start "${ENTRY_PATH}" --name "${APP_NAME}" --cwd "${APP_DIR}" fi log "保存 PM2 进程快照" pm2 save log "当前 PM2 状态" pm2 status "${APP_NAME}" } main() { if [[ "$(id -u)" -eq 0 ]]; then warn "当前正在使用 root 用户执行。该服务会启动 Claude/Codex 子进程,root 运行风险较高,建议改用非 root 用户。" fi [[ -f "${ENTRY_PATH}" ]] || fail "找不到入口文件: ${ENTRY_PATH}" 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。 $(node_upgrade_hint)" fi check_runtime_config install_pm2 install_dependencies start_or_restart_app log "完成。若需要开机自启,请执行 pm2 startup,并按输出提示执行生成的命令。" } main "$@"