#!/bin/bash # # OpenClaw + OpenViking one-click installer # Usage: curl -fsSL https://raw.githubusercontent.com/volcengine/OpenViking/main/examples/openclaw-memory-plugin/install.sh | bash # # Environment variables: # REPO=owner/repo - GitHub repository (default: volcengine/OpenViking) # BRANCH=branch - Git branch/tag/commit (default: main) # OPENVIKING_INSTALL_YES=1 - non-interactive mode (same as -y) # SKIP_OPENCLAW=1 - skip OpenClaw check # SKIP_OPENVIKING=1 - skip OpenViking installation # NPM_REGISTRY=url - npm registry (default: https://registry.npmmirror.com) # PIP_INDEX_URL=url - pip index URL (default: https://pypi.tuna.tsinghua.edu.cn/simple) # OPENVIKING_VLM_API_KEY - VLM model API key (optional) # OPENVIKING_EMBEDDING_API_KEY - Embedding model API key (optional) # OPENVIKING_ARK_API_KEY - legacy fallback for both keys # OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1 - if venv unavailable (PEP 668 only), allow pip --break-system-packages (opt-in, default off) # GET_PIP_URL=url - URL for get-pip.py when using venv --without-pip (default: auto) # # On Debian/Ubuntu (PEP 668), the script installs OpenViking into a venv at # ~/.openviking/venv to avoid "externally-managed-environment" errors. # set -e # Set by install_openviking when using venv (e.g. on Debian/Ubuntu); used by write_openviking_env OPENVIKING_PYTHON_PATH="" REPO="${REPO:-volcengine/OpenViking}" BRANCH="${BRANCH:-main}" INSTALL_YES="${OPENVIKING_INSTALL_YES:-0}" SKIP_OC="${SKIP_OPENCLAW:-0}" SKIP_OV="${SKIP_OPENVIKING:-0}" NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmmirror.com}" PIP_INDEX_URL="${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple}" HOME_DIR="${HOME:-$USERPROFILE}" OPENCLAW_DIR="${HOME_DIR}/.openclaw" OPENVIKING_DIR="${HOME_DIR}/.openviking" PLUGIN_DEST="${OPENCLAW_DIR}/extensions/memory-openviking" DEFAULT_SERVER_PORT=1933 DEFAULT_AGFS_PORT=1833 DEFAULT_VLM_MODEL="doubao-seed-2-0-pro-260215" DEFAULT_EMBED_MODEL="doubao-embedding-vision-250615" SELECTED_SERVER_PORT="${DEFAULT_SERVER_PORT}" LANG_UI="en" # Parse args (supports curl | bash -s -- ...) for arg in "$@"; do [[ "$arg" == "-y" || "$arg" == "--yes" ]] && INSTALL_YES="1" [[ "$arg" == "--zh" ]] && LANG_UI="zh" [[ "$arg" == "-h" || "$arg" == "--help" ]] && { echo "Usage: curl -fsSL | bash [-s -- -y --zh]" echo "" echo "Options:" echo " -y, --yes Non-interactive mode" echo " --zh Chinese prompts" echo " -h, --help Show this help" echo "" echo "Env vars: REPO, BRANCH, OPENVIKING_INSTALL_YES, SKIP_OPENCLAW, SKIP_OPENVIKING, NPM_REGISTRY, PIP_INDEX_URL" exit 0 } done tr() { local en="$1" local zh="$2" if [[ "$LANG_UI" == "zh" ]]; then echo "$zh" else echo "$en" fi } # Prefer interactive mode. Even with curl | bash, try reading from /dev/tty. # Fall back to defaults only when no interactive TTY is available. if [[ ! -t 0 && "$INSTALL_YES" != "1" ]]; then if [[ ! -r /dev/tty ]]; then INSTALL_YES="1" echo "[WARN] $(tr "No interactive TTY detected. Falling back to defaults (-y)." "未检测到可交互终端,自动切换为默认配置模式(等同于 -y)")" else echo "[INFO] $(tr "Pipeline execution detected. Interactive prompts will use /dev/tty." "检测到管道执行,将通过 /dev/tty 进入交互配置")" fi fi # 颜色与输出 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BOLD='\033[1m' NC='\033[0m' info() { echo -e "${GREEN}[INFO]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } err() { echo -e "${RED}[ERROR]${NC} $1"; } bold() { echo -e "${BOLD}$1${NC}"; } # Detect OS detect_os() { case "$(uname -s)" in Linux*) OS="linux";; Darwin*) OS="macos";; CYGWIN*|MINGW*|MSYS*) OS="windows";; *) OS="unknown";; esac if [[ "$OS" == "windows" ]]; then err "$(tr "Windows is not supported by this installer yet. Please follow the docs for manual setup." "Windows 暂不支持此一键安装脚本,请参考文档手动安装。")" exit 1 fi } # Detect Linux distro detect_distro() { DISTRO="unknown" if [[ -f /etc/os-release ]]; then . /etc/os-release 2>/dev/null || true case "${ID:-}" in ubuntu|debian|linuxmint) DISTRO="debian";; fedora|rhel|centos|rocky|almalinux|openeuler) DISTRO="rhel";; esac fi if command -v apt &>/dev/null; then DISTRO="debian" elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then DISTRO="rhel" fi } # ─── Environment checks ─── check_python() { local py="${OPENVIKING_PYTHON:-python3}" local out if ! out=$("$py" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null); then echo "fail|$py|$(tr "Python not found. Install Python >= 3.10." "Python 未找到,请安装 Python >= 3.10")" return 1 fi local major minor IFS=. read -r major minor <<< "$out" if [[ "$major" -lt 3 ]] || [[ "$major" -eq 3 && "$minor" -lt 10 ]]; then echo "fail|$out|$(tr "Python $out is too old. Need >= 3.10." "Python 版本 $out 过低,需要 >= 3.10")" return 1 fi echo "ok|$out|$py" return 0 } check_node() { local out if ! out=$(node -v 2>/dev/null); then echo "fail||$(tr "Node.js not found. Install Node.js >= 22." "Node.js 未找到,请安装 Node.js >= 22")" return 1 fi local v="${out#v}" local major major="${v%%.*}" if [[ -z "$major" ]] || [[ "$major" -lt 22 ]]; then echo "fail|$out|$(tr "Node.js $out is too old. Need >= 22." "Node.js 版本 $out 过低,需要 >= 22")" return 1 fi echo "ok|$out|node" return 0 } # Print guidance for missing dependencies print_install_hints() { local missing=("$@") bold "\n═══════════════════════════════════════════════════════════" bold " $(tr "Environment check failed. Install missing dependencies first:" "环境校验未通过,请先安装以下缺失组件:")" bold "═══════════════════════════════════════════════════════════\n" for item in "${missing[@]}"; do local name="${item%%|*}" local rest="${item#*|}" err "$(tr "Missing: $name" "缺失: $name")" [[ -n "$rest" ]] && echo " $rest" echo "" done detect_distro echo "$(tr "Based on your system ($DISTRO), you can run:" "根据你的系统 ($DISTRO),可执行以下命令安装:")" echo "" if printf '%s\n' "${missing[@]}" | grep -q "Python"; then echo " # $(tr "Install Python 3.10+ (pyenv recommended)" "安装 Python 3.10+(推荐 pyenv)")" echo " curl https://pyenv.run | bash" echo " export PATH=\"\$HOME/.pyenv/bin:\$PATH\"" echo " eval \"\$(pyenv init -)\"" echo " pyenv install 3.11.12" echo " pyenv global 3.11.12" echo " python3 --version # $(tr "verify >= 3.10" "确认 >= 3.10")" echo "" fi if printf '%s\n' "${missing[@]}" | grep -q "Node"; then echo " # $(tr "Install Node.js 22+ (nvm)" "安装 Node.js 22+(nvm)")" echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash" echo " source ~/.bashrc" echo " nvm install 22" echo " nvm use 22" echo " node -v # $(tr "verify >= v22" "确认 >= v22")" echo "" fi bold "$(tr "After installation, rerun this script." "安装完成后,请重新运行本脚本。")" bold "$(tr "See details: https://github.com/${REPO}/blob/${BRANCH}/examples/openclaw-memory-plugin/INSTALL.md" "详细说明见: https://github.com/${REPO}/blob/${BRANCH}/examples/openclaw-memory-plugin/INSTALL-ZH.md")" echo "" exit 1 } # Validate environment validate_environment() { info "$(tr "Checking OpenViking runtime environment..." "正在校验 OpenViking 运行环境...")" echo "" local missing=() local r r=$(check_python) || missing+=("Python 3.10+ | $(echo "$r" | cut -d'|' -f3)") if [[ "${r%%|*}" == "ok" ]]; then info " Python: $(echo "$r" | cut -d'|' -f2) ✓" fi r=$(check_node) || missing+=("Node.js 22+ | $(echo "$r" | cut -d'|' -f3)") if [[ "${r%%|*}" == "ok" ]]; then info " Node.js: $(echo "$r" | cut -d'|' -f2) ✓" fi if [[ ${#missing[@]} -gt 0 ]]; then echo "" print_install_hints "${missing[@]}" fi echo "" info "$(tr "Environment check passed ✓" "环境校验通过 ✓")" echo "" } # ─── Install flow ─── install_openclaw() { if [[ "$SKIP_OC" == "1" ]]; then info "$(tr "Skipping OpenClaw check (SKIP_OPENCLAW=1)" "跳过 OpenClaw 校验 (SKIP_OPENCLAW=1)")" return 0 fi info "$(tr "Checking OpenClaw..." "正在校验 OpenClaw...")" if command -v openclaw >/dev/null 2>&1; then info "$(tr "OpenClaw detected ✓" "OpenClaw 已安装 ✓")" return 0 fi err "$(tr "OpenClaw not found. Install it manually, then rerun this script." "未检测到 OpenClaw,请先手动安装后再执行本脚本")" echo "" echo "$(tr "Recommended command:" "推荐命令:")" echo " npm install -g openclaw --registry ${NPM_REGISTRY}" echo "" echo "$(tr "If npm global install fails, install Node via nvm and retry." "如 npm 全局安装失败,建议先用 nvm 安装 Node 后再执行上述命令。")" echo "$(tr "After installation, run:" "安装完成后,运行:")" echo " openclaw --version" echo " openclaw onboard" echo "" exit 1 } install_openviking() { if [[ "$SKIP_OV" == "1" ]]; then info "$(tr "Skipping OpenViking install (SKIP_OPENVIKING=1)" "跳过 OpenViking 安装 (SKIP_OPENVIKING=1)")" return 0 fi local py="${OPENVIKING_PYTHON:-python3}" info "$(tr "Installing OpenViking from PyPI..." "正在安装 OpenViking (PyPI)...")" info "$(tr "Using pip index: ${PIP_INDEX_URL}" "使用 pip 镜像源: ${PIP_INDEX_URL}")" # Try system-wide pip first (works on many systems) local err_out err_out=$("$py" -m pip install --upgrade pip -q -i "${PIP_INDEX_URL}" 2>&1) || true if err_out=$("$py" -m pip install openviking -i "${PIP_INDEX_URL}" 2>&1); then OPENVIKING_PYTHON_PATH="$(command -v "$py" || true)" [[ -z "$OPENVIKING_PYTHON_PATH" ]] && OPENVIKING_PYTHON_PATH="$py" info "$(tr "OpenViking installed ✓" "OpenViking 安装完成 ✓")" return 0 fi # When system has no pip, or PEP 668 (Debian/Ubuntu): use a venv if echo "$err_out" | grep -q "externally-managed-environment\|externally managed\|No module named pip"; then if echo "$err_out" | grep -q "No module named pip"; then info "$(tr "System Python has no pip. Using a venv at ~/.openviking/venv" "系统 Python 未安装 pip,将使用 ~/.openviking/venv 虚拟环境")" else # Opt-in: allow install with --break-system-packages when venv is not available (PEP 668 only, default off) if [[ "${OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES}" == "1" ]]; then info "$(tr "Installing OpenViking with --break-system-packages (OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1)" "正在以 --break-system-packages 安装 OpenViking(已设置 OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1)")" if "$py" -m pip install --break-system-packages openviking -i "${PIP_INDEX_URL}"; then OPENVIKING_PYTHON_PATH="$(command -v "$py" || true)" [[ -z "$OPENVIKING_PYTHON_PATH" ]] && OPENVIKING_PYTHON_PATH="$py" info "$(tr "OpenViking installed ✓ (system)" "OpenViking 安装完成 ✓(系统)")" return 0 fi fi info "$(tr "System Python is externally managed (PEP 668). Using a venv at ~/.openviking/venv" "检测到系统 Python 受管 (PEP 668),将使用 ~/.openviking/venv 虚拟环境")" fi mkdir -p "${OPENVIKING_DIR}" local venv_dir="${OPENVIKING_DIR}/venv" local venv_py="${venv_dir}/bin/python" # Reuse existing venv if it has openviking (avoid repeated create on re-run) if [[ -x "${venv_py}" ]] && "${venv_py}" -c "import openviking" 2>/dev/null; then info "$(tr "Using existing venv with openviking: ${venv_dir}" "复用已有虚拟环境(已装 openviking): ${venv_dir}")" "${venv_py}" -m pip install -q -U openviking -i "${PIP_INDEX_URL}" 2>/dev/null || true OPENVIKING_PYTHON_PATH="${venv_dir}/bin/python" info "$(tr "OpenViking installed ✓ (venv)" "OpenViking 安装完成 ✓(虚拟环境)")" return 0 fi local venv_ok=0 # Try 1: stdlib venv with ensurepip (needs python3-venv); errors suppressed to avoid confusing "ensurepip not available" message if "$py" -m venv "${venv_dir}" 2>/dev/null; then venv_ok=1 fi # Try 2: venv --without-pip then bootstrap pip via get-pip.py (no ensurepip needed; works when Try 1 fails) if [[ "$venv_ok" -eq 0 ]]; then rm -rf "${venv_dir}" 2>/dev/null || true info "$(tr "Creating venv without system pip, then installing pip..." "正在创建无系统 pip 的虚拟环境并安装 pip...")" if "$py" -m venv --without-pip "${venv_dir}" 2>/dev/null; then info "$(tr "Venv created without pip; bootstrapping pip (using index: ${PIP_INDEX_URL})..." "已创建无 pip 的虚拟环境,正在安装 pip(使用镜像: ${PIP_INDEX_URL})...")" local get_pip get_pip_url get_pip=$(mktemp -t get-pip.XXXXXX.py 2>/dev/null || echo "/tmp/get-pip.py") # Prefer mirror for get-pip.py when PIP_INDEX_URL is in China to avoid slow/timeout if [[ -n "${GET_PIP_URL}" ]]; then get_pip_url="${GET_PIP_URL}" elif echo "${PIP_INDEX_URL}" | grep -q "tuna.tsinghua\|pypi.tuna"; then get_pip_url="https://mirrors.tuna.tsinghua.edu.cn/pypi/web/static/get-pip.py" else get_pip_url="https://bootstrap.pypa.io/get-pip.py" fi if ! curl -fsSL --connect-timeout 15 --max-time 120 "${get_pip_url}" -o "${get_pip}" 2>/dev/null; then if [[ "${get_pip_url}" != "https://bootstrap.pypa.io/get-pip.py" ]]; then curl -fsSL --connect-timeout 15 --max-time 120 "https://bootstrap.pypa.io/get-pip.py" -o "${get_pip}" 2>/dev/null || true fi fi if [[ -s "${get_pip}" ]] && PIP_INDEX_URL="${PIP_INDEX_URL}" "$venv_py" "${get_pip}" -q 2>/dev/null; then venv_ok=1 fi rm -f "${get_pip}" 2>/dev/null || true fi fi # Try 3: virtualenv (if already installed or installable with --user) if [[ "$venv_ok" -eq 0 ]]; then rm -rf "${venv_dir}" 2>/dev/null || true if "$py" -m virtualenv "${venv_dir}" 2>/dev/null; then venv_ok=1 elif "$py" -m pip install --user virtualenv -i "${PIP_INDEX_URL}" -q 2>/dev/null && "$py" -m virtualenv "${venv_dir}" 2>/dev/null; then venv_ok=1 fi fi if [[ "$venv_ok" -eq 0 ]]; then rm -rf "${venv_dir}" 2>/dev/null || true local py_ver py_ver=$("$py" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || echo "3") err "$(tr "Could not create venv. Install venv then re-run:" "无法创建虚拟环境。请先安装 venv 后重新执行:")" echo " sudo apt install python${py_ver}-venv # or python3-full" echo "" echo "$(tr "Or (may conflict with system packages):" "或允许安装到系统(可能与系统包冲突):")" echo " OPENVIKING_ALLOW_BREAK_SYSTEM_PACKAGES=1 curl -fsSL ... | bash" exit 1 fi "$venv_py" -m pip install --upgrade pip -q -i "${PIP_INDEX_URL}" if ! "$venv_py" -m pip install openviking -i "${PIP_INDEX_URL}"; then err "$(tr "OpenViking install failed in venv." "在虚拟环境中安装 OpenViking 失败。")" exit 1 fi OPENVIKING_PYTHON_PATH="${venv_dir}/bin/python" info "$(tr "OpenViking installed ✓ (venv)" "OpenViking 安装完成 ✓(虚拟环境)")" return 0 fi err "$(tr "OpenViking install failed. Check Python version (>=3.10) and pip." "OpenViking 安装失败,请检查 Python 版本 (需 >= 3.10) 及 pip")" echo "$err_out" >&2 exit 1 } configure_openviking_conf() { mkdir -p "${OPENVIKING_DIR}" local workspace="${OPENVIKING_DIR}/data" local server_port="${DEFAULT_SERVER_PORT}" local agfs_port="${DEFAULT_AGFS_PORT}" local vlm_model="${DEFAULT_VLM_MODEL}" local embedding_model="${DEFAULT_EMBED_MODEL}" local vlm_api_key="${OPENVIKING_VLM_API_KEY:-${OPENVIKING_ARK_API_KEY:-}}" local embedding_api_key="${OPENVIKING_EMBEDDING_API_KEY:-${OPENVIKING_ARK_API_KEY:-}}" local conf_path="${OPENVIKING_DIR}/ov.conf" local vlm_api_json="null" local embedding_api_json="null" if [[ "$INSTALL_YES" != "1" ]]; then echo "" read -r -p "$(tr "OpenViking workspace path [${workspace}]: " "OpenViking 数据目录 [${workspace}]: ")" _workspace < /dev/tty || true read -r -p "$(tr "OpenViking HTTP port [${server_port}]: " "OpenViking HTTP 端口 [${server_port}]: ")" _server_port < /dev/tty || true read -r -p "$(tr "AGFS port [${agfs_port}]: " "AGFS 端口 [${agfs_port}]: ")" _agfs_port < /dev/tty || true read -r -p "$(tr "VLM model [${vlm_model}]: " "VLM 模型 [${vlm_model}]: ")" _vlm_model < /dev/tty || true read -r -p "$(tr "Embedding model [${embedding_model}]: " "Embedding 模型 [${embedding_model}]: ")" _embedding_model < /dev/tty || true echo "$(tr "VLM and Embedding API keys can differ. You can leave either empty and edit ov.conf later." "说明:VLM 与 Embedding 的 API Key 可能不同,可分别填写;留空后续可在 ov.conf 修改。")" read -r -p "$(tr "VLM API key (optional): " "VLM API Key(可留空): ")" _vlm_api_key < /dev/tty || true read -r -p "$(tr "Embedding API key (optional): " "Embedding API Key(可留空): ")" _embedding_api_key < /dev/tty || true workspace="${_workspace:-$workspace}" server_port="${_server_port:-$server_port}" agfs_port="${_agfs_port:-$agfs_port}" vlm_model="${_vlm_model:-$vlm_model}" embedding_model="${_embedding_model:-$embedding_model}" vlm_api_key="${_vlm_api_key:-$vlm_api_key}" embedding_api_key="${_embedding_api_key:-$embedding_api_key}" fi if [[ -n "${vlm_api_key}" ]]; then vlm_api_json="\"${vlm_api_key}\"" fi if [[ -n "${embedding_api_key}" ]]; then embedding_api_json="\"${embedding_api_key}\"" fi mkdir -p "${workspace}" cat > "${conf_path}" </dev/null; then ok=1 break fi [[ "$attempt" -lt "${max_retries}" ]] && sleep 2 attempt=$((attempt + 1)) done if [[ "$ok" -eq 1 ]]; then echo "✓" elif [[ "$name" == ".gitignore" ]]; then echo "$(tr "(retries failed, using minimal .gitignore)" "(重试失败,使用最小 .gitignore)")" echo "node_modules/" > "${PLUGIN_DEST}/${name}" else echo "" err "$(tr "Download failed after ${max_retries} retries: ${url}" "下载失败(已重试 ${max_retries} 次): ${url}")" exit 1 fi done info "$(tr "Installing plugin npm dependencies (may take 1-2 min, npm will show progress)..." "正在安装插件 npm 依赖(约 1–2 分钟,npm 会显示进度)...")" (cd "${PLUGIN_DEST}" && npm install --no-audit --no-fund) || { err "$(tr "Plugin dependency install failed: ${PLUGIN_DEST}" "插件依赖安装失败: ${PLUGIN_DEST}")" exit 1 } info "$(tr "Plugin deployed: ${PLUGIN_DEST}" "插件部署完成: ${PLUGIN_DEST}")" } configure_openclaw_plugin() { local server_port="${SELECTED_SERVER_PORT}" # Use absolute path so openclaw works when started from any cwd (e.g. root from /root) local config_path="${OPENVIKING_DIR}/ov.conf" info "$(tr "Configuring OpenClaw plugin..." "正在配置 OpenClaw 插件...")" openclaw config set plugins.enabled true openclaw config set plugins.allow '["memory-openviking"]' --json openclaw config set gateway.mode local openclaw config set plugins.slots.memory memory-openviking openclaw config set plugins.load.paths "[\"${PLUGIN_DEST}\"]" --json openclaw config set plugins.entries.memory-openviking.config.mode local openclaw config set plugins.entries.memory-openviking.config.configPath "${config_path}" openclaw config set plugins.entries.memory-openviking.config.port "${server_port}" openclaw config set plugins.entries.memory-openviking.config.targetUri viking://user/memories openclaw config set plugins.entries.memory-openviking.config.autoRecall true --json openclaw config set plugins.entries.memory-openviking.config.autoCapture true --json info "$(tr "OpenClaw plugin configured" "OpenClaw 插件配置完成")" } write_openviking_env() { local py_path if [[ -n "${OPENVIKING_PYTHON_PATH}" ]]; then py_path="${OPENVIKING_PYTHON_PATH}" else py_path="$(command -v python3 || command -v python || true)" fi mkdir -p "${OPENCLAW_DIR}" cat > "${OPENCLAW_DIR}/openviking.env" <