#!/bin/bash # ============================================================ # U-Claw 一键安装脚本 (Mac/Linux) # 用法: curl -fsSL https://u-claw.org/install.sh | bash # 或: bash install.sh # ============================================================ set -e set -o pipefail # 注意:未启用 `set -u` —— 第 5 步交互式分支会有意把 API_KEY/BASE_URL/KEY_LABEL 等未赋值的变量 # 用 [-z "$X"] 检测后再写配置;启用 -u 会破坏这条降级路径。改用显式 `: "${VAR:=}"` 默认值过于侵入。 # ---- 颜色定义 ---- GREEN='\033[0;32m' CYAN='\033[0;36m' RED='\033[0;31m' YELLOW='\033[1;33m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # ---- 常量 ---- UCLAW_DIR="$HOME/.uclaw" RUNTIME_DIR="$UCLAW_DIR/runtime" CORE_DIR="$UCLAW_DIR/core" DATA_DIR="$UCLAW_DIR/data" CONFIG_PATH="$DATA_DIR/.openclaw/openclaw.json" NODE_VERSION="v22.22.1" MIRROR="https://registry.npmjs.org" NODE_MIRROR="https://nodejs.org/dist" OPENCLAW_VERSION="2026.7.1-2" # Skills: content lives in skills/, described by skills/manifest.json. # UCLAW_REF pins which revision curl|bash pulls skill content from. UCLAW_LOCALE="${UCLAW_LOCALE:-en}" UCLAW_REF="${UCLAW_REF:-main}" SKILL_INSTALLER_URL="https://gitea.fanghe.it.com/zhenghy/u-claw/raw/branch/$UCLAW_REF/lib/install-skills.mjs" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo "")" # ============================================================ # Step 1: Banner + 系统检测 # ============================================================ clear 2>/dev/null || true echo "" echo -e "${CYAN}${BOLD}" cat << 'BANNER' ╔══════════════════════════════════════════╗ ║ U-Claw installer ║ ║ One command. One AI assistant. ║ ╚══════════════════════════════════════════╝ BANNER echo -e "${NC}" # 系统检测 OS=$(uname -s) ARCH=$(uname -m) if [ "$OS" = "Darwin" ]; then if [ "$ARCH" = "arm64" ]; then PLATFORM="darwin-arm64" NODE_DIR="node-mac-arm64" echo -e " System: ${GREEN}macOS Apple Silicon ✓${NC}" else PLATFORM="darwin-x64" NODE_DIR="node-mac-x64" echo -e " System: ${GREEN}macOS Intel ✓${NC}" fi elif [ "$OS" = "Linux" ]; then if [ "$ARCH" = "x86_64" ]; then PLATFORM="linux-x64" NODE_DIR="node-linux-x64" echo -e " System: ${GREEN}Linux x64 ✓${NC}" elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then PLATFORM="linux-arm64" NODE_DIR="node-linux-arm64" echo -e " System: ${GREEN}Linux ARM64 ✓${NC}" else echo -e " ${RED}U-Claw does not have a build for this processor ($ARCH).${NC}" exit 1 fi else echo -e " ${RED}U-Claw does not have a build for this operating system ($OS).${NC}" echo -e " ${YELLOW}On Windows, run this in PowerShell instead: irm https://u-claw.org/install.ps1 | iex${NC}" exit 1 fi echo -e " Installing to: ${CYAN}$UCLAW_DIR${NC}" echo "" # 检查已有安装 if [ -d "$UCLAW_DIR/core/node_modules/openclaw" ]; then echo -e " ${YELLOW}U-Claw is already installed at $UCLAW_DIR${NC}" if [ -t 0 ]; then read -p " Reinstall over it? (y/n) [y]: " -n 1 OVERWRITE echo "" if [ "$OVERWRITE" = "n" ] || [ "$OVERWRITE" = "N" ]; then echo -e " ${DIM}Cancelled.${NC}" exit 0 fi else echo -e " ${CYAN}Running non-interactively — reinstalling.${NC}" fi echo "" fi # 创建目录结构 mkdir -p "$RUNTIME_DIR" "$CORE_DIR" "$DATA_DIR/.openclaw" "$DATA_DIR/memory" "$DATA_DIR/backups" # ============================================================ # Step 2: Node.js v22 安装 # ============================================================ echo -e " ${BOLD}[1/7] Installing Node.js $NODE_VERSION${NC}" NODE_INSTALL_DIR="$RUNTIME_DIR/$NODE_DIR" INSTALL_NODE="" INSTALL_NPM="" # 检查系统 Node.js USE_SYSTEM_NODE=false if command -v node >/dev/null 2>&1; then SYS_VER=$(node --version) MAJOR=$(echo "$SYS_VER" | sed 's/v//' | cut -d. -f1) if [ "$MAJOR" -ge 20 ] 2>/dev/null; then echo -e " ${GREEN}✓${NC} Using the Node.js already on this machine ($SYS_VER)" INSTALL_NODE="$(which node)" INSTALL_NPM="$(which npm)" USE_SYSTEM_NODE=true fi fi if [ "$USE_SYSTEM_NODE" = "false" ]; then if [ -f "$NODE_INSTALL_DIR/bin/node" ]; then echo -e " ${GREEN}✓${NC} Node.js already downloaded" INSTALL_NODE="$NODE_INSTALL_DIR/bin/node" INSTALL_NPM="$NODE_INSTALL_DIR/bin/npm-cli.js" else echo -e " ${CYAN}↓${NC} Downloading Node.js $NODE_VERSION ($PLATFORM)" TARBALL="node-${NODE_VERSION}-${PLATFORM}.tar.gz" URL="${NODE_MIRROR}/${NODE_VERSION}/${TARBALL}" mkdir -p "$NODE_INSTALL_DIR" if command -v curl >/dev/null 2>&1; then curl -# -L "$URL" -o "/tmp/$TARBALL" elif command -v wget >/dev/null 2>&1; then wget -q --show-progress "$URL" -O "/tmp/$TARBALL" else echo -e " ${RED}✗ Neither curl nor wget is available.${NC}\n Install one of them, then run this again." exit 1 fi tar -xzf "/tmp/$TARBALL" -C "$NODE_INSTALL_DIR" --strip-components=1 rm -f "/tmp/$TARBALL" chmod +x "$NODE_INSTALL_DIR/bin/node" if [ -f "$NODE_INSTALL_DIR/bin/node" ]; then echo -e " ${GREEN}✓${NC} Node.js installed" INSTALL_NODE="$NODE_INSTALL_DIR/bin/node" INSTALL_NPM="$NODE_INSTALL_DIR/lib/node_modules/npm/bin/npm-cli.js" else echo -e " ${RED}✗ The Node.js download did not finish.${NC}\n Usually a dropped connection. Check your network and run this again." exit 1 fi fi fi # Helper: invoke npm. For bundled Node we have npm-cli.js (run via node). # For system Node, INSTALL_NPM is the npm shell wrapper (executable). run_npm() { if [ "$USE_SYSTEM_NODE" = "true" ]; then "$INSTALL_NPM" "$@" else "$INSTALL_NODE" "$INSTALL_NPM" "$@" fi } run_node() { if [ "$USE_SYSTEM_NODE" = "true" ]; then node "$@" else "$INSTALL_NODE" "$@" fi } echo "" # ============================================================ # Step 3: OpenClaw 安装 # ============================================================ echo -e " ${BOLD}[2/7] Installing OpenClaw${NC}" if [ -d "$CORE_DIR/node_modules/openclaw" ]; then echo -e " ${GREEN}✓${NC} OpenClaw already installed" else if [ ! -f "$CORE_DIR/package.json" ]; then cat > "$CORE_DIR/package.json" << PKGJSON { "name": "u-claw-core", "version": "1.0.0", "private": true, "dependencies": { "openclaw": "$OPENCLAW_VERSION" } } PKGJSON fi echo -e " ${CYAN}↓${NC} Downloading from the npm registry" NPM_LOG=$(mktemp /tmp/uclaw-npm.XXXXXX.log) if run_npm install --prefix "$CORE_DIR" --registry="$MIRROR" >"$NPM_LOG" 2>&1; then tail -5 "$NPM_LOG" rm -f "$NPM_LOG" echo -e " ${GREEN}✓${NC} OpenClaw installed" else echo -e " ${RED}✗ OpenClaw could not be installed. Full log: $NPM_LOG${NC}" tail -20 "$NPM_LOG" exit 1 fi fi echo "" # ============================================================ # Step 4: QQ 插件(非致命) # ============================================================ echo -e " ${BOLD}[3/7] Installing the QQ plugin${NC}" if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then echo -e " ${GREEN}✓${NC} QQ plugin already installed" else echo -e " ${CYAN}↓${NC} Installing the QQ plugin" run_npm install @sliverp/qqbot@latest --prefix "$CORE_DIR" --registry="$MIRROR" 2>/dev/null || { echo -e " ${YELLOW}⚠${NC} QQ plugin failed to install. Everything else still works." } if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then echo -e " ${GREEN}✓${NC} QQ plugin installed" fi fi echo "" # ============================================================ # Step 5: Install skills from the manifest # ============================================================ # Skill content lives in skills/ and is described by skills/manifest.json. # This script carries none of it, which is what keeps the shell and PowerShell # installers identical (tests/skills-manifest.test.mjs enforces that). echo -e " ${BOLD}[4/7] Installing skills ...${NC}" SKILLS_TARGET="$CORE_DIR/node_modules/openclaw/skills" mkdir -p "$SKILLS_TARGET" SKILL_INSTALLER="$SCRIPT_DIR/../lib/install-skills.mjs" if [ -f "$SKILL_INSTALLER" ]; then run_node "$SKILL_INSTALLER" --target "$SKILLS_TARGET" --locale "$UCLAW_LOCALE" else # curl | bash has no checkout: fetch the installer, then let it fetch content. SKILL_TMP=$(mktemp "${TMPDIR:-/tmp}/uclaw-skills.XXXXXX.mjs") if curl -fsSL "$SKILL_INSTALLER_URL" -o "$SKILL_TMP"; then run_node "$SKILL_TMP" --target "$SKILLS_TARGET" --locale "$UCLAW_LOCALE" --ref "$UCLAW_REF" rm -f "$SKILL_TMP" else rm -f "$SKILL_TMP" echo -e " ${RED}✗${NC} Could not download the skill installer — skipping skills." echo -e " You can add them later from the Skill Hub." fi fi echo "" # ============================================================ # Step 6: 交互式模型配置 # ============================================================ echo -e " ${BOLD}[5/7] Choosing a model${NC}" echo "" # 如果已有配置且包含 apiKey,跳过 if [ -f "$CONFIG_PATH" ] && grep -q '"apiKey"' "$CONFIG_PATH" 2>/dev/null; then echo -e " ${GREEN}✓${NC} A model is already configured" else # 检测是否可交互 if [ -t 0 ]; then echo -e " Which model do you want to use?" echo "" echo -e " ${GREEN}1)${NC} Gemini easiest to get a key for, free tier" echo -e " ${NC}2)${NC} Claude best all-rounder" echo -e " ${NC}3)${NC} GPT most widely used" echo -e " ${NC}4)${NC} OpenRouter one key, many models" echo -e " ${NC}5)${NC} SEA-LION Malay, Tamil and Singlish" echo -e " ${NC}6)${NC} Local runs offline via Ollama" echo "" echo -e " ${DIM}7) Other providers (DeepSeek, Kimi, Qwen, GLM, ...)${NC}" echo "" read -p " Pick a number [1]: " MODEL_CHOICE MODEL_CHOICE=${MODEL_CHOICE:-1} echo "" PROVIDER="custom" NEED_KEY=true case $MODEL_CHOICE in 1) MODEL_NAME="gemini-2.5-flash" BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai" KEY_LABEL="Gemini API key" KEY_HINT="Get one at https://aistudio.google.com/apikey — sign in with Google, no card needed" ;; 2) MODEL_NAME="claude-sonnet-4-20250514" BASE_URL="https://api.anthropic.com/v1" KEY_LABEL="Anthropic API key" KEY_HINT="Get one at https://console.anthropic.com/settings/keys" ;; 3) MODEL_NAME="gpt-4o" BASE_URL="https://api.openai.com/v1" KEY_LABEL="OpenAI API key" KEY_HINT="Get one at https://platform.openai.com/api-keys" ;; 4) MODEL_NAME="anthropic/claude-sonnet-4" BASE_URL="https://openrouter.ai/api/v1" KEY_LABEL="OpenRouter API key" KEY_HINT="Get one at https://openrouter.ai/keys" ;; 5) MODEL_NAME="aisingapore/Qwen-SEA-LION-v4.5-27B-IT" BASE_URL="https://api.sea-lion.ai/v1" KEY_LABEL="SEA-LION API key" KEY_HINT="Get one at https://playground.sea-lion.ai — free, but rate-limited to 10 requests a minute" ;; 6) MODEL_NAME="llama3.2" BASE_URL="http://127.0.0.1:11434/v1" KEY_LABEL="" KEY_HINT="Install Ollama from https://ollama.com, then run: ollama run llama3.2" NEED_KEY=false ;; 7) echo -e " Paste the endpoint and model for your provider." read -p " API endpoint (e.g. https://api.example.com/v1): " BASE_URL read -p " Model name: " MODEL_NAME KEY_LABEL="API key" KEY_HINT="Use the key your provider issued you" ;; *) echo -e " ${YELLOW}Not a listed option — using Gemini.${NC}" MODEL_NAME="gemini-2.5-flash" BASE_URL="https://generativelanguage.googleapis.com/v1beta/openai" KEY_LABEL="Gemini API key" KEY_HINT="Get one at https://aistudio.google.com/apikey" ;; esac echo "" # 输入 API Key API_KEY="" if [ "$NEED_KEY" = "true" ]; then echo -e " ${CYAN}$KEY_HINT${NC}" echo "" read -p " Paste your $KEY_LABEL: " API_KEY if [ -z "$API_KEY" ]; then echo -e " ${YELLOW}⚠ No key entered. You can add one later from Settings.${NC}" fi else echo -e " ${CYAN}$KEY_HINT${NC}" fi # 转义 API_KEY 中可能破坏 JSON 的字符 (反斜杠和双引号)。这里只处理这两个, # 因为合法的 API key 极少包含控制字符。如果用户粘贴了奇怪的内容,至少 JSON 仍然可解析。 API_KEY_JSON=${API_KEY//\\/\\\\} API_KEY_JSON=${API_KEY_JSON//\"/\\\"} # 写配置文件 if [ "$PROVIDER" = "custom" ] && [ -n "$BASE_URL" ]; then cat > "$CONFIG_PATH" << CFGEOF { "gateway": { "mode": "local", "auth": { "token": "uclaw" } }, "models": { "mode": "merge", "providers": { "custom": { "baseUrl": "$BASE_URL", "apiKey": "$API_KEY_JSON", "api": "openai-completions", "models": [{ "id": "$MODEL_NAME" }] } } }, "agents": { "defaults": { "model": { "primary": "custom/$MODEL_NAME" } } } } CFGEOF elif [ "$PROVIDER" = "anthropic" ]; then cat > "$CONFIG_PATH" << CFGEOF { "gateway": { "mode": "local", "auth": { "token": "uclaw" } }, "models": { "mode": "merge", "providers": { "anthropic": { "apiKey": "$API_KEY_JSON", "api": "anthropic", "models": [{ "id": "$MODEL_NAME" }] } } }, "agents": { "defaults": { "model": { "primary": "anthropic/$MODEL_NAME" } } } } CFGEOF elif [ "$PROVIDER" = "openai" ]; then cat > "$CONFIG_PATH" << CFGEOF { "gateway": { "mode": "local", "auth": { "token": "uclaw" } }, "models": { "mode": "merge", "providers": { "openai": { "apiKey": "$API_KEY_JSON", "api": "openai-completions", "models": [{ "id": "$MODEL_NAME" }] } } }, "agents": { "defaults": { "model": { "primary": "openai/$MODEL_NAME" } } } } CFGEOF fi echo -e " ${GREEN}✓${NC} Model set: $MODEL_NAME" else # 非交互模式,写默认配置 echo -e " ${YELLOW}⚠${NC} Running non-interactively — skipping model setup." echo -e " ${DIM}Pick a model from Settings after the first launch.${NC}" if [ ! -f "$CONFIG_PATH" ]; then cat > "$CONFIG_PATH" << 'CFGEOF' { "gateway": { "mode": "local", "auth": { "token": "uclaw" } } } CFGEOF fi fi fi echo "" # ============================================================ # Step 7: 生成启动脚本 + 验证 + 摘要 # ============================================================ echo -e " ${BOLD}[6/7] Writing the start script${NC}" # Mac 启动脚本 cat > "$UCLAW_DIR/start.command" << 'STARTEOF' #!/bin/bash DIR="$(cd "$(dirname "$0")" && pwd)" ARCH=$(uname -m) # Find Node.js if [ "$ARCH" = "arm64" ]; then NODE_BIN="$DIR/runtime/node-mac-arm64/bin/node" else NODE_BIN="$DIR/runtime/node-mac-x64/bin/node" fi [ ! -f "$NODE_BIN" ] && NODE_BIN="$(which node 2>/dev/null)" if [ -z "$NODE_BIN" ] || [ ! -f "$NODE_BIN" ]; then echo "U-Claw cannot start: the bundled Node.js is missing."; echo "Reinstall with: curl -fsSL https://u-claw.org/install.sh | bash" exit 1 fi OPENCLAW_MJS="$DIR/core/node_modules/openclaw/openclaw.mjs" if [ ! -f "$OPENCLAW_MJS" ]; then echo "U-Claw cannot start: OpenClaw is missing."; echo "Reinstall with: curl -fsSL https://u-claw.org/install.sh | bash" exit 1 fi export OPENCLAW_HOME="$DIR/data" export OPENCLAW_STATE_DIR="$DIR/data/.openclaw" export OPENCLAW_CONFIG_PATH="$DIR/data/.openclaw/openclaw.json" # Find available port PORT=18789 while lsof -i :$PORT >/dev/null 2>&1; do PORT=$((PORT + 1)) [ $PORT -gt 18799 ] && echo "No free port between 18789 and 18799 — U-Claw may already be running." && exit 1 done cd "$DIR/core" "$NODE_BIN" "$OPENCLAW_MJS" gateway run --allow-unconfigured --force --port $PORT & PID=$! # Wait for gateway to start, then open browser for i in $(seq 1 30); do sleep 0.5 if curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then open "http://127.0.0.1:$PORT/#token=uclaw" 2>/dev/null || true break fi done echo "" echo " U-Claw is running: http://127.0.0.1:$PORT/#token=uclaw" echo " Press Ctrl+C to stop." echo "" wait $PID STARTEOF chmod +x "$UCLAW_DIR/start.command" # Linux 启动脚本 if [ "$OS" = "Linux" ]; then cat > "$UCLAW_DIR/start.sh" << 'STARTEOF' #!/bin/bash DIR="$(cd "$(dirname "$0")" && pwd)" ARCH=$(uname -m) if [ "$ARCH" = "x86_64" ]; then NODE_BIN="$DIR/runtime/node-linux-x64/bin/node" elif [ "$ARCH" = "aarch64" ]; then NODE_BIN="$DIR/runtime/node-linux-arm64/bin/node" fi [ ! -f "$NODE_BIN" ] && NODE_BIN="$(which node 2>/dev/null)" OPENCLAW_MJS="$DIR/core/node_modules/openclaw/openclaw.mjs" export OPENCLAW_HOME="$DIR/data" export OPENCLAW_STATE_DIR="$DIR/data/.openclaw" export OPENCLAW_CONFIG_PATH="$DIR/data/.openclaw/openclaw.json" PORT=18789 while ss -tln | grep -q ":$PORT "; do PORT=$((PORT + 1)) [ $PORT -gt 18799 ] && echo "No free port — U-Claw may already be running." && exit 1 done cd "$DIR/core" "$NODE_BIN" "$OPENCLAW_MJS" gateway run --allow-unconfigured --force --port $PORT & PID=$! sleep 2 echo "" echo " U-Claw is running: http://127.0.0.1:$PORT/#token=uclaw" echo " Press Ctrl+C to stop." echo "" xdg-open "http://127.0.0.1:$PORT/#token=uclaw" 2>/dev/null || true wait $PID STARTEOF chmod +x "$UCLAW_DIR/start.sh" fi echo -e " ${GREEN}✓${NC} Start script written" echo "" # ============================================================ # 验证 # ============================================================ echo -e " ${BOLD}[7/7] Checking the install${NC}" echo "" # Node.js if [ -n "$INSTALL_NODE" ] && "$INSTALL_NODE" --version >/dev/null 2>&1; then NODE_VER_STR=$("$INSTALL_NODE" --version) echo -e " ${GREEN}[✓]${NC} Node.js $NODE_VER_STR" else echo -e " ${RED}[✗]${NC} Node.js" fi # OpenClaw if [ -f "$CORE_DIR/node_modules/openclaw/openclaw.mjs" ]; then echo -e " ${GREEN}[✓]${NC} OpenClaw" else echo -e " ${RED}[✗]${NC} OpenClaw" fi # QQ 插件 if [ -d "$CORE_DIR/node_modules/@sliverp/qqbot" ]; then echo -e " ${GREEN}[✓]${NC} QQ plugin" else echo -e " ${YELLOW}[⚠]${NC} QQ plugin not installed — everything else works" fi # 技能 INSTALLED_SKILLS=$(ls -d "$SKILLS_TARGET"/*/ 2>/dev/null | wc -l | tr -d ' ') echo -e " ${GREEN}[✓]${NC} Skills (${INSTALLED_SKILLS})" # 配置文件 if [ -f "$CONFIG_PATH" ]; then echo -e " ${GREEN}[✓]${NC} Config file" else echo -e " ${YELLOW}[⚠]${NC} No config yet — set it up on first launch" fi echo "" # ============================================================ # 摘要 # ============================================================ INSTALL_SIZE=$(du -sh "$UCLAW_DIR" 2>/dev/null | cut -f1) echo -e "${GREEN}${BOLD}" echo " ╔══════════════════════════════════════════╗" echo " ║ U-Claw is installed ║" echo " ╚══════════════════════════════════════════╝" echo -e "${NC}" echo -e " ${BOLD}Location:${NC} $UCLAW_DIR" echo -e " ${BOLD}Size:${NC} $INSTALL_SIZE" echo "" echo -e " ${BOLD}To start it:${NC}" if [ "$OS" = "Darwin" ]; then echo -e " Double-click ${CYAN}~/.uclaw/start.command${NC}" echo -e " or run ${CYAN}bash ~/.uclaw/start.command${NC}" else echo -e " Run ${CYAN}bash ~/.uclaw/start.sh${NC}" fi echo "" echo -e " ${BOLD}Then:${NC}" echo -e " Your browser opens and you can start talking." echo "" echo -e " ${DIM}To change model or key later, open Settings from the app.${NC}" echo -e " ${DIM}To remove it: rm -rf ~/.uclaw${NC}" echo ""