294 lines
6.7 KiB
Bash
Executable File
294 lines
6.7 KiB
Bash
Executable File
#!/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
|
||
}
|
||
|
||
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
|
||
fail "请使用非 root 用户执行。该服务会启动 Claude/Codex 子进程,root 运行风险过高。"
|
||
fi
|
||
|
||
[[ -f "${ENTRY_PATH}" ]] || fail "找不到入口文件: ${ENTRY_PATH}"
|
||
|
||
ensure_command node "请先安装 Node.js 18 或更高版本。"
|
||
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。"
|
||
fi
|
||
|
||
check_runtime_config
|
||
install_pm2
|
||
install_dependencies
|
||
start_or_restart_app
|
||
|
||
log "完成。若需要开机自启,请执行 pm2 startup,并按输出提示执行生成的命令。"
|
||
}
|
||
|
||
main "$@"
|