All checks were successful
Tests / test (push) Successful in 1m8s
Sync install scripts and CI container image with NODE_VERSION, regenerate Electron Config.html, and skip gitignored portable/app in origin scan. Co-authored-by: Cursor <cursoragent@cursor.com>
633 lines
21 KiB
Bash
633 lines
21 KiB
Bash
#!/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.3"
|
||
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 ""
|