#!/usr/bin/env bash # # CodeDeck 一键安装(裸装 · 客户 VPS) # # 直接装到宿主机,不使用 Docker:因此在服务器 /root 等目录里创建项目、 # 操作文件都不受容器边界限制,也不需要额外挂载。 # # 用法: # curl -fsSL https://get.codedeck.top/install.sh | \ # DECK_TICKET='<管理端签发的票据>' \ # DECK_ADMIN_URL='https://admin.codedeck.top' bash # # 常用可选变量: # DECK_PORT=3001 对外端口 # DECK_VERSION=1.37.5 安装版本(默认取管理端可用的最新) # DECK_BUNDLE_URL=https://... 自定义服务端包地址(离线安装用) # DECK_HOME=/root/.cloudcli CodeDeck 运行数据目录(账号库等) # DECK_WORKSPACES_ROOT=/root 项目工作区根目录 # DECK_PUBLIC_ADDRESS=1.2.3.4:3001 跳过公网 IP 探测 # DECK_ADMIN_EMAIL / DECK_ADMIN_PASSWORD 指定初始账号(默认随机生成) # DECK_NODE_MAJOR=22 需要安装的 Node 大版本 # # 重复执行是安全的:会复用已有数据目录、配置与已注册的实例状态。 set -euo pipefail PORT="${DECK_PORT:-3001}" VERSION="${DECK_VERSION:-}" BUNDLE_URL="${DECK_BUNDLE_URL:-}" DECK_HOME="${DECK_HOME:-$HOME/.cloudcli}" WORKSPACES_ROOT="${DECK_WORKSPACES_ROOT:-$HOME}" NODE_MAJOR="${DECK_NODE_MAJOR:-22}" INSTALL_DIR="/opt/codedeck" ENV_DIR="/etc/codedeck" ENV_FILE="${ENV_DIR}/codedeck.env" SERVICE_FILE="/etc/systemd/system/codedeck.service" SERVICE_USER="$(id -un)" log() { printf '\033[36m[install]\033[0m %s\n' "$*"; } warn() { printf '\033[33m[warn]\033[0m %s\n' "$*"; } fail() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; } require_root() { if [ "$(id -u)" -ne 0 ]; then fail "请用 root 运行(sudo bash install.sh)" fi } install_packages() { if command -v apt-get >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get install -y curl ca-certificates tar gzip git build-essential python3 elif command -v dnf >/dev/null 2>&1; then dnf install -y curl ca-certificates tar gzip git gcc-c++ make python3 elif command -v yum >/dev/null 2>&1; then yum install -y curl ca-certificates tar gzip git gcc-c++ make python3 else fail "不支持的发行版:请手动安装 curl / tar / git / build-essential 后重试" fi } install_node() { local current_major="" if command -v node >/dev/null 2>&1; then current_major="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)" fi if [ "$current_major" = "$NODE_MAJOR" ]; then log "已安装 Node $(node -v)" return fi log "安装 Node.js ${NODE_MAJOR}.x" local arch arch="$(uname -m)" case "$arch" in x86_64|amd64) arch="x64" ;; aarch64|arm64) arch="arm64" ;; *) fail "不支持的 CPU 架构:$arch" ;; esac local setup setup="$(mktemp)" curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o "$setup" bash "$setup" rm -f "$setup" if command -v apt-get >/dev/null 2>&1; then apt-get install -y nodejs else dnf install -y nodejs || yum install -y nodejs fi command -v node >/dev/null 2>&1 || fail "Node 安装失败" log "Node 版本:$(node -v)" } open_port() { log "开放 TCP ${PORT}" if command -v ufw >/dev/null 2>&1; then ufw allow "${PORT}/tcp" >/dev/null 2>&1 || true fi if command -v firewall-cmd >/dev/null 2>&1; then firewall-cmd --permanent --add-port="${PORT}/tcp" >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 || true fi warn "如使用云厂商安全组,请同时放行 TCP ${PORT}" } detect_public_address() { if [ -n "${DECK_PUBLIC_ADDRESS:-}" ]; then echo "${DECK_PUBLIC_ADDRESS}" return fi local ip="" ip="$(curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)" [ -n "$ip" ] || ip="$(curl -fsS --max-time 5 https://ifconfig.me 2>/dev/null || true)" [ -n "$ip" ] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')" [ -n "$ip" ] || fail "无法探测公网 IP,请设置 DECK_PUBLIC_ADDRESS=1.2.3.4:${PORT}" echo "${ip}:${PORT}" } # 未显式指定版本时,从管理端票据接口拿到"推荐版本",避免让客户手填 resolve_version() { [ -n "${VERSION}" ] && return [ -n "${BUNDLE_URL}" ] && return if [ -n "${DECK_ADMIN_URL:-}" ]; then local detected detected="$(curl -fsS --max-time 8 "${DECK_ADMIN_URL%/}/health" 2>/dev/null \ | grep -o '"version":"[^"]*"' | head -1 | cut -d'"' -f4 || true)" if [ -n "${detected}" ] && [ "${detected}" != "dev" ]; then VERSION="${detected}" log "从管理端获取版本:${VERSION}" fi fi [ -n "${VERSION}" ] || fail "缺少版本号:请设置 DECK_VERSION,或提供 DECK_BUNDLE_URL" } write_env() { mkdir -p "${ENV_DIR}" chmod 700 "${ENV_DIR}" if [ -f "${ENV_FILE}" ]; then log "复用已有配置:${ENV_FILE}" # shellcheck disable=SC1090 . "${ENV_FILE}" # 已初始化过的实例不会再写安装令牌 if grep -q '^DECK_INSTALL_TOKEN=' "${ENV_FILE}"; then INSTALL_TOKEN="${DECK_INSTALL_TOKEN}" fi fi : "${JWT_SECRET:=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')}" : "${INSTALL_TOKEN:=$(openssl rand -hex 24 2>/dev/null || head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n')}" : "${INITIAL_EMAIL:=${DECK_ADMIN_EMAIL:-admin@$(hostname -f 2>/dev/null || echo localhost)}}" : "${INITIAL_PASSWORD:=${DECK_ADMIN_PASSWORD:-$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | cut -c1-16)}}" cat > "${ENV_FILE}" </dev/null; then if (cd "$(dirname "${archive}")" && sha256sum -c <(sed "s|codedeck-server-[^ ]*|codedeck-server.tar.gz|" "${archive}.sha256") >/dev/null 2>&1); then log "校验和通过" else fail "服务端包校验失败,请重试或检查下载源" fi fi log "解压到 ${INSTALL_DIR}" rm -rf "${INSTALL_DIR}" mkdir -p "${INSTALL_DIR}" tar -xzf "${archive}" -C "${INSTALL_DIR}" rm -rf "$(dirname "${archive}")" [ -f "${INSTALL_DIR}/dist-server/server/index.js" ] || fail "包内容不完整:缺少 dist-server/server/index.js" } write_service() { log "写入 systemd 服务:${SERVICE_FILE}" cat > "${SERVICE_FILE}" </dev/null 2>&1 || true } start_service() { log "启动 CodeDeck 服务" systemctl restart codedeck } wait_for_health() { log "等待服务就绪..." for _ in $(seq 1 40); do if curl -fsS --max-time 2 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then log "服务已就绪" return fi sleep 2 done journalctl -u codedeck --no-pager -n 60 || true fail "服务启动超时,请查看上面的服务日志" } # 首次初始化:服务在 127.0.0.1 上以安装令牌保护,初始化完成后移除令牌 bootstrap_account() { local status status="$(curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/api/auth/status" 2>/dev/null || echo '{}')" if printf '%s' "${status}" | grep -q '"needsSetup":true'; then log "首次初始化管理员账号" curl -fsS --max-time 15 -X POST "http://127.0.0.1:${PORT}/api/auth/register" \ -H 'Content-Type: application/json' \ -H "X-Install-Token: ${INSTALL_TOKEN}" \ -d "{\"username\":\"${INITIAL_EMAIL}\",\"password\":\"${INITIAL_PASSWORD}\"}" >/dev/null \ || fail "初始化账号失败" INITIALIZED=1 log "账号已创建" else INITIALIZED=0 log "已存在账号,跳过初始化" fi } # 初始化完成后去掉安装令牌并重启,关闭远程抢注窗口 remove_install_token() { if [ "${INITIALIZED:-0}" != "1" ]; then return fi log "移除安装令牌并重启服务" sed -i '/^DECK_INSTALL_TOKEN=/d' "${ENV_FILE}" systemctl restart codedeck wait_for_health } main() { require_root install_packages install_node open_port PUBLIC_ADDRESS="$(detect_public_address)" resolve_version mkdir -p "${DECK_HOME}" "${WORKSPACES_ROOT}" write_env install_bundle chown -R "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}" write_service # 安装令牌已写入 env 文件,仅在初始化阶段生效;初始化后移除并重启 start_service wait_for_health bootstrap_account remove_install_token echo echo "============================================================" echo " CodeDeck 安装完成" echo "------------------------------------------------------------" echo " 访问地址 : http://${PUBLIC_ADDRESS}" echo " 账号 : ${INITIAL_EMAIL}" echo " 密码 : ${INITIAL_PASSWORD}" echo " 安装目录 : ${INSTALL_DIR}" echo " 数据目录 : ${DECK_HOME}" echo " 工作区根 : ${WORKSPACES_ROOT}" echo " 配置文件 : ${ENV_FILE}" echo " 服务管理 : systemctl {status,restart,stop} codedeck" echo " 日志 : journalctl -u codedeck -f" echo "------------------------------------------------------------" echo " 请首次登录后立即修改密码;端口 ${PORT} 对公网开放," echo " 如需加密请自行配置反向代理(HTTPS)。" echo "============================================================" } main "$@"