From 8a823c940ee6a0558a56950e7ec096fa7cb113dc Mon Sep 17 00:00:00 2001 From: Liang Jiaqing Date: Wed, 10 Jun 2026 23:18:10 +0800 Subject: [PATCH] docs+fix: README install overhaul, self-hosted install scripts, TMWebDriver logging fix - README (EN): clone+uv as recommended install; one-line installer demoted to convenience with script source link; drop unpolished exe section; recommend frontends/tui_v3.py (prompt_toolkit+rich, not Textual); note agent core's single dependency (requests); TMWebdriver naming - assets: add ga_install.ps1/.sh (GLOBAL mode uses GitHub/astral-sh sources) - TMWebDriver.py: don't let logging failure mask successful remote JS result --- README.md | 73 +++--- TMWebDriver.py | 11 +- assets/ga_install.ps1 | 577 ++++++++++++++++++++++++++++++++++++++++++ assets/ga_install.sh | 289 +++++++++++++++++++++ 4 files changed, 905 insertions(+), 45 deletions(-) create mode 100644 assets/ga_install.ps1 create mode 100644 assets/ga_install.sh diff --git a/README.md b/README.md index 462f5eb8..a5a2fcd7 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Every time GenericAgent solves a new task, it automatically crystallizes the exe | :--- | :--- | | 🧬 **Self-Evolving** | Automatically crystallizes each task into a Skill. Capabilities grow with every use, forming your personal skill tree. | | 🪶 **Minimal Architecture** | ~3K lines of core code. Agent Loop is ~100 lines. No complex dependencies, zero deployment overhead. | -| ⚡ **Strong Execution** | Injects into a real browser (preserving login sessions). 9 atomic tools take direct control of the system. | +| ⚡ **Strong Execution** | **TMWebdriver** injects into a real browser (preserving login sessions). 9 atomic tools take direct control of the system. | | 🔌 **High Compatibility** | Supports Claude / Gemini / Kimi / MiniMax and other major models. Cross-platform. | | 💰 **Token Efficient** | <30K context window — a fraction of the 200K–1M other agents consume. Less noise, fewer hallucinations, higher success rate, lower cost. | @@ -129,34 +129,37 @@ curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/ma ### For Humans -#### Method 1 — One-line install *(recommended)* - -This installs GenericAgent with an isolated Python environment and Git, then downloads a ready-to-run package. - -**Windows PowerShell** - -```powershell -powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm http://fudankw.cn:9000/files/ga_install.ps1 | iex" -``` - -**Linux / macOS** - -```bash -GLOBAL=1 bash -c "$(curl -fsSL http://fudankw.cn:9000/files/ga_install.sh)" -``` - -After installation, launch the desktop app: - -- **Windows** — double-click `frontends/GenericAgent.exe` -- **Linux / macOS** — run `python launch.pyw` from the install directory - -#### Method 2 — Python install *(for developers)* +#### Method 1 — Clone & install *(recommended)* ```bash git clone https://github.com/lsdefine/GenericAgent.git && cd GenericAgent uv venv && uv pip install -e ".[ui]" cp mykey_template_en.py mykey.py # fill in your LLM API key -python launch.pyw +``` + +The agent core depends on just **one** library — `requests`. The other four lightweight packages (`beautifulsoup4`, `bottle`, `simple-websocket-server`, `aiohttp`) power TMWebdriver's local server. No Playwright, no LangChain, no browser binaries to download. + +Then launch: + +```bash +python frontends/tui_v3.py # Terminal UI (recommended) +python launch.pyw # Streamlit web UI +``` + +#### Method 2 — One-line installer *(convenience)* + +Sets up a self-contained directory with an isolated Python environment, Git, and a ready-to-run package. The script is in [`assets/`](assets/) if you'd like to read it first. + +**Windows PowerShell** + +```powershell +powershell -ExecutionPolicy Bypass -c "$env:GLOBAL=1; irm https://raw.githubusercontent.com/lsdefine/GenericAgent/main/assets/ga_install.ps1 | iex" +``` + +**Linux / macOS** + +```bash +GLOBAL=1 bash -c "$(curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/main/assets/ga_install.sh)" ``` > 💡 GenericAgent grows its environment **through the Agent itself** — don't pre-install everything. See [Unlocking Advanced Capabilities](#-unlocking-advanced-capabilities) below. @@ -167,20 +170,12 @@ python launch.pyw ### Frontends -#### Desktop App +#### Terminal UI *(recommended)* -For one-line installs on Windows, double-click: - -```text -frontends/GenericAgent.exe -``` - -#### Terminal UI - -A lightweight, keyboard-driven interface built on [Textual](https://github.com/Textualize/textual). Supports multiple concurrent sessions and real-time streaming. +A lightweight, scrollback-first terminal interface built on `prompt_toolkit` + `rich`. Supports multiple concurrent sessions and real-time streaming. ```bash -python frontends/tuiapp_v2.py +python frontends/tui_v3.py ```
@@ -188,10 +183,10 @@ python frontends/tuiapp_v2.py TUI rendering on Windows can be flaky depending on terminal + font. Common causes: -1. `textual` is not on the latest version — `pip install -U textual` first. +1. `prompt_toolkit` / `rich` are not on the latest version — `pip install -U prompt_toolkit rich` first. 2. PowerShell / cmd ship with terminals that have rough Unicode + key-binding support. **Prefer Git Bash on Windows**, which is much better behaved. 3. If it still looks broken, ask GA itself to fix it: - > *"My experience using `frontends/tuiapp_v2.py` in PowerShell / cmd / Git Bash on Windows is very poor — lots of incompatibility. Please refer to Claude Code's best practices for the Windows terminal and fix all font and rendering incompatibilities."* + > *"My experience using `frontends/tui_v3.py` in PowerShell / cmd / Git Bash on Windows is very poor — lots of incompatibility. Please refer to Claude Code's best practices for the Windows terminal and fix all font and rendering incompatibilities."*
@@ -362,9 +357,9 @@ Baselines across these dimensions include **Claude Code**, **OpenAI CodeX**, and -### Browser Realness of GA Web Tools +### Browser Realness of GA Web Tools (TMWebdriver) -GA web tools run through a **real, persistent Chrome/Chromium session** rather than a disposable headless sandbox, preserving cookies, login state, extensions, GPU/WebGL behavior, and normal browser-session fingerprints. +GA web tools are powered by **TMWebdriver** — a local WebSocket server plus a Chrome extension — running through a **real, persistent Chrome/Chromium session** rather than a disposable headless sandbox, preserving cookies, login state, extensions, GPU/WebGL behavior, and normal browser-session fingerprints. | Detection Service / Signal | Vanilla Headless Automation | GA Web Tools | Notes | | :--- | :---: | :---: | :--- | diff --git a/TMWebDriver.py b/TMWebDriver.py index b0e1a343..c14535a1 100644 --- a/TMWebDriver.py +++ b/TMWebDriver.py @@ -93,12 +93,11 @@ class TMWebDriver: session_id = data.get('sessionId') code = data.get('code') timeout = float(data.get('timeout', 10.0)) - try: - result = self.execute_js(code, timeout=timeout, session_id=session_id) - print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' ')) - return json.dumps({'r': result}, ensure_ascii=False) - except Exception as e: - return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False) + try: result = self.execute_js(code, timeout=timeout, session_id=session_id) + except Exception as e: return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False) + try: print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' ')) + except Exception: pass + return json.dumps({'r': result}, ensure_ascii=False) return 'ok' def run(): from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler diff --git a/assets/ga_install.ps1 b/assets/ga_install.ps1 new file mode 100644 index 00000000..5d5e1950 --- /dev/null +++ b/assets/ga_install.ps1 @@ -0,0 +1,577 @@ +#requires -version 5.1 + +<#! + +GenericAgent one-click portable deployer for Windows. + + + +Modes: + + Default/Mainland: download GenericAgent.zip + uv + PortableGit from user's VPS, set China PyPI mirror. + + GLOBAL=1: clone GenericAgent from GitHub; uv and PortableGit also come from GitHub releases; no PyPI mirror. + + + +Portable components are installed under \.portable: + + uv, Python installed by uv, PortableGit. + +#> + +param( + + [string]$InstallDir = "$env:USERPROFILE\GenericAgent", + + [string]$PythonVersion = "3.12", + + [switch]$Force + +) + + + +$ErrorActionPreference = "Stop" + + + +# Make Chinese output reliable in Windows PowerShell 5.1 and redirected logs. + +try { + + [Console]::InputEncoding = [System.Text.Encoding]::UTF8 + + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + + $OutputEncoding = [System.Text.Encoding]::UTF8 + +} catch { } + + + +$RepoUrl = "https://github.com/lsdefine/GenericAgent.git" + +$VpsBase = "http://47.101.182.29:9000" + +$GaZipUrl = "$VpsBase/files/GenericAgent.zip" + +$UvUrl = "$VpsBase/uv/uv-x86_64-pc-windows-msvc.zip" + +$GitUrl = "$VpsBase/files/PortableGit-2.54.0-64-bit.7z.exe" + +$Deps = @("requests>=2.28", "beautifulsoup4>=4.12", "bottle>=0.12", "simple-websocket-server>=0.4", "streamlit>=1.28") + +$MainlandIndex = "https://pypi.tuna.tsinghua.edu.cn/simple" + +$GlobalMode = ($env:GLOBAL -eq "1") + +if ($GlobalMode) { + # GLOBAL=1: fetch everything from GitHub; no mainland endpoints involved. + $UvUrl = "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip" + $GitUrl = "https://github.com/git-for-windows/git/releases/download/v2.54.0.windows.1/PortableGit-2.54.0-64-bit.7z.exe" +} + + + +$GaDir = [IO.Path]::GetFullPath($InstallDir) + +$PortableRoot = Join-Path $GaDir ".portable" + +$Bin = Join-Path $PortableRoot "bin" + +$Cache = Join-Path $PortableRoot "cache" + +$Tools = Join-Path $PortableRoot "tools" + +$UvZip = Join-Path $Cache "uv-x86_64-pc-windows-msvc.zip" + +$GaZip = Join-Path $Cache "GenericAgent.zip" + +$GitExeArchive = Join-Path $Cache "PortableGit-2.54.0-64-bit.7z.exe" + +$UvExtract = Join-Path $Cache "uv-extract" + +$GaExtract = Join-Path $Cache "ga-extract" + +$GitDir = Join-Path $Tools "PortableGit" + +$UvExe = Join-Path $Bin "uv.exe" + +$GitExe = Join-Path $GitDir "bin\git.exe" + +$EnvCmd = Join-Path $GaDir "env.cmd" + +$EnvPs1 = Join-Path $GaDir "env.ps1" + + + +function Say($m) { Write-Host "[ga-deploy] $m" -ForegroundColor Cyan } + +function Ok($m) { Write-Host "[ok] $m" -ForegroundColor Green } + +function Die($m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 } + +function Invoke-Native([scriptblock]$Command) { + + $prevEAP = $ErrorActionPreference + + $ErrorActionPreference = 'Continue' + + try { & $Command } finally { $ErrorActionPreference = $prevEAP } + + return $LASTEXITCODE + +} + + + +function Download-File($Url, $OutFile) { + + New-Item -ItemType Directory -Force -Path (Split-Path $OutFile) | Out-Null + + Say "Downloading $Url" + + $wc = New-Object System.Net.WebClient + + $wc.Headers.Add("User-Agent", "Mozilla/5.0 ga-deploy") + + try { $wc.DownloadFile($Url, $OutFile) } finally { $wc.Dispose() } + + if (!(Test-Path $OutFile) -or ((Get-Item $OutFile).Length -lt 1024)) { Die "Download failed: $Url" } + +} + + + +function Expand-ZipClean($Zip, $Dest) { + + if (Test-Path $Dest) { Remove-Item -Recurse -Force $Dest } + + New-Item -ItemType Directory -Force -Path $Dest | Out-Null + + Expand-Archive -Path $Zip -DestinationPath $Dest -Force + +} + + + +function Copy-DirectoryContents($Src, $Dst) { + + New-Item -ItemType Directory -Force -Path $Dst | Out-Null + + Get-ChildItem -LiteralPath $Src -Force | ForEach-Object { + + Copy-Item -LiteralPath $_.FullName -Destination $Dst -Recurse -Force + + } + +} + + + +Say "Install dir: $GaDir" + +Say "Mode: $(if ($GlobalMode) { 'GLOBAL=1 / GitHub clone' } else { 'Mainland / VPS zip' })" + + + +if ((Test-Path $GaDir) -and $Force) { Remove-Item -Recurse -Force $GaDir } + +New-Item -ItemType Directory -Force -Path $GaDir,$PortableRoot,$Bin,$Cache,$Tools | Out-Null + + + +# uv (GitHub release in GLOBAL mode, user's VPS otherwise) + +if (!(Test-Path $UvExe) -or $Force) { + + Download-File $UvUrl $UvZip + + Expand-ZipClean $UvZip $UvExtract + + $foundUv = Get-ChildItem -Path $UvExtract -Recurse -Filter "uv.exe" | Select-Object -First 1 + + if (!$foundUv) { Die "uv.exe not found in archive" } + + Copy-Item $foundUv.FullName $UvExe -Force + +} + +Ok "uv: $(& $UvExe --version)" + + + +# Configure portable Python location. Mirror only in mainland mode. + +$env:UV_PYTHON_INSTALL_DIR = Join-Path $PortableRoot "uv-python" + +$env:UV_CACHE_DIR = Join-Path $PortableRoot "uv-cache" + +if ($GlobalMode) { + + Remove-Item Env:UV_DEFAULT_INDEX -ErrorAction SilentlyContinue + + Remove-Item Env:PIP_INDEX_URL -ErrorAction SilentlyContinue + +} else { + + $env:UV_DEFAULT_INDEX = $MainlandIndex + + $env:PIP_INDEX_URL = $MainlandIndex + +} + +$env:PATH = "$Bin;$env:PATH" + + + + +# Workaround: uv creates minor-version symlinks (junctions) in UV_PYTHON_INSTALL_DIR. +# If a previous interrupted install left a plain directory, uv fails with os error 4390. +# Fix: remove any non-junction subdirectory so uv can recreate them cleanly. +$uvPyDir = $env:UV_PYTHON_INSTALL_DIR +if (Test-Path $uvPyDir) { + Get-ChildItem -LiteralPath $uvPyDir -Directory | ForEach-Object { + $attr = $_.Attributes + if (($attr -band [IO.FileAttributes]::ReparsePoint) -eq 0) { + Say "Removing stale non-junction dir: $($_.Name)" + Remove-Item -LiteralPath $_.FullName -Recurse -Force + } + } +} + +Say "Installing Python $PythonVersion via uv" + +$ec = Invoke-Native { & $UvExe python install $PythonVersion } + +if ($ec -ne 0) { Die "uv python install failed" } + +$PythonExe = (& $UvExe python find $PythonVersion).Trim() + +if (!(Test-Path $PythonExe)) { Die "uv installed Python but python.exe was not found" } + +Ok "Python: $(& $PythonExe --version)" + + + +# PortableGit (GitHub release in GLOBAL mode, user's VPS otherwise). Needed for GLOBAL=1 and useful for user shell. + +if (!(Test-Path $GitExe) -or $Force) { + + Download-File $GitUrl $GitExeArchive + + if (Test-Path $GitDir) { Remove-Item -Recurse -Force $GitDir } + + New-Item -ItemType Directory -Force -Path $GitDir | Out-Null + + Say "Extracting PortableGit" + + $ec = Invoke-Native { & $GitExeArchive -y -o"$GitDir" | Out-Null } + + if ($ec -ne 0) { Die "PortableGit extraction failed" } + +} + +if (!(Test-Path $GitExe)) { Die "git.exe missing: $GitExe" } + +Ok "Git: $(& $GitExe --version)" + + + +$PythonDir = Split-Path $PythonExe -Parent + +$GitBin = Split-Path $GitExe -Parent + +$GitUsrBin = Join-Path $GitDir "usr\bin" + +$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;$env:PATH" + + + +# Fetch/update GenericAgent source. + +if ($GlobalMode) { + + Say "Cloning GenericAgent from GitHub" + + $items = @(Get-ChildItem -LiteralPath $GaDir -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne ".portable" }) + + if ($items.Count -gt 0) { + + if (!$Force) { Die "Install dir contains files. Re-run with -Force to replace source while preserving portable tools." } + + $items | Remove-Item -Recurse -Force + + } + + $TmpClone = Join-Path $Cache "ga-clone" + + if (Test-Path $TmpClone) { Remove-Item -Recurse -Force $TmpClone } + + $ec = Invoke-Native { & $GitExe clone --depth 1 $RepoUrl $TmpClone } + + if ($ec -ne 0) { Die "git clone failed" } + + Copy-DirectoryContents $TmpClone $GaDir + + Remove-Item -Recurse -Force $TmpClone + +} else { + + Say "Downloading GenericAgent package from VPS" + + Download-File $GaZipUrl $GaZip + + Expand-ZipClean $GaZip $GaExtract + + $SrcDir = Join-Path $GaExtract "GenericAgent" + + if (!(Test-Path $SrcDir)) { $SrcDir = $GaExtract } + + $items = @(Get-ChildItem -LiteralPath $GaDir -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne ".portable" }) + + if ($items.Count -gt 0) { $items | Remove-Item -Recurse -Force } + + Copy-DirectoryContents $SrcDir $GaDir + +} + +Ok "GenericAgent source ready: $GaDir" + + + +# Install basic dependencies and project in editable mode into portable Python. + +Say "Installing GenericAgent dependencies via uv pip" + +$installArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + +if (!$GlobalMode) { $installArgs += @("--index-url", $MainlandIndex) } + +$installArgs += $Deps + +$ec = Invoke-Native { & $UvExe @installArgs } + +if ($ec -ne 0) { Die "dependency install failed" } + + + +if (Test-Path (Join-Path $GaDir "pyproject.toml")) { + + $projectArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + + if (!$GlobalMode) { $projectArgs += @("--index-url", $MainlandIndex) } + + $projectArgs += @("-e", $GaDir) + + $ec = Invoke-Native { & $UvExe @projectArgs } + + if ($ec -ne 0) { Die "editable project install failed" } + +} + + + +# Try-install pywebview (optional UI). Failure is non-fatal. + +Say "Attempting to install pywebview (optional, failure is OK)" + +$webviewArgs = @("pip", "install", "--break-system-packages", "--python", $PythonExe) + +if (!$GlobalMode) { $webviewArgs += @("--index-url", $MainlandIndex) } + +$webviewArgs += @("pywebview>=4.0") + +$ec = Invoke-Native { & $UvExe @webviewArgs 2>&1 | Out-Null } + +if ($ec -ne 0) { + + Write-Host "[warn] pywebview install failed. This is optional." -ForegroundColor Yellow + + Write-Host " On Windows it usually works out of the box." -ForegroundColor Yellow + + Write-Host " If needed later: uv pip install pywebview" -ForegroundColor Yellow + +} else { + + Ok "pywebview installed successfully" + +} + + + +# Activation scripts: portable paths are intentionally before system PATH. + +if ($GlobalMode) { + +@" + +@echo off + +set "PORTABLE_DEV_ROOT=$PortableRoot" + +set "GENERICAGENT_HOME=$GaDir" + +set "UV_PYTHON_INSTALL_DIR=$PortableRoot\uv-python" + +set "UV_CACHE_DIR=$PortableRoot\uv-cache" + +set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;%PATH%" + +echo Activated GenericAgent portable env: %GENERICAGENT_HOME% + +"@ | Set-Content -Path $EnvCmd -Encoding ASCII + + + +@" + +`$env:PORTABLE_DEV_ROOT = "$PortableRoot" + +`$env:GENERICAGENT_HOME = "$GaDir" + +`$env:UV_PYTHON_INSTALL_DIR = "$PortableRoot\uv-python" + +`$env:UV_CACHE_DIR = "$PortableRoot\uv-cache" + +`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;`$env:PATH" + +Write-Host "Activated GenericAgent portable env: `$env:GENERICAGENT_HOME" -ForegroundColor Green + +"@ | Set-Content -Path $EnvPs1 -Encoding UTF8 + +} else { + +@" + +@echo off + +set "PORTABLE_DEV_ROOT=$PortableRoot" + +set "GENERICAGENT_HOME=$GaDir" + +set "UV_PYTHON_INSTALL_DIR=$PortableRoot\uv-python" + +set "UV_CACHE_DIR=$PortableRoot\uv-cache" + +set "UV_DEFAULT_INDEX=$MainlandIndex" + +set "PIP_INDEX_URL=$MainlandIndex" + +set "PATH=$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;%PATH%" + +echo Activated GenericAgent portable env: %GENERICAGENT_HOME% + +"@ | Set-Content -Path $EnvCmd -Encoding ASCII + + + +@" + +`$env:PORTABLE_DEV_ROOT = "$PortableRoot" + +`$env:GENERICAGENT_HOME = "$GaDir" + +`$env:UV_PYTHON_INSTALL_DIR = "$PortableRoot\uv-python" + +`$env:UV_CACHE_DIR = "$PortableRoot\uv-cache" + +`$env:UV_DEFAULT_INDEX = "$MainlandIndex" + +`$env:PIP_INDEX_URL = "$MainlandIndex" + +`$env:PATH = "$Bin;$PythonDir;$PythonDir\Scripts;$GitBin;$GitUsrBin;`$env:PATH" + +Write-Host "Activated GenericAgent portable env: `$env:GENERICAGENT_HOME" -ForegroundColor Green + +"@ | Set-Content -Path $EnvPs1 -Encoding UTF8 + +} + + + +Ok "Verification:" + +& $UvExe --version + +& $PythonExe --version + +& $GitExe --version + +& $PythonExe -c "import requests, bs4, bottle; print('deps ok')" + +Write-Host "" + + + +# Copy mykey template if mykey.py does not exist (GLOBAL mode only) + +$MykeyDst = Join-Path $GaDir "mykey.py" + +if ($GlobalMode -and !(Test-Path $MykeyDst)) { + + $MykeyTpl = Join-Path $GaDir "mykey_template_en.py" + + if (Test-Path $MykeyTpl) { + + Copy-Item $MykeyTpl $MykeyDst + + Ok "Copied mykey_template_en.py -> mykey.py" + + } + +} + + + +# Final banner + +Write-Host "" + +if ($GlobalMode) { + + Write-Host @" + +╔═══════════════════════════════════════════════╗ + +║ ✅ GenericAgent installed successfully! ║ + +╠═══════════════════════════════════════════════╣ + +║ 📁 Location: $GaDir + +║ 🔑 Config: edit mykey.py (copied from template) + +║ 🚀 Launch: ga tui / ga launch / ga hub + +╚═══════════════════════════════════════════════╝ + +"@ + +} else { + + Write-Host @" + +╔═══════════════════════════════════════════════╗ + +║ [OK] GenericAgent 安装完成! ║ + +╠═══════════════════════════════════════════════╣ + +║ 安装目录: $GaDir + +║ 配置密钥: ga configure + +║ 启动: ga tui / ga launch / ga hub + +╚═══════════════════════════════════════════════╝ + +"@ + +} + +Write-Host "" + +Write-Host " Activate env: cmd.exe → call `"$EnvCmd`" | PowerShell → . `"$EnvPs1`"" + diff --git a/assets/ga_install.sh b/assets/ga_install.sh new file mode 100644 index 00000000..df6ad322 --- /dev/null +++ b/assets/ga_install.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +set -euo pipefail + +# GenericAgent one-click portable deployer for macOS/Linux. +# Modes: +# Default/Mainland: download GenericAgent.zip + uv from user's VPS, set China PyPI mirror. +# GLOBAL=1: clone GenericAgent from GitHub; uv also from GitHub releases; no PyPI mirror. +# Portable components are installed under /.portable: +# uv, Python installed by uv. On macOS/Linux git is expected from system package manager. + +INSTALL_DIR="${INSTALL_DIR:-$HOME/GenericAgent}" +PYTHON_VERSION="${PYTHON_VERSION:-3.12}" +FORCE="${FORCE:-0}" +GLOBAL="${GLOBAL:-0}" + +REPO_URL="https://github.com/lsdefine/GenericAgent.git" +VPS_BASE="http://47.101.182.29:9000" +GA_ZIP_URL="$VPS_BASE/files/GenericAgent.zip" +MAINLAND_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple" +DEPS=("requests>=2.28" "beautifulsoup4>=4.12" "bottle>=0.12" "simple-websocket-server>=0.4" "streamlit>=1.28") + +say(){ printf '\033[36m[ga-deploy]\033[0m %s\n' "$*"; } +ok(){ printf '\033[32m[ok]\033[0m %s\n' "$*"; } +die(){ printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +usage(){ cat <<'EOF' +Usage: + bash install_portable_env.sh + INSTALL_DIR="$HOME/GenericAgent" PYTHON_VERSION=3.12 FORCE=1 bash install_portable_env.sh + GLOBAL=1 bash install_portable_env.sh + +Environment variables: + INSTALL_DIR Install GenericAgent here. Default: ~/GenericAgent + PYTHON_VERSION Python version installed by uv. Default: 3.12 + FORCE=1 Replace existing source files while preserving/reinstalling portable tools. + GLOBAL=1 Clone from GitHub directly and do not set China PyPI mirror. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage; exit 0; fi + +OS="$(uname -s)" +ARCH="$(uname -m)" +case "$OS:$ARCH" in + Darwin:x86_64) uv_file="uv-x86_64-apple-darwin.tar.gz" ;; + Darwin:arm64) uv_file="uv-aarch64-apple-darwin.tar.gz" ;; + Linux:x86_64) uv_file="uv-x86_64-unknown-linux-gnu.tar.gz" ;; + Linux:aarch64|Linux:arm64) uv_file="uv-aarch64-unknown-linux-gnu.tar.gz" ;; + *) die "Unsupported platform: $OS $ARCH" ;; +esac + +GA_DIR="${INSTALL_DIR/#\~/$HOME}" +case "$GA_DIR" in + /*) ;; + *) GA_DIR="$PWD/$GA_DIR" ;; +esac +mkdir -p "$GA_DIR" +GA_DIR="$(cd "$GA_DIR" && pwd -P)" +PORTABLE_ROOT="$GA_DIR/.portable" +BIN="$PORTABLE_ROOT/bin" +CACHE="$PORTABLE_ROOT/cache" +UV_TGZ="$CACHE/$uv_file" +GA_ZIP="$CACHE/GenericAgent.zip" +UV_EXTRACT="$CACHE/uv-extract" +GA_EXTRACT="$CACHE/ga-extract" +UV_EXE="$BIN/uv" +ENV_SH="$GA_DIR/env.sh" + +mkdir -p "$GA_DIR" "$PORTABLE_ROOT" "$BIN" "$CACHE" + +say "Install dir: $GA_DIR" +if [[ "$GLOBAL" == "1" ]]; then say "Mode: GLOBAL=1 / GitHub clone"; else say "Mode: Mainland / VPS zip"; fi + +if [[ "$FORCE" == "1" ]]; then + # Preserve .portable if present; remove source files later before deploying. + : +fi + +download(){ + local url="$1" out="$2" + mkdir -p "$(dirname "$out")" + say "Downloading $url" + if command -v curl >/dev/null 2>&1; then + curl -fL --retry 3 -A "ga-deploy" -o "$out" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$out" "$url" + else + die "curl or wget is required" + fi + [[ -s "$out" ]] || die "Download failed: $url" +} + +extract_tgz_clean(){ + local tgz="$1" dest="$2" + rm -rf "$dest"; mkdir -p "$dest" + tar -xzf "$tgz" -C "$dest" +} + +extract_zip_clean(){ + local zip="$1" dest="$2" + rm -rf "$dest"; mkdir -p "$dest" + if command -v unzip >/dev/null 2>&1; then + unzip -q "$zip" -d "$dest" + else + python3 - "$zip" "$dest" <<'PY' +import sys, zipfile +with zipfile.ZipFile(sys.argv[1]) as z: + z.extractall(sys.argv[2]) +PY + fi +} + +copy_contents(){ + local src="$1" dst="$2" + mkdir -p "$dst" + (cd "$src" && tar -cf - .) | (cd "$dst" && tar -xf -) +} + +remove_source_files(){ + shopt -s dotglob nullglob + for p in "$GA_DIR"/*; do + [[ "$(basename "$p")" == ".portable" ]] && continue + rm -rf "$p" + done + shopt -u dotglob nullglob +} + +# uv: GitHub release in GLOBAL mode, user's VPS otherwise +if [[ ! -x "$UV_EXE" || "$FORCE" == "1" ]]; then + if [[ "$GLOBAL" == "1" ]]; then + download "https://github.com/astral-sh/uv/releases/latest/download/$uv_file" "$UV_TGZ" + else + download "$VPS_BASE/uv/$uv_file" "$UV_TGZ" + fi + extract_tgz_clean "$UV_TGZ" "$UV_EXTRACT" + found_uv="$(find "$UV_EXTRACT" -type f -name uv | head -n 1 || true)" + [[ -n "$found_uv" ]] || die "uv not found in archive" + cp "$found_uv" "$UV_EXE" + chmod +x "$UV_EXE" +fi +ok "uv: $($UV_EXE --version)" + +export UV_PYTHON_INSTALL_DIR="$PORTABLE_ROOT/uv-python" +export UV_CACHE_DIR="$PORTABLE_ROOT/uv-cache" +if [[ "$GLOBAL" == "1" ]]; then + unset UV_DEFAULT_INDEX PIP_INDEX_URL +else + export UV_DEFAULT_INDEX="$MAINLAND_INDEX" + export PIP_INDEX_URL="$MAINLAND_INDEX" +fi +export PATH="$BIN:$PATH" + +say "Installing Python $PYTHON_VERSION via uv" +"$UV_EXE" python install "$PYTHON_VERSION" +PYTHON_EXE="$($UV_EXE python find "$PYTHON_VERSION")" +[[ -x "$PYTHON_EXE" ]] || die "uv installed Python but executable was not found" +ok "Python: $($PYTHON_EXE --version)" +PYTHON_DIR="$(dirname "$PYTHON_EXE")" +export PATH="$BIN:$PYTHON_DIR:$PATH" + +# git: macOS/Linux use system git. In mainland mode it is not required for source fetch. +GIT_EXE="" +if command -v git >/dev/null 2>&1; then + GIT_EXE="$(command -v git)" + ok "git: $($GIT_EXE --version)" +elif [[ "$GLOBAL" == "1" ]]; then + die "GLOBAL=1 requires git. Install git with your system package manager, then rerun." +else + say "git not found; continuing because mainland mode uses VPS zip. Install git later if needed." +fi + +# Fetch/update GenericAgent source. +if [[ "$GLOBAL" == "1" ]]; then + say "Cloning GenericAgent from GitHub" + if [[ -n "$(find "$GA_DIR" -mindepth 1 -maxdepth 1 ! -name .portable -print -quit)" ]]; then + [[ "$FORCE" == "1" ]] || die "Install dir contains files. Re-run with FORCE=1 to replace source while preserving portable tools." + remove_source_files + fi + TMP_CLONE="$CACHE/ga-clone" + rm -rf "$TMP_CLONE" + "$GIT_EXE" clone --depth 1 "$REPO_URL" "$TMP_CLONE" + copy_contents "$TMP_CLONE" "$GA_DIR" + rm -rf "$TMP_CLONE" +else + say "Downloading GenericAgent package from VPS" + download "$GA_ZIP_URL" "$GA_ZIP" + extract_zip_clean "$GA_ZIP" "$GA_EXTRACT" + SRC_DIR="$GA_EXTRACT/GenericAgent" + [[ -d "$SRC_DIR" ]] || SRC_DIR="$GA_EXTRACT" + remove_source_files + copy_contents "$SRC_DIR" "$GA_DIR" +fi +ok "GenericAgent source ready: $GA_DIR" + +# Install basic dependencies and project in editable mode into portable Python. +say "Installing GenericAgent dependencies via uv pip" +install_args=(pip install --python "$PYTHON_EXE" --break-system-packages) +if [[ "$GLOBAL" != "1" ]]; then install_args+=(--index-url "$MAINLAND_INDEX"); fi +install_args+=("${DEPS[@]}") +"$UV_EXE" "${install_args[@]}" + +if [[ -f "$GA_DIR/pyproject.toml" ]]; then + project_args=(pip install --python "$PYTHON_EXE" --break-system-packages) + if [[ "$GLOBAL" != "1" ]]; then project_args+=(--index-url "$MAINLAND_INDEX"); fi + project_args+=(-e "$GA_DIR") + "$UV_EXE" "${project_args[@]}" +fi + +# Try-install pywebview (optional UI). Failure is non-fatal. +say "Attempting to install pywebview (optional, failure is OK)" +webview_args=(pip install --python "$PYTHON_EXE" --break-system-packages) +if [[ "$GLOBAL" != "1" ]]; then webview_args+=(--index-url "$MAINLAND_INDEX"); fi +webview_args+=("pywebview>=4.0") +if "$UV_EXE" "${webview_args[@]}" 2>/dev/null; then + ok "pywebview installed successfully" +else + printf '\033[33m[warn]\033[0m pywebview install failed. This is optional.\n' + printf ' On Linux, pywebview requires system GTK/WebKit libraries.\n' + printf ' Install them first, e.g.:\n' + printf ' Debian/Ubuntu: sudo apt install python3-gi gir1.2-webkit2-4.1 libgirepository1.0-dev\n' + printf ' Fedora: sudo dnf install python3-gobject webkit2gtk4.1\n' + printf ' macOS: usually works out of the box (uses PyObjC)\n' + printf ' Then retry: uv pip install pywebview\n' +fi + +# Activation script: portable paths are intentionally before system PATH. +if [[ "$GLOBAL" == "1" ]]; then + cat > "$ENV_SH" < "$ENV_SH" < mykey.py" + fi +fi + +# Final banner +echo "" +if [[ "$GLOBAL" == "1" ]]; then + cat <