mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-09-28 11:23:02 +08:00
Replace orcakit-harness-agent / harness-* with octop-harness, octop-gateway, octop-memory, and octop-browser, and align docs, UI copy, and generated paths. Co-authored-by: jubaoliang <jubaoliang@tencent.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1155 lines
43 KiB
Bash
Executable File
1155 lines
43 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Octop 安装脚本 (macOS / Linux)
|
||
# 用法: bash scripts/install.sh # 从 PyPI 安装(默认)
|
||
# 或: bash scripts/install.sh --from-source # 从本地源码安装
|
||
# 或: curl -fsSL <url>/install.sh | bash # 远程安装
|
||
#
|
||
# 将 Octop 安装到 ~/.octop,使用 uv 管理 Python 环境。
|
||
# 用户无需预先安装 Python — uv 会处理一切。
|
||
# 安装后会尽量把 octop 链接到已在 PATH 中的目录(如 /usr/local/bin),
|
||
# 当前终端无需 source / 重开即可直接使用。
|
||
set -euo pipefail
|
||
|
||
# ── 默认配置 ──────────────────────────────────────────────────────────────────
|
||
OCTOP_HOME="${OCTOP_HOME:-$HOME/.octop}"
|
||
OCTOP_VENV="$OCTOP_HOME/venv"
|
||
OCTOP_BIN="$OCTOP_HOME/bin"
|
||
PYTHON_VERSION="3.12"
|
||
OCTOP_REPO="${OCTOP_REPO:-https://github.com/TencentCloud/Octop.git}"
|
||
_OCTOP_REPO_BASE="${OCTOP_REPO%/*}"
|
||
HARNESS_AGENT_REPO="${HARNESS_AGENT_REPO:-${_OCTOP_REPO_BASE}/octop-harness.git}"
|
||
HARNESS_GATEWAY_REPO="${HARNESS_GATEWAY_REPO:-${_OCTOP_REPO_BASE}/octop-gateway.git}"
|
||
HARNESS_BROWSER_REPO="${HARNESS_BROWSER_REPO:-${_OCTOP_REPO_BASE}/octop-browser.git}"
|
||
|
||
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||
_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
_REPO_ROOT="$(cd "$_SCRIPT_DIR/.." && pwd)"
|
||
if [ -f "$_REPO_ROOT/pyproject.toml" ]; then
|
||
SOURCE_DIR="$_REPO_ROOT"
|
||
else
|
||
SOURCE_DIR=""
|
||
fi
|
||
else
|
||
SOURCE_DIR=""
|
||
fi
|
||
|
||
VERSION=""
|
||
FROM_SOURCE=false
|
||
EXTRAS=""
|
||
PYPI_MIRROR=""
|
||
|
||
# ── 颜色 ─────────────────────────────────────────────────────────────────────
|
||
if [ -t 1 ]; then
|
||
BOLD="\033[1m"
|
||
GREEN="\033[0;32m"
|
||
YELLOW="\033[0;33m"
|
||
RED="\033[0;31m"
|
||
RESET="\033[0m"
|
||
else
|
||
BOLD="" GREEN="" YELLOW="" RED="" RESET=""
|
||
fi
|
||
|
||
info() { printf "${GREEN}[octop]${RESET} %s\n" "$*"; }
|
||
warn() { printf "${YELLOW}[octop]${RESET} %s\n" "$*"; }
|
||
error() { printf "${RED}[octop]${RESET} %s\n" "$*" >&2; }
|
||
die() { error "$@"; exit 1; }
|
||
|
||
# ── 解析参数 ──────────────────────────────────────────────────────────────────
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--version)
|
||
VERSION="$2"; shift 2 ;;
|
||
--from-source)
|
||
FROM_SOURCE=true
|
||
if [[ $# -ge 2 && "$2" != --* ]]; then
|
||
SOURCE_DIR="$(cd "$2" && pwd)" || die "Directory not found: $2"
|
||
shift
|
||
fi
|
||
shift ;;
|
||
--from-pypi|--pypi)
|
||
FROM_SOURCE=false
|
||
SOURCE_DIR=""
|
||
shift ;;
|
||
--extras)
|
||
EXTRAS="$2"; shift 2 ;;
|
||
--mirror)
|
||
PYPI_MIRROR="$2"; shift 2 ;;
|
||
-h|--help)
|
||
cat <<EOF
|
||
Octop installer (macOS / Linux)
|
||
|
||
Usage: bash install.sh [OPTIONS]
|
||
|
||
Options:
|
||
--version <VER> Install a specific version (e.g. 0.1.0) [PyPI only]
|
||
--from-source [DIR] Install from source; clones the git repo if DIR is omitted
|
||
--from-pypi Install from PyPI (default)
|
||
--extras <EXTRAS> Extra optional components (e.g. desktop, browser)
|
||
--mirror <URL> Use a specific PyPI mirror (e.g. https://mirrors.cloud.tencent.com/pypi/simple)
|
||
-h, --help Show this help
|
||
|
||
Note: Playwright Chromium is not downloaded by default. Pass
|
||
--extras browser to install it, or install later from the dashboard.
|
||
If a system Chrome/Chromium already exists, that download is skipped
|
||
even with --extras browser.
|
||
|
||
Environment variables:
|
||
OCTOP_HOME Install directory (default: ~/.octop)
|
||
OCTOP_PYPI_MIRROR PyPI mirror URL (same as --mirror)
|
||
OCTOP_REPO Git URL to clone (used by --from-source with no local dir)
|
||
HARNESS_AGENT_REPO octop-harness repo (derived from OCTOP_REPO by default)
|
||
HARNESS_GATEWAY_REPO octop-gateway repo (derived from OCTOP_REPO by default)
|
||
HARNESS_BROWSER_REPO octop-browser repo (used for source installs)
|
||
PLAYWRIGHT_DOWNLOAD_HOST Playwright download mirror (optional; auto: npmmirror -> official)
|
||
PLAYWRIGHT_INSTALL_TIMEOUT Per-mirror download timeout in seconds (default 600)
|
||
|
||
Details:
|
||
Everything is installed into an isolated virtualenv (~/.octop/venv), so the
|
||
system Python is untouched. Playwright Chromium is opt-in (--extras browser):
|
||
1. Playwright Chromium browser (skipped if a system browser exists)
|
||
2. System libraries (Linux: apt/dnf/yum/pacman/zypper; only needed for Chromium)
|
||
3. CJK fonts for rendering Chinese web pages
|
||
EOF
|
||
exit 0 ;;
|
||
*)
|
||
die "Unknown option: $1 (try --help)" ;;
|
||
esac
|
||
done
|
||
|
||
# ── 操作系统检查 ──────────────────────────────────────────────────────────────
|
||
OS="$(uname -s)"
|
||
case "$OS" in
|
||
Linux|Darwin) ;;
|
||
*) die "Unsupported OS: $OS. Use install.ps1 or install.bat on Windows." ;;
|
||
esac
|
||
|
||
printf "${GREEN}[octop]${RESET} Installing Octop into ${BOLD}%s${RESET}\n" "$OCTOP_HOME"
|
||
|
||
# ── 步骤 1: 确保 uv 可用 ────────────────────────────────────────────────────
|
||
_install_uv_via_pip() {
|
||
local py_bin=""
|
||
for candidate in python3 python; do
|
||
if command -v "$candidate" &>/dev/null; then
|
||
py_bin="$candidate"
|
||
break
|
||
fi
|
||
done
|
||
[ -z "$py_bin" ] && return 1
|
||
|
||
local install_dir="$HOME/.local/bin"
|
||
mkdir -p "$install_dir"
|
||
|
||
# 不使用清华/中科大镜像:部分环境拉 wheel 会 302 到 TUNA 并返回 403
|
||
local mirrors=(
|
||
"https://mirrors.cloud.tencent.com/pypi/simple"
|
||
"https://mirrors.aliyun.com/pypi/simple"
|
||
)
|
||
for mirror in "${mirrors[@]}"; do
|
||
local host
|
||
host="$(echo "$mirror" | awk -F/ '{print $3}')"
|
||
info "Trying uv from PyPI mirror: $mirror"
|
||
"$py_bin" -m pip install -q uv \
|
||
--break-system-packages \
|
||
-i "$mirror" --trusted-host "$host" 2>/dev/null || \
|
||
"$py_bin" -m pip install -q uv --user \
|
||
--break-system-packages \
|
||
-i "$mirror" --trusted-host "$host" 2>/dev/null || \
|
||
"$py_bin" -m pip install -q uv \
|
||
-i "$mirror" --trusted-host "$host" 2>/dev/null || \
|
||
"$py_bin" -m pip install -q uv --user \
|
||
-i "$mirror" --trusted-host "$host" 2>/dev/null || true
|
||
|
||
local uv_bin
|
||
uv_bin="$("$py_bin" -c 'from uv._find_uv import find_uv_bin; print(find_uv_bin())' 2>/dev/null)" || true
|
||
if [ -z "$uv_bin" ] || [ ! -x "$uv_bin" ]; then
|
||
uv_bin="$("$py_bin" -c '
|
||
import sysconfig, os, sys
|
||
for p in [
|
||
sysconfig.get_path("scripts"),
|
||
sysconfig.get_path("scripts", vars={"base": sys.base_prefix}),
|
||
sysconfig.get_path("scripts", scheme="posix_user"),
|
||
os.path.expanduser("~/.local/bin"),
|
||
"/usr/local/bin",
|
||
]:
|
||
if p and os.path.isfile(os.path.join(p, "uv")):
|
||
print(os.path.join(p, "uv"))
|
||
break
|
||
' 2>/dev/null)" || true
|
||
fi
|
||
|
||
if [ -n "$uv_bin" ] && [ -x "$uv_bin" ]; then
|
||
[ "$uv_bin" != "$install_dir/uv" ] && \
|
||
{ ln -sf "$uv_bin" "$install_dir/uv" 2>/dev/null || cp "$uv_bin" "$install_dir/uv"; }
|
||
chmod +x "$install_dir/uv"
|
||
export PATH="$install_dir:$PATH"
|
||
command -v uv &>/dev/null && return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
_install_uv_via_astral() {
|
||
info "Trying the official uv installer..."
|
||
if curl -LsSf --connect-timeout 20 https://astral.sh/uv/install.sh 2>/dev/null | sh 2>/dev/null; then
|
||
if [ -f "$HOME/.local/bin/env" ]; then
|
||
# shellcheck disable=SC1091
|
||
. "$HOME/.local/bin/env" 2>/dev/null || true
|
||
fi
|
||
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
|
||
command -v uv &>/dev/null && return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
ensure_uv() {
|
||
if command -v uv &>/dev/null; then
|
||
info "Found uv: $(command -v uv)"
|
||
return
|
||
fi
|
||
for candidate in "$HOME/.local/bin/uv" "$HOME/.cargo/bin/uv"; do
|
||
if [ -x "$candidate" ]; then
|
||
export PATH="$(dirname "$candidate"):$PATH"
|
||
info "Found uv: $candidate"
|
||
return
|
||
fi
|
||
done
|
||
|
||
info "Installing uv..."
|
||
if _install_uv_via_pip; then
|
||
command -v uv &>/dev/null && { info "uv installed successfully (via PyPI)"; return; }
|
||
fi
|
||
if _install_uv_via_astral; then
|
||
command -v uv &>/dev/null && { info "uv installed successfully (via astral.sh)"; return; }
|
||
fi
|
||
die "Failed to install uv. Run manually: pip3 install uv -i https://mirrors.cloud.tencent.com/pypi/simple"
|
||
}
|
||
|
||
ensure_uv
|
||
|
||
# ── 选择最快的 PyPI 镜像 ──────────────────────────────────────────────────────
|
||
# 仅保留实测可用的国内源。清华 TUNA / 中科大 USTC 在部分网络下拉 wheel
|
||
# 会 302 到 TUNA 并 403,导致 uv pip install 失败,故不再作为候选。
|
||
_PYPI_MIRRORS=(
|
||
"https://mirrors.cloud.tencent.com/pypi/simple"
|
||
"https://mirrors.aliyun.com/pypi/simple"
|
||
)
|
||
_FASTEST_MIRROR=""
|
||
_select_fastest_pypi_mirror() {
|
||
local best_mirror="${_PYPI_MIRRORS[0]}"
|
||
local best_time=9999
|
||
local found=0
|
||
info "Benchmarking PyPI mirrors..."
|
||
for mirror in "${_PYPI_MIRRORS[@]}"; do
|
||
local t t_ms code
|
||
# 同时校验 HTTP 状态:连通快但返回非 2xx 的源不可用
|
||
code="$(curl -o /dev/null -s -w '%{http_code}' \
|
||
--connect-timeout 3 --max-time 5 \
|
||
"$mirror/pip/" 2>/dev/null || echo '000')"
|
||
case "$code" in
|
||
2*) ;;
|
||
*)
|
||
warn "Mirror unavailable (HTTP $code), skipping: $mirror"
|
||
continue
|
||
;;
|
||
esac
|
||
t="$(curl -o /dev/null -s -w '%{time_total}' \
|
||
--connect-timeout 3 --max-time 5 \
|
||
"$mirror/pip/" 2>/dev/null || echo '9999')"
|
||
t_ms="$(echo "$t" | awk '{printf "%d", $1*1000}')"
|
||
if [ "$t_ms" -lt "$best_time" ] 2>/dev/null; then
|
||
best_time="$t_ms"
|
||
best_mirror="$mirror"
|
||
found=1
|
||
fi
|
||
done
|
||
if [ "$found" -eq 0 ]; then
|
||
warn "No usable mirror found; using official PyPI only"
|
||
_FASTEST_MIRROR=""
|
||
return
|
||
fi
|
||
info "Fastest mirror: $best_mirror (${best_time}ms)"
|
||
_FASTEST_MIRROR="$best_mirror"
|
||
}
|
||
|
||
# 按候选镜像依次尝试 uv pip install;失败则换源,最后回退官方 PyPI。
|
||
# 参数:包规格(可含 extras) + 额外 uv 参数(如 --prerelease=explicit)
|
||
_uv_pip_install_with_mirror_fallback() {
|
||
local package="$1"
|
||
shift
|
||
local -a candidates=()
|
||
local m
|
||
|
||
if [ -n "${_EXTRA_MIRROR:-}" ]; then
|
||
candidates+=("$_EXTRA_MIRROR")
|
||
fi
|
||
for m in "${_PYPI_MIRRORS[@]}"; do
|
||
if [ -n "$m" ] && [ "$m" != "${_EXTRA_MIRROR:-}" ]; then
|
||
candidates+=("$m")
|
||
fi
|
||
done
|
||
candidates+=("") # 官方 PyPI only
|
||
|
||
local mirror
|
||
local -a seen=()
|
||
for mirror in "${candidates[@]}"; do
|
||
local dup=0 s
|
||
for s in "${seen[@]+"${seen[@]}"}"; do
|
||
[ "$s" = "$mirror" ] && { dup=1; break; }
|
||
done
|
||
[ "$dup" -eq 1 ] && continue
|
||
seen+=("$mirror")
|
||
|
||
local -a args=(
|
||
--python "$OCTOP_VENV/bin/python"
|
||
--quiet
|
||
--index-url https://pypi.org/simple
|
||
)
|
||
if [ -n "$mirror" ]; then
|
||
args+=(--extra-index-url "$mirror")
|
||
info "Trying dependency mirror: $mirror"
|
||
else
|
||
info "Trying official PyPI only (no mirror)..."
|
||
fi
|
||
|
||
if UV_SYSTEM_PYTHON=0 uv pip install "$package" "${args[@]}" "$@"; then
|
||
_EXTRA_MIRROR="$mirror"
|
||
return 0
|
||
fi
|
||
warn "Install failed from this index, trying the next mirror..."
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# ── glibc / 旧发行版:部分依赖(如 tiktoken)仅提供 manylinux_2_28+ wheel ──
|
||
_glibc_major_minor() {
|
||
# 输出如 2.17;非 Linux / 无法检测时返回空
|
||
[ "$OS" = "Linux" ] || { echo ""; return; }
|
||
local ver
|
||
ver="$(ldd --version 2>&1 | head -n1 | awk '{print $NF}')"
|
||
echo "$ver"
|
||
}
|
||
|
||
_glibc_too_old_for_wheels() {
|
||
# manylinux_2_28 要求 glibc >= 2.28(CentOS 7 = 2.17)
|
||
local ver
|
||
ver="$(_glibc_major_minor)"
|
||
[ -n "$ver" ] || return 1
|
||
local major minor
|
||
major="${ver%%.*}"
|
||
minor="${ver#*.}"
|
||
minor="${minor%%.*}"
|
||
[ "$major" -lt 2 ] 2>/dev/null && return 0
|
||
[ "$major" -eq 2 ] && [ "$minor" -lt 28 ] 2>/dev/null && return 0
|
||
return 1
|
||
}
|
||
|
||
_ensure_rustc() {
|
||
if command -v rustc &>/dev/null; then
|
||
info "Found rustc: $(command -v rustc) ($(rustc --version 2>/dev/null | awk '{print $2}'))"
|
||
return 0
|
||
fi
|
||
for candidate in "$HOME/.cargo/bin/rustc"; do
|
||
if [ -x "$candidate" ]; then
|
||
export PATH="$(dirname "$candidate"):$PATH"
|
||
info "Found rustc: $candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
info "Installing the Rust toolchain (rustup) to build some deps from source..."
|
||
if curl -LsSf --connect-timeout 30 https://sh.rustup.rs 2>/dev/null | sh -s -- -y --default-toolchain stable 2>/dev/null; then
|
||
# shellcheck disable=SC1091
|
||
. "$HOME/.cargo/env" 2>/dev/null || export PATH="$HOME/.cargo/bin:$PATH"
|
||
command -v rustc &>/dev/null && return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
# 以 root 或免密 sudo 执行包管理命令(CentOS 常用 root 登录,不可强制 sudo)
|
||
_sudo_nopass() {
|
||
command -v sudo &>/dev/null && sudo -n true 2>/dev/null
|
||
}
|
||
|
||
_run_as_root() {
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
"$@"
|
||
elif _sudo_nopass; then
|
||
sudo "$@"
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 当前将用于编译扩展的 Python 是否带有 Python.h(uv 自带或系统 python3-dev)
|
||
_python_dev_headers_ok() {
|
||
local py=""
|
||
if [ -x "${OCTOP_VENV:-}/bin/python" ]; then
|
||
py="$OCTOP_VENV/bin/python"
|
||
elif command -v python3 &>/dev/null; then
|
||
py="$(command -v python3)"
|
||
else
|
||
return 1
|
||
fi
|
||
"$py" -c 'import sysconfig, os
|
||
p = sysconfig.get_path("include")
|
||
raise SystemExit(0 if p and os.path.isfile(os.path.join(p, "Python.h")) else 1)' 2>/dev/null
|
||
}
|
||
|
||
# gcc + Python 头文件:evdev/pynput 等在无匹配 wheel 时需本地编译。
|
||
# 注意:仅有 gcc、缺 python3-dev 时(常见于 Ubuntu 云镜像)也会失败,不可因已有 gcc 而跳过。
|
||
_ensure_c_build_tools() {
|
||
local have_cc=0 have_pyh=0
|
||
if command -v cc &>/dev/null || command -v gcc &>/dev/null; then
|
||
have_cc=1
|
||
fi
|
||
if _python_dev_headers_ok; then
|
||
have_pyh=1
|
||
fi
|
||
if [ "$have_cc" -eq 1 ] && [ "$have_pyh" -eq 1 ]; then
|
||
return 0
|
||
fi
|
||
|
||
info "Installing local build dependencies (gcc / Python dev headers)..."
|
||
if command -v apt-get &>/dev/null; then
|
||
_run_as_root env DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null || true
|
||
_run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||
build-essential python3-dev 2>/dev/null || true
|
||
elif command -v dnf &>/dev/null; then
|
||
_run_as_root dnf install -y \
|
||
gcc gcc-c++ make openssl-devel libffi-devel python3-devel 2>/dev/null || true
|
||
elif command -v yum &>/dev/null; then
|
||
_run_as_root yum install -y \
|
||
gcc gcc-c++ make openssl-devel libffi-devel python3-devel 2>/dev/null || true
|
||
fi
|
||
|
||
have_cc=0
|
||
if command -v cc &>/dev/null || command -v gcc &>/dev/null; then
|
||
have_cc=1
|
||
fi
|
||
have_pyh=0
|
||
if _python_dev_headers_ok; then
|
||
have_pyh=1
|
||
fi
|
||
[ "$have_cc" -eq 1 ] || return 1
|
||
# 系统 python3-devel 可能与 uv 拉取的 Python 小版本不一致;uv 自带解释器通常自带头文件。
|
||
# 若仍缺失,后续源码编译可能失败,由 pip 错误提示即可。
|
||
return 0
|
||
}
|
||
|
||
_cxx_major() {
|
||
local v
|
||
v="$(${CXX:-g++} -dumpversion 2>/dev/null || true)"
|
||
echo "${v%%.*}"
|
||
}
|
||
|
||
_fix_centos7_scl_repos() {
|
||
# CentOS 7 EOL:mirrorlist.centos.org 已失效,改用 vault
|
||
local f
|
||
for f in /etc/yum.repos.d/CentOS-SCLo*.repo; do
|
||
[ -f "$f" ] || continue
|
||
sed -i \
|
||
-e 's|^mirrorlist=|#mirrorlist=|g' \
|
||
-e 's|^#[[:space:]]*baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' \
|
||
-e 's|^baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' \
|
||
"$f" 2>/dev/null || true
|
||
done
|
||
}
|
||
|
||
_ensure_modern_cxx() {
|
||
# playwright → greenlet、numpy 2.x 等需较新 C++;CentOS 7 自带 gcc 4.8 不够
|
||
# numpy>=2.5 要求 GCC >= 10.3,故优先 devtoolset-11
|
||
local major
|
||
major="$(_cxx_major)"
|
||
if [ -n "$major" ] && [ "$major" -ge 10 ] 2>/dev/null; then
|
||
info "C++ compiler ready: $(${CXX:-g++} --version 2>/dev/null | head -n1)"
|
||
return 0
|
||
fi
|
||
|
||
if ! command -v yum &>/dev/null; then
|
||
warn "System gcc is too old and yum is unavailable; building greenlet/numpy may fail"
|
||
return 1
|
||
fi
|
||
|
||
if [ "$(id -u)" -ne 0 ] && ! _sudo_nopass; then
|
||
warn "Installing devtoolset requires root/sudo"
|
||
return 1
|
||
fi
|
||
|
||
_run_as_root yum install -y centos-release-scl 2>/dev/null || true
|
||
_fix_centos7_scl_repos
|
||
|
||
local dts enable_file=""
|
||
for dts in 11 10 9; do
|
||
info "System gcc is too old (numpy needs >=10.3), installing devtoolset-${dts}..."
|
||
if _run_as_root yum install -y \
|
||
"devtoolset-${dts}-gcc" "devtoolset-${dts}-gcc-c++" make 2>/dev/null; then
|
||
if [ -f "/opt/rh/devtoolset-${dts}/enable" ]; then
|
||
enable_file="/opt/rh/devtoolset-${dts}/enable"
|
||
break
|
||
fi
|
||
fi
|
||
warn "devtoolset-${dts} install failed, trying the next version..."
|
||
done
|
||
|
||
if [ -z "$enable_file" ]; then
|
||
warn "Could not install a usable devtoolset; greenlet/numpy may fail to build"
|
||
return 1
|
||
fi
|
||
|
||
# enable 脚本会读未定义的 MANPATH;临时关闭 nounset
|
||
set +u
|
||
# shellcheck disable=SC1091
|
||
. "$enable_file"
|
||
set -u
|
||
export CC=gcc CXX=g++
|
||
info "Enabled $(basename "$(dirname "$enable_file")"): $(g++ --version 2>/dev/null | head -n1)"
|
||
major="$(_cxx_major)"
|
||
if [ -n "$major" ] && [ "$major" -ge 10 ] 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
# gcc 9 仍可能编 greenlet,但编不了新版 numpy
|
||
warn "Current g++ major version is ${major:-?} (numpy 2.5+ needs >=10)"
|
||
return 0
|
||
}
|
||
|
||
_ensure_old_glibc_image_libs() {
|
||
# Pillow 等在无 manylinux_2_28 wheel 时需源码编译,依赖 jpeg/zlib/freetype 头文件
|
||
info "Installing image library headers (needed to build Pillow from source)..."
|
||
if command -v apt-get &>/dev/null; then
|
||
_run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||
libjpeg-dev zlib1g-dev libfreetype6-dev libtiff-dev libwebp-dev \
|
||
liblcms2-dev libopenjp2-7-dev 2>/dev/null || true
|
||
elif command -v dnf &>/dev/null; then
|
||
_run_as_root dnf install -y \
|
||
libjpeg-turbo-devel zlib-devel freetype-devel libtiff-devel \
|
||
libwebp-devel lcms2-devel openjpeg2-devel 2>/dev/null || true
|
||
elif command -v yum &>/dev/null; then
|
||
_run_as_root yum install -y \
|
||
libjpeg-turbo-devel zlib-devel freetype-devel libtiff-devel \
|
||
libwebp-devel lcms2-devel openjpeg2-devel 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
_ensure_old_glibc_build_toolchain() {
|
||
# CentOS 7 / 旧 RHEL:无 manylinux_2_28 wheel 时需源码编译 Rust/C++ 扩展
|
||
if ! _glibc_too_old_for_wheels; then
|
||
return 0
|
||
fi
|
||
local ver
|
||
ver="$(_glibc_major_minor)"
|
||
warn "Detected glibc ${ver} (< 2.28, e.g. CentOS 7): some deps must be built locally"
|
||
_ensure_c_build_tools || warn "gcc not found; building from source may fail"
|
||
_ensure_old_glibc_image_libs
|
||
_ensure_modern_cxx || warn "No modern g++ enabled; the playwright dep (greenlet) may fail to build"
|
||
if ! _ensure_rustc; then
|
||
die "glibc=$ver on this system: prebuilt wheels are unusable and installing Rust failed. Upgrade to CentOS/RHEL 8+ or Ubuntu 20.04+, or install rustup manually and retry."
|
||
fi
|
||
}
|
||
|
||
# ── 步骤 2: 创建/更新虚拟环境 ────────────────────────────────────────────────
|
||
if [ -x "$OCTOP_VENV/bin/python" ]; then
|
||
info "Existing environment found, upgrading..."
|
||
else
|
||
info "Creating the Python $PYTHON_VERSION environment..."
|
||
uv venv "$OCTOP_VENV" --python "$PYTHON_VERSION" --quiet --seed
|
||
fi
|
||
[ -x "$OCTOP_VENV/bin/python" ] || die "Failed to create the virtualenv"
|
||
info "Python environment ready ($("$OCTOP_VENV/bin/python" --version))"
|
||
|
||
# Linux 上始终确保可编译本地扩展(Ubuntu 缺 python3-dev、CentOS 缺 python3-devel 等)
|
||
if [ "$OS" = "Linux" ]; then
|
||
_ensure_c_build_tools || warn "gcc / Python headers are incomplete; deps needing a source build may fail"
|
||
fi
|
||
_ensure_old_glibc_build_toolchain
|
||
|
||
# ── 步骤 3: 安装 Octop ───────────────────────────────────────────────────────
|
||
# playwright Python 包已是核心依赖;Chromium 浏览器仅在 --extras browser 时下载。
|
||
_merge_install_extras() {
|
||
local result="browser"
|
||
if [ -n "$EXTRAS" ]; then
|
||
local IFS=','
|
||
local part
|
||
for part in $EXTRAS; do
|
||
case "$part" in
|
||
""|browser|channels-feishu) continue ;;
|
||
*) result="${result},${part}" ;;
|
||
esac
|
||
done
|
||
fi
|
||
echo "$result"
|
||
}
|
||
EXTRAS_MERGED="$(_merge_install_extras)"
|
||
EXTRAS_SUFFIX="[$EXTRAS_MERGED]"
|
||
|
||
_EXTRA_MIRROR="${PYPI_MIRROR:-${OCTOP_PYPI_MIRROR:-}}"
|
||
if [ -z "$_EXTRA_MIRROR" ]; then
|
||
_select_fastest_pypi_mirror
|
||
_EXTRA_MIRROR="$_FASTEST_MIRROR"
|
||
else
|
||
info "Using the specified mirror: $_EXTRA_MIRROR"
|
||
fi
|
||
|
||
_CONSOLE_AVAILABLE=0
|
||
prepare_console() {
|
||
local repo_dir="$1"
|
||
local console_dest="$repo_dir/src/octop/dashboard"
|
||
|
||
if [ -f "$console_dest/index.html" ]; then
|
||
_CONSOLE_AVAILABLE=1
|
||
return
|
||
fi
|
||
|
||
if [ ! -f "$repo_dir/dashboard/package.json" ]; then
|
||
warn "Frontend source not found - the Web UI will be unavailable."
|
||
return
|
||
fi
|
||
|
||
if ! command -v npm &>/dev/null; then
|
||
warn "npm not found - skipping the frontend build."
|
||
warn "Install Node.js and re-run, or build manually: cd dashboard && npm ci && npm run build"
|
||
return
|
||
fi
|
||
|
||
info "Building the frontend (npm ci && npm run build)..."
|
||
(cd "$repo_dir/dashboard" && npm ci && npm run build)
|
||
if [ -f "$console_dest/index.html" ]; then
|
||
_CONSOLE_AVAILABLE=1
|
||
info "Frontend build succeeded"
|
||
else
|
||
warn "Frontend build finished but index.html is missing - the Web UI will be unavailable."
|
||
fi
|
||
}
|
||
|
||
# TEMP: mcp 2.x 移除 RequestContext,与 langchain-mcp-adapters 不兼容。
|
||
# octop-harness>=0.9.18 已在依赖中 pin;此处在验证前再钉一次,覆盖仍拉取到
|
||
# 旧版 harness / 镜像滞后的安装路径。待 Octop 发版跟上后可删除。
|
||
_pin_mcp_compat() {
|
||
info "Pinning mcp<2 (langchain-mcp-adapters compatibility; temporary)..."
|
||
if ! _uv_pip_install_with_mirror_fallback "mcp>=1.27.1,<2"; then
|
||
warn "Failed to pin mcp; install verification may fail"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
_verify_install() {
|
||
info "Verifying the installation..."
|
||
if ! "$OCTOP_VENV/bin/python" -c "from octop.infra.agents.manager import AgentManager" 2>/dev/null; then
|
||
die "Verification failed: core modules cannot be imported. Check dependency versions or re-run the installer."
|
||
fi
|
||
info "Installation verified"
|
||
}
|
||
|
||
_clone_source_workspace() {
|
||
local workdir="$1"
|
||
command -v git &>/dev/null || die "git is required to clone the repos. Install git or use --from-pypi."
|
||
mkdir -p "$workdir"
|
||
info "Cloning octop-harness / octop-gateway / Octop sources..."
|
||
git clone --depth 1 "$HARNESS_AGENT_REPO" "$workdir/octop-harness"
|
||
git clone --depth 1 "$HARNESS_GATEWAY_REPO" "$workdir/octop-gateway"
|
||
git clone --depth 1 "$HARNESS_BROWSER_REPO" "$workdir/octop-browser"
|
||
git clone --depth 1 "$OCTOP_REPO" "$workdir/orca"
|
||
}
|
||
|
||
if [ "$FROM_SOURCE" = true ]; then
|
||
if [ -n "$SOURCE_DIR" ]; then
|
||
info "Installing from local source: $SOURCE_DIR"
|
||
prepare_console "$SOURCE_DIR"
|
||
uv pip install "${SOURCE_DIR}${EXTRAS_SUFFIX}" --python "$OCTOP_VENV/bin/python"
|
||
else
|
||
INSTALL_WORKDIR="$(mktemp -d)"
|
||
trap 'rm -rf "$INSTALL_WORKDIR"' EXIT
|
||
_clone_source_workspace "$INSTALL_WORKDIR"
|
||
REPO_DIR="$INSTALL_WORKDIR/orca"
|
||
prepare_console "$REPO_DIR"
|
||
uv pip install "${REPO_DIR}${EXTRAS_SUFFIX}" --python "$OCTOP_VENV/bin/python"
|
||
fi
|
||
else
|
||
# PEP 508: extras 必须在包名与版本说明符之间(octop[browser]==x.y.z),
|
||
# 不能拼在版本号后面(octop==x.y.z[browser] 是非法需求串,uv/pip 解析报错)。
|
||
PACKAGE="octop${EXTRAS_SUFFIX}"
|
||
[ -n "$VERSION" ] && PACKAGE="octop${EXTRAS_SUFFIX}==$VERSION"
|
||
|
||
info "Installing ${PACKAGE} from PyPI..."
|
||
if [ -n "${_EXTRA_MIRROR:-}" ]; then
|
||
info "Primary index: https://pypi.org/simple preferred mirror: $_EXTRA_MIRROR"
|
||
else
|
||
info "Primary index: https://pypi.org/simple"
|
||
fi
|
||
_PYPI_EXTRA_ARGS=()
|
||
if [ -n "$VERSION" ] && [[ "$VERSION" =~ (dev|a|b|rc) ]]; then
|
||
_PYPI_EXTRA_ARGS+=(--prerelease=explicit)
|
||
fi
|
||
_uv_pip_install_with_mirror_fallback "$PACKAGE" ${_PYPI_EXTRA_ARGS[@]+"${_PYPI_EXTRA_ARGS[@]}"} \
|
||
|| die "Install from PyPI failed (tried mirrors and the official index)"
|
||
fi
|
||
|
||
_pin_mcp_compat || true
|
||
_verify_install
|
||
|
||
[ -x "$OCTOP_VENV/bin/octop" ] || die "Install failed: octop CLI not found in the virtualenv"
|
||
info "Octop installed successfully"
|
||
|
||
if [ "$_CONSOLE_AVAILABLE" = 0 ]; then
|
||
CONSOLE_CHECK="$("$OCTOP_VENV/bin/python" -c "import importlib.resources, octop; p=importlib.resources.files('octop')/'dashboard'/'index.html'; print('yes' if p.is_file() else 'no')" 2>/dev/null || echo 'no')"
|
||
[ "$CONSOLE_CHECK" = "yes" ] && _CONSOLE_AVAILABLE=1
|
||
fi
|
||
|
||
# ── 步骤 3.4: Linux 安装 bubblewrap(局部 root_dir 下 execute jail)──────────
|
||
_ensure_bubblewrap() {
|
||
# macOS 无可用 bwrap;仅 Linux 安装。缺失时 harness 走普通 local_shell(无目录狱)。
|
||
if [ "$(uname -s 2>/dev/null || echo unknown)" != "Linux" ]; then
|
||
return 0
|
||
fi
|
||
if command -v bwrap &>/dev/null; then
|
||
info "bubblewrap ready: $(command -v bwrap)"
|
||
return 0
|
||
fi
|
||
info "Installing bubblewrap (required for the execute jail under a local root_dir)..."
|
||
if command -v apt-get &>/dev/null || command -v apt &>/dev/null; then
|
||
_run_as_root env DEBIAN_FRONTEND=noninteractive apt-get update -qq 2>/dev/null || true
|
||
_run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq bubblewrap 2>/dev/null || true
|
||
elif command -v dnf &>/dev/null; then
|
||
_run_as_root dnf install -y bubblewrap 2>/dev/null || true
|
||
elif command -v yum &>/dev/null; then
|
||
_run_as_root yum install -y bubblewrap 2>/dev/null || true
|
||
elif command -v pacman &>/dev/null; then
|
||
_run_as_root pacman -Sy --noconfirm bubblewrap 2>/dev/null || true
|
||
elif command -v zypper &>/dev/null; then
|
||
_run_as_root zypper install -y bubblewrap 2>/dev/null || true
|
||
else
|
||
warn "Unrecognized package manager; install bubblewrap (bwrap) manually"
|
||
return 0
|
||
fi
|
||
if command -v bwrap &>/dev/null; then
|
||
info "bubblewrap installed successfully: $(command -v bwrap)"
|
||
else
|
||
warn "bubblewrap install failed: execute under a local root_dir falls back to path rewriting (no real jail)"
|
||
fi
|
||
}
|
||
|
||
_ensure_bubblewrap
|
||
|
||
# ── 步骤 3.5: 安装 Playwright Chromium 及系统依赖 ─────────────────────────────
|
||
_install_playwright_system_deps() {
|
||
# macOS 无需额外系统依赖
|
||
if [ "$OS" = "Darwin" ]; then
|
||
info "macOS: Playwright system dependencies are built in"
|
||
return
|
||
fi
|
||
|
||
if command -v apt-get &>/dev/null || command -v apt &>/dev/null; then
|
||
info "Detected the apt package manager (Debian/Ubuntu)..."
|
||
info "Installing Playwright system dependencies..."
|
||
# 优先使用 Playwright 自带的 install-deps
|
||
if "$OCTOP_VENV/bin/python" -m playwright install-deps chromium --with-deps 2>/dev/null; then
|
||
return
|
||
fi
|
||
# 回退:手动安装(Ubuntu 24+ 部分包名为 *t64)
|
||
_run_as_root apt-get update 2>/dev/null || true
|
||
# 逐包尝试,兼容 Ubuntu 22/24 包名差异
|
||
local pkg
|
||
for pkg in \
|
||
libnss3 libxss1 libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 \
|
||
libxrender1 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||
libexpat1 libfontconfig1 libfreetype6 libgbm1 libglib2.0-0 \
|
||
libgtk-3-0 libpango-1.0-0 libpangocairo-1.0-0 libxfixes3 \
|
||
libxinerama1 libxt6 zlib1g fonts-noto-cjk \
|
||
libasound2t64 libasound2 \
|
||
libatk-bridge2.0-0t64 libatk-bridge2.0-0 \
|
||
libgdk-pixbuf-2.0-0 libgdk-pixbuf2.0-0 \
|
||
; do
|
||
_run_as_root apt-get install -y "$pkg" 2>/dev/null || true
|
||
done
|
||
return
|
||
fi
|
||
|
||
if command -v dnf &>/dev/null; then
|
||
info "Detected the dnf package manager (Fedora/RHEL)..."
|
||
info "Installing Playwright system dependencies..."
|
||
_run_as_root dnf install -y \
|
||
alsa-lib atk at-spi2-atk cups-libs libdrm libgbm \
|
||
libX11 libXcomposite libXdamage libXext libXfixes libXrandr \
|
||
libxkbcommon nss pango \
|
||
google-noto-sans-cjk-ttc-fonts 2>/dev/null || true
|
||
return
|
||
fi
|
||
|
||
if command -v yum &>/dev/null; then
|
||
info "Detected the yum package manager (CentOS/RHEL)..."
|
||
info "Installing Playwright system dependencies..."
|
||
_run_as_root yum install -y \
|
||
alsa-lib atk at-spi2-atk cups-libs libdrm libgbm \
|
||
libX11 libXcomposite libXdamage libXext libXfixes libXrandr \
|
||
libxkbcommon nss pango \
|
||
google-noto-sans-cjk-ttc-fonts 2>/dev/null || true
|
||
return
|
||
fi
|
||
|
||
if command -v pacman &>/dev/null; then
|
||
info "Detected the pacman package manager (Arch/Manjaro)..."
|
||
info "Installing Playwright system dependencies..."
|
||
_run_as_root pacman -S --noconfirm --needed \
|
||
alsa-lib atk at-spi2-atk cups libdrm mesa \
|
||
libx11 libxcomposite libxdamage libxext libxfixes libxrandr \
|
||
libxkbcommon nss pango \
|
||
noto-fonts-cjk 2>/dev/null || true
|
||
return
|
||
fi
|
||
|
||
if command -v zypper &>/dev/null; then
|
||
info "Detected the zypper package manager (openSUSE)..."
|
||
info "Installing Playwright system dependencies..."
|
||
_run_as_root zypper install -y \
|
||
alsa libatk-1_0-0 libatk-bridge-2_0-0 libcups2 libdrm2 \
|
||
Mesa-libgbm1 libX11-6 libXcomposite1 libXdamage1 libXext6 \
|
||
libXfixes3 libXrandr2 libxkbcommon0 libnspr4 libnss3 \
|
||
libpango-1_0-0 \
|
||
noto-sans-cjk-fonts 2>/dev/null || true
|
||
return
|
||
fi
|
||
|
||
warn "No known package manager detected; skipping automatic Playwright system deps"
|
||
warn "If Playwright fails at runtime, install deps manually or run: playwright install-deps chromium"
|
||
}
|
||
|
||
_install_playwright_browsers() {
|
||
if _glibc_too_old_for_wheels; then
|
||
warn "glibc=$(_glibc_major_minor) (e.g. CentOS 7) cannot run Playwright's bundled Node/Chromium (needs glibc >= 2.28)"
|
||
warn "The playwright Python package is installed but Chromium was skipped; Ubuntu 20.04+ / CentOS/RHEL 8+ recommended"
|
||
return 1
|
||
fi
|
||
|
||
# 镜像分层(对齐 finnie TencentOS 策略):
|
||
# 1. 用户指定 PLAYWRIGHT_DOWNLOAD_HOST
|
||
# 2. npmmirror(国内最快)
|
||
# 3. 官方 CDN(失败兜底)
|
||
# 单源超时避免 GCS 卡住拖死整次安装;可用 PLAYWRIGHT_INSTALL_TIMEOUT 覆盖(秒)
|
||
local _pw_timeout="${PLAYWRIGHT_INSTALL_TIMEOUT:-600}"
|
||
_run_playwright_chromium_install() {
|
||
if command -v timeout &>/dev/null; then
|
||
timeout "$_pw_timeout" "$OCTOP_VENV/bin/python" -m playwright install chromium
|
||
else
|
||
"$OCTOP_VENV/bin/python" -m playwright install chromium
|
||
fi
|
||
}
|
||
|
||
local -a _pw_hosts=()
|
||
local _h
|
||
if [ -n "${PLAYWRIGHT_DOWNLOAD_HOST:-}" ]; then
|
||
_pw_hosts+=("$PLAYWRIGHT_DOWNLOAD_HOST")
|
||
fi
|
||
_pw_hosts+=("https://cdn.npmmirror.com/binaries/playwright")
|
||
_pw_hosts+=("") # 官方:清空 PLAYWRIGHT_DOWNLOAD_HOST
|
||
|
||
local _seen="|"
|
||
for _h in "${_pw_hosts[@]}"; do
|
||
case "$_seen" in
|
||
*"|${_h}|"*) continue ;;
|
||
esac
|
||
_seen="${_seen}${_h}|"
|
||
|
||
if [ -n "$_h" ]; then
|
||
export PLAYWRIGHT_DOWNLOAD_HOST="$_h"
|
||
info "Trying Playwright mirror: $_h"
|
||
else
|
||
unset PLAYWRIGHT_DOWNLOAD_HOST || true
|
||
info "Trying the official Playwright CDN..."
|
||
fi
|
||
|
||
if _run_playwright_chromium_install; then
|
||
info "✓ Playwright Chromium installed successfully"
|
||
return 0
|
||
fi
|
||
warn "This source failed or timed out, trying the next mirror..."
|
||
done
|
||
|
||
warn "⚠ Playwright Chromium install failed; you can run this later:"
|
||
warn " PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright \\"
|
||
warn " $OCTOP_VENV/bin/python -m playwright install chromium"
|
||
return 1
|
||
}
|
||
|
||
# 检测系统是否已安装 Chrome / Chromium。
|
||
# GUI 系统(macOS / Windows / Linux 桌面)通常已自带,无需再下载 Playwright 自带 Chromium。
|
||
_detect_system_chrome() {
|
||
# 优先复用 octop-browser 的探测器(与运行期 launch 路径一致)
|
||
local chrome
|
||
chrome="$("$OCTOP_VENV/bin/python" -c '
|
||
import sys
|
||
try:
|
||
from octop_browser.cdp.launcher import find_chrome
|
||
except Exception:
|
||
sys.exit(0)
|
||
p = find_chrome()
|
||
if p:
|
||
print(p)
|
||
' 2>/dev/null)"
|
||
[ -n "$chrome" ] && { echo "$chrome"; return 0; }
|
||
|
||
# 回退:常见命令
|
||
local candidate
|
||
for candidate in google-chrome google-chrome-stable chromium chromium-browser chrome; do
|
||
if command -v "$candidate" &>/dev/null; then
|
||
command -v "$candidate"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# 回退:常见安装路径(GUI 系统)
|
||
local p
|
||
for p in \
|
||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
||
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
||
"/opt/google/chrome/chrome" \
|
||
"/usr/bin/google-chrome" \
|
||
"/usr/bin/chromium" \
|
||
"/usr/bin/chromium-browser" \
|
||
; do
|
||
[ -x "$p" ] && { echo "$p"; return 0; }
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# Playwright Chromium is opt-in (--extras browser).
|
||
_WANT_PW_CHROMIUM=0
|
||
if [ -n "$EXTRAS" ]; then
|
||
_old_ifs="$IFS"
|
||
IFS=','
|
||
for _part in $EXTRAS; do
|
||
[ "$_part" = "browser" ] && _WANT_PW_CHROMIUM=1
|
||
done
|
||
IFS="$_old_ifs"
|
||
fi
|
||
|
||
if [ "$_WANT_PW_CHROMIUM" != 1 ]; then
|
||
info "Skipping Playwright Chromium download."
|
||
info "For remote-browser automation, re-run with --extras browser, use the dashboard, or:"
|
||
info " $OCTOP_VENV/bin/python -m playwright install chromium"
|
||
elif "$OCTOP_VENV/bin/python" -c "import playwright" 2>/dev/null; then
|
||
_SYSTEM_CHROME="$(_detect_system_chrome || true)"
|
||
if [ -n "$_SYSTEM_CHROME" ]; then
|
||
info "Detected system Chrome/Chromium: $_SYSTEM_CHROME"
|
||
info "Using the system browser and skipping the Playwright Chromium download (faster, smaller)."
|
||
info "To use Playwright's bundled Chromium instead, run:"
|
||
info " $OCTOP_VENV/bin/python -m playwright install chromium"
|
||
else
|
||
_install_playwright_system_deps
|
||
_install_playwright_browsers || true
|
||
fi
|
||
else
|
||
warn "playwright is not in the virtualenv, skipping Chromium; you can later run: uv pip install playwright --python $OCTOP_VENV/bin/python"
|
||
fi
|
||
|
||
# ── 步骤 4: 创建包装脚本 ─────────────────────────────────────────────────────
|
||
mkdir -p "$OCTOP_BIN"
|
||
|
||
cat > "$OCTOP_BIN/octop" << 'WRAPPER'
|
||
#!/usr/bin/env bash
|
||
# Octop CLI 包装脚本 — 委托给 uv 管理的环境。
|
||
set -euo pipefail
|
||
|
||
OCTOP_HOME="${OCTOP_HOME:-$HOME/.octop}"
|
||
REAL_BIN="$OCTOP_HOME/venv/bin/octop"
|
||
|
||
if [ ! -x "$REAL_BIN" ]; then
|
||
echo "Error: Octop environment not found in $OCTOP_HOME/venv" >&2
|
||
echo "Please re-run the installer" >&2
|
||
exit 1
|
||
fi
|
||
|
||
exec "$REAL_BIN" "$@"
|
||
WRAPPER
|
||
|
||
chmod +x "$OCTOP_BIN/octop"
|
||
info "Wrapper script created: $OCTOP_BIN/octop"
|
||
|
||
# ── 步骤 5: 让 octop 立即可用(无需 source)──────────────────────────────────
|
||
# 子进程无法修改父 shell 的 PATH。要让 curl|bash / bash install.sh 后立刻可用,
|
||
# 只能把可执行文件放进「当前 PATH 里已有」的目录(常见为 /usr/local/bin)。
|
||
|
||
_can_write_dir() {
|
||
local dir="$1"
|
||
[ -d "$dir" ] && [ -w "$dir" ]
|
||
}
|
||
|
||
_try_symlink() {
|
||
# 在 target_dir 创建指向包装脚本的 octop 符号链接。成功返回 0。
|
||
local target_dir="$1"
|
||
local link_path="$target_dir/octop"
|
||
local src="$OCTOP_BIN/octop"
|
||
|
||
mkdir -p "$target_dir" 2>/dev/null || true
|
||
|
||
if _can_write_dir "$target_dir"; then
|
||
ln -sfn "$src" "$link_path" && return 0
|
||
fi
|
||
if _sudo_nopass; then
|
||
sudo mkdir -p "$target_dir" 2>/dev/null || true
|
||
sudo ln -sfn "$src" "$link_path" && return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
_path_contains() {
|
||
case ":$PATH:" in
|
||
*":$1:"*) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
LINKED_PATH=""
|
||
_link_into_existing_path() {
|
||
local candidates=()
|
||
case "$OS" in
|
||
Darwin)
|
||
# Apple Silicon Homebrew 优先,再退回传统 /usr/local/bin
|
||
candidates=(/opt/homebrew/bin /usr/local/bin)
|
||
;;
|
||
*)
|
||
candidates=(/usr/local/bin)
|
||
;;
|
||
esac
|
||
|
||
local dir
|
||
for dir in "${candidates[@]}"; do
|
||
if _path_contains "$dir" && _try_symlink "$dir"; then
|
||
LINKED_PATH="$dir/octop"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# 回退:~/.local/bin(Ubuntu/Debian 的 ~/.profile 会在目录存在时加入 PATH;
|
||
# 若当前会话 PATH 尚无该目录,仍无法免 source,仅作持久化兜底)
|
||
if _try_symlink "$HOME/.local/bin"; then
|
||
LINKED_PATH="$HOME/.local/bin/octop"
|
||
if _path_contains "$HOME/.local/bin"; then
|
||
return 0
|
||
fi
|
||
# 目录刚创建、尚未在当前 PATH 中:本会话仍依赖下方 profile / 手动 PATH
|
||
return 1
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
IMMEDIATE_OK=false
|
||
if _link_into_existing_path; then
|
||
IMMEDIATE_OK=true
|
||
info "Linked to ${LINKED_PATH} (octop works in the current shell)"
|
||
elif [ -n "$LINKED_PATH" ]; then
|
||
info "Linked to $LINKED_PATH"
|
||
fi
|
||
|
||
# ── 步骤 6: 更新 shell / 系统 profile(新开终端持久生效)─────────────────────
|
||
PATH_ENTRY="export PATH=\"${OCTOP_BIN}:\$PATH\""
|
||
|
||
add_to_profile() {
|
||
local profile="$1"
|
||
local create="$2"
|
||
if [ -f "$profile" ] && grep -qF "$OCTOP_BIN" "$profile" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
# 兼容旧版标记(仅含 .octop/bin 字样)
|
||
if [ -f "$profile" ] && grep -qF '.octop/bin' "$profile" 2>/dev/null; then
|
||
return 0
|
||
fi
|
||
if [ -f "$profile" ] || [ "$create" = "create" ]; then
|
||
printf '\n# Octop\n%s\n' "$PATH_ENTRY" >> "$profile"
|
||
info "Updated $profile"
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
_write_profile_d() {
|
||
# CentOS / Ubuntu 登录 shell 会加载 /etc/profile.d/*.sh
|
||
local dest="/etc/profile.d/octop.sh"
|
||
local content="# Octop CLI
|
||
export PATH=\"${OCTOP_BIN}:\$PATH\"
|
||
"
|
||
if [ -d /etc/profile.d ]; then
|
||
if [ -w /etc/profile.d ] || _can_write_dir /etc/profile.d; then
|
||
printf '%s' "$content" > "$dest" && chmod 644 "$dest" && {
|
||
info "Wrote $dest"
|
||
return 0
|
||
}
|
||
fi
|
||
if _sudo_nopass; then
|
||
printf '%s' "$content" | sudo tee "$dest" >/dev/null && sudo chmod 644 "$dest" && {
|
||
info "Wrote $dest"
|
||
return 0
|
||
}
|
||
fi
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
UPDATED_PROFILE=false
|
||
case "$OS" in
|
||
Darwin)
|
||
add_to_profile "$HOME/.zshrc" "create" && UPDATED_PROFILE=true
|
||
add_to_profile "$HOME/.bash_profile" "no-create" || true
|
||
add_to_profile "$HOME/.bashrc" "no-create" || true
|
||
;;
|
||
Linux)
|
||
# CentOS/RHEL:SSH 登录读 .bash_profile;Ubuntu:登录读 .profile,交互读 .bashrc
|
||
add_to_profile "$HOME/.bashrc" "create" && UPDATED_PROFILE=true
|
||
add_to_profile "$HOME/.bash_profile" "no-create" || true
|
||
add_to_profile "$HOME/.profile" "no-create" || true
|
||
add_to_profile "$HOME/.zshrc" "no-create" || true
|
||
_write_profile_d || true
|
||
;;
|
||
esac
|
||
|
||
export PATH="$OCTOP_BIN:$PATH"
|
||
|
||
|
||
# Probe mobile host capability into config.json
|
||
if [ -f "$OCTOP_VENV/bin/python" ] && [ -d "$OCTOP_HOME" ]; then
|
||
CONFIG_PATH="$OCTOP_HOME/config.json"
|
||
if [ ! -f "$CONFIG_PATH" ]; then
|
||
echo "{}" > "$CONFIG_PATH"
|
||
fi
|
||
"$OCTOP_VENV/bin/python" -m octop.infra.mobile "$CONFIG_PATH" || true
|
||
fi
|
||
|
||
# ── 完成 ──────────────────────────────────────────────────────────────────────
|
||
echo ""
|
||
printf "${GREEN}${BOLD}Octop installed successfully!${RESET}\n"
|
||
echo ""
|
||
printf " Location: ${BOLD}%s${RESET}\n" "$OCTOP_HOME"
|
||
printf " Python: ${BOLD}%s${RESET}\n" "$("$OCTOP_VENV/bin/python" --version 2>&1)"
|
||
if [ -n "$LINKED_PATH" ]; then
|
||
printf " CLI link: ${BOLD}%s${RESET}\n" "$LINKED_PATH"
|
||
fi
|
||
if [ "$_CONSOLE_AVAILABLE" = 1 ]; then
|
||
printf " Console (Web UI): ${GREEN}available${RESET}\n"
|
||
else
|
||
printf " Console (Web UI): ${YELLOW}unavailable${RESET}\n"
|
||
fi
|
||
echo ""
|
||
|
||
if [ "$IMMEDIATE_OK" = true ]; then
|
||
info "octop is ready to use in the current shell (no source needed)"
|
||
elif [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
|
||
export PATH="$OCTOP_BIN:$PATH"
|
||
info "PATH updated; octop is available now"
|
||
else
|
||
warn "Could not write to a directory already in PATH (e.g. /usr/local/bin). In this shell run:"
|
||
echo ""
|
||
printf " ${BOLD}export PATH=\"%s:\$PATH\"${RESET}\n" "$OCTOP_BIN"
|
||
echo ""
|
||
if [ "$UPDATED_PROFILE" = true ]; then
|
||
echo "New shells will not need this step."
|
||
fi
|
||
fi
|
||
echo ""
|
||
echo "Then run:"
|
||
echo ""
|
||
printf " ${BOLD}octop run${RESET} # Run in the foreground (API + Web console)\n"
|
||
printf " ${BOLD}octop service start${RESET} # Install and run in the background (systemd / launchd)\n"
|
||
printf " ${BOLD}open http://127.0.0.1:8088${RESET}\n"
|
||
echo ""
|
||
printf "Upgrade: re-run this installer. Cleanup: ${BOLD}octop clean${RESET}\n"
|
||
printf "Playwright Chromium (optional): ${BOLD}$OCTOP_VENV/bin/python -m playwright install chromium${RESET}\n"
|