feat: add one-click install scripts for Linux and Windows

- Support multiple install sources: npm, GitHub Release (official/fork)
- Auto-detect installed version and support upgrade across sources
- Auto-match system architecture when downloading from GitHub Release
- Create systemd services (Linux) and scheduled tasks (Windows)
- Update README with quick-start one-liner commands

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <[email protected]>
This commit is contained in:
i
2026-06-01 22:57:55 +08:00
co-authored by HAPI
parent 4759787276
commit 4f2a71c3e3
4 changed files with 2091 additions and 0 deletions
+23
View File
@@ -8,6 +8,7 @@
## 目录 ## 目录
- [快速开始(一键安装)](#快速开始一键安装)
- [服务端部署](#服务端部署) - [服务端部署](#服务端部署)
- [客户端配置](#客户端配置) - [客户端配置](#客户端配置)
- [安装 CLI](#安装-cli) - [安装 CLI](#安装-cli)
@@ -20,6 +21,28 @@
--- ---
## 快速开始(一键安装)
一键完成全部环境搭建:Node.js v22 + HAPI CLI(多来源可选)+ Hub Docker + 开机自启。
### Linux
```bash
curl -fsSL https://raw.githubusercontent.com/arkylin/Hapi-Docker/main/scripts/install.sh | sudo bash
```
### Windows
以管理员身份打开 PowerShell:
```powershell
irm https://raw.githubusercontent.com/arkylin/Hapi-Docker/main/scripts/install.ps1 | iex
```
脚本支持**多来源安装**(npm / 官方 GitHub Release / 本仓库 GitHub Release)、**版本检测**、**自动升级**。详见 [`scripts/README.md`](scripts/README.md)。
---
## 服务端部署 ## 服务端部署
在服务器上执行以下步骤。 在服务器上执行以下步骤。
+222
View File
@@ -0,0 +1,222 @@
# Scripts
HAPI 一键安装脚本,自动完成 Node.js v22、HAPI CLI(多来源可选)、Hub Docker 部署以及 Hub & Runner 开机自启的全套配置。
**核心特性:**
- **多来源安装**:npm registry / 官方 GitHub Release / 本仓库 GitHub Release
- **版本检测**:自动检测已安装版本,智能比较远程最新版本
- **多途径升级**:支持跨来源切换升级,重复运行脚本即可更新
- **架构自适应**:自动检测系统平台与架构,匹配对应二进制
---
## 脚本列表
| 脚本 | 平台 | 说明 |
|------|------|------|
| `install.sh` | Linux (systemd) | Bash 脚本,支持 Debian/Ubuntu、RHEL/CentOS、Alpine 等 |
| `install.ps1` | Windows | PowerShell 脚本,需要管理员权限 |
---
## 安装来源对比
| 来源 | 说明 | 需要 Node.js | 适用场景 |
|------|------|-------------|----------|
| **npm** | `npm install -g @twsxtd/hapi` | ✅ | 官方推荐,自动处理依赖 |
| **github-official** | 从 [tiann/hapi](https://github.com/tiann/hapi/releases) 下载二进制 | ❌ | 独立可执行文件,无需 Node.js 运行时 |
| **github-fork** | 从 [arkylin/Hapi-Docker](https://github.com/arkylin/Hapi-Docker/releases) 下载二进制 | ❌ | 本仓库预构建版本 |
---
## 使用方法
### Linux
```bash
# 交互式安装(默认提示选择来源)
sudo bash install.sh
# 指定安装来源
sudo bash install.sh --source npm # npm registry
sudo bash install.sh --source github-official # 官方 GitHub Release
sudo bash install.sh --source github-fork # 本仓库 GitHub Release
# 仅升级(已是最新则跳过)
sudo bash install.sh --upgrade
# 强制重新安装(即使版本相同)
sudo bash install.sh --force
# 仅安装/升级 CLI,跳过 Hub Docker 配置
sudo bash install.sh --skip-hub
# 显示帮助
sudo bash install.sh --help
```
### Windows
以**管理员身份**打开 PowerShell:
```powershell
# 交互式安装
.\install.ps1
# 指定安装来源
.\install.ps1 -Source npm
.\install.ps1 -Source github-official
.\install.ps1 -Source github-fork
# 仅升级
.\install.ps1 -Upgrade
# 强制重新安装
.\install.ps1 -Force
# 仅安装/升级 CLI
.\install.ps1 -SkipHub
```
---
## 脚本流程
```
1. 前置检查 → root/管理员权限、系统架构、必要工具
2. 选择安装来源 → 交互式选择或 --source 参数指定
3. 获取远程版本 → npm view / GitHub API 查询最新版本
4. 检测已安装 → 读取本地版本、来源、安装路径
5. 版本比较 → 远程 vs 本地,决定升级/跳过/切换来源
6. 安装/升级 CLI → 按来源下载/安装,记录版本和来源
7. 安装 Node.js → npm 来源时自动安装 v22(GitHub 来源跳过)
8. 检查 Docker → 未安装则自动安装(Linux)/ 提示安装(Windows)
9. 配置 Hub → docker-compose.yml + .env,启动容器
10. 开机自启 → systemd 服务(Linux)/ 计划任务(Windows)
11. 返回结果 → 地址、Token、状态、常用命令
```
---
## 版本管理
脚本会在安装目录下记录元数据:
| 文件 | 说明 |
|------|------|
| `.hapi-version` | 已安装的 HAPI CLI 版本号 |
| `.hapi-source` | 安装来源(`npm` / `github:tiann/hapi` / `github:arkylin/Hapi-Docker`)|
重复运行脚本时会自动读取这些信息,判断是否需要升级。
### 升级场景
```bash
# 场景 1: 保持当前来源升级
sudo bash install.sh --upgrade
# 场景 2: 切换到 GitHub Release 安装
sudo bash install.sh --source github-official
# 场景 3: 已是最新,强制重新安装
sudo bash install.sh --force
```
---
## 安装后管理
### Linux (systemd)
```bash
# 查看状态
systemctl status hapi-hub
systemctl status hapi-runner
# 启停控制
systemctl start|stop|restart hapi-hub
systemctl start|stop|restart hapi-runner
# 禁用开机自启
systemctl disable hapi-hub
systemctl disable hapi-runner
# 查看日志
journalctl -u hapi-hub -f
journalctl -u hapi-runner -f
docker compose -f /opt/hapi/docker-compose.yml logs -f
```
### Windows (计划任务)
```powershell
# 查看状态
Get-ScheduledTask -TaskName "HAPI Hub"
Get-ScheduledTask -TaskName "HAPI Runner"
# 启停控制
Start-ScheduledTask -TaskName "HAPI Hub"
Stop-ScheduledTask -TaskName "HAPI Hub"
# 删除计划任务
Unregister-ScheduledTask -TaskName "HAPI Hub" -Confirm:$false
Unregister-ScheduledTask -TaskName "HAPI Runner" -Confirm:$false
# 查看日志
docker compose -f C:\hapi\docker-compose.yml logs -f
```
---
## 安装目录
| 平台 | 路径 | 说明 |
|------|------|------|
| Linux | `/opt/hapi/` | Hub 配置、docker-compose、数据卷 |
| Linux | `/opt/hapi/bin/` | GitHub Release 方式安装的 HAPI CLI |
| Linux | `/opt/hapi/hapi-data/` | SQLite 数据库、Hub 持久化数据 |
| Linux | `/etc/profile.d/hapi.sh` | PATH 环境变量配置 |
| Linux | `/tmp/hapi-install.log` | 安装日志 |
| Windows | `C:\hapi\` | Hub 配置、docker-compose、数据卷 |
| Windows | `C:\hapi\bin\` | GitHub Release 方式安装的 HAPI CLI |
| Windows | `C:\hapi\hapi-data\` | SQLite 数据库、Hub 持久化数据 |
| Windows | `%TEMP%\hapi-install.log` | 安装日志 |
---
## 环境变量说明
脚本自动生成的 `.env` 文件:
| 变量 | 必填 | 默认值 | 说明 |
|------|------|--------|------|
| `CLI_API_TOKEN` | 是 | 自动生成 | Hub 认证令牌 |
| `HAPI_PUBLIC_URL` | 否 | 自动检测 | Hub 公网地址 |
| `CORS_ORIGINS` | 否 | `*` | CORS 允许的域名 |
| `TZ` | 否 | `Asia/Shanghai` | 时区 |
---
## 常见问题
### GitHub API rate limit
未认证的 GitHub API 请求有每小时 60 次的限制。如遇此错误:
```bash
# Linux: 设置 GITHUB_TOKEN 后重试
export GITHUB_TOKEN=your_token_here
sudo -E bash install.sh --source github-official
```
### npm 全局路径不在 PATH
脚本会自动检测并添加。如仍失败,手动添加:
```bash
# Linux
export PATH="$(npm config get prefix)/bin:$PATH"
# Windows (PowerShell)
$env:Path += ";$(npm config get prefix)"
```
+806
View File
@@ -0,0 +1,806 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
HAPI 一键安装脚本 (Windows)
多来源安装 | 版本检测 | 自动升级 | 开机自启
.DESCRIPTION
自动完成以下步骤:
1. 检测已安装的 HAPI CLI(版本、来源)
2. 选择安装来源(npm / GitHub Release 官方 / GitHub Release 本仓库)
3. 检测/安装 Node.js v22(npm 来源时)
4. 安装/升级 HAPI CLI(支持多途径)
5. 检查 Docker
6. 配置并启动 HAPI Hub (Docker)
7. 创建 Hub & Runner 开机自启计划任务
.PARAMETER Source
指定 HAPI CLI 安装来源:npm, github-official, github-fork
.PARAMETER Force
强制重新安装(即使版本相同)
.PARAMETER Upgrade
仅升级模式(已是最新则退出)
.PARAMETER SkipHub
跳过 Hub Docker 配置(仅安装/升级 CLI)
.EXAMPLE
.\install.ps1
.\install.ps1 -Source npm
.\install.ps1 -Source github-official
.\install.ps1 -Force
.\install.ps1 -Upgrade
.\install.ps1 -SkipHub
#>
[CmdletBinding()]
param(
[ValidateSet("npm", "github-official", "github-fork", "")]
[string]$Source = "",
[switch]$Force,
[switch]$Upgrade,
[switch]$SkipHub
)
$ErrorActionPreference = "Stop"
# ─────────────────────────────────────────
# 配置常量
# ─────────────────────────────────────────
$INSTALL_DIR = "C:\hapi"
$HAPI_BIN_DIR = "$INSTALL_DIR\bin"
$HAPI_DATA = "$INSTALL_DIR\hapi-data"
$LOG_FILE = "$env:TEMP\hapi-install.log"
$VERSION_FILE = "$INSTALL_DIR\.hapi-version"
$SOURCE_FILE = "$INSTALL_DIR\.hapi-source"
$NODE_MIN_MAJOR = 22
$NPM_PACKAGE = "@twsxtd/hapi"
$GITHUB_OFFICIAL_REPO = "tiann/hapi"
$GITHUB_FORK_REPO = "arkylin/Hapi-Docker"
# ─────────────────────────────────────────
# 输出函数
# ─────────────────────────────────────────
function Write-Step { param($msg) Write-Host "[STEP] $msg" -ForegroundColor Blue; "[STEP] $msg" | Out-File $LOG_FILE -Append }
function Write-Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan; "[INFO] $msg" | Out-File $LOG_FILE -Append }
function Write-Ok { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green; "[OK] $msg" | Out-File $LOG_FILE -Append }
function Write-Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow; "[WARN] $msg" | Out-File $LOG_FILE -Append }
function Write-Err { param($msg) Write-Host "[ERR] $msg" -ForegroundColor Red; "[ERR] $msg" | Out-File $LOG_FILE -Append }
# ─────────────────────────────────────────
# 刷新环境变量 PATH
# ─────────────────────────────────────────
function Update-EnvPath {
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("Path", "User")
}
# 持久化添加 PATH
function Add-ToPath {
param([string]$dir)
$current = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
if ($current -notlike "*$dir*") {
[System.Environment]::SetEnvironmentVariable("Path", "$current;$dir", "Machine")
$env:Path = "$env:Path;$dir"
Write-Ok "已添加 $dir 到系统 PATH"
}
}
# ─────────────────────────────────────────
# 平台/架构检测
# ─────────────────────────────────────────
$DETECTED_PLATFORM = "windows"
$DETECTED_ARCH = switch ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture) {
X64 { "x64" }
Arm64 { "arm64" }
X86 { "x86" }
Arm { "armv7l" }
default { "x64" }
}
# ─────────────────────────────────────────
# 版本号比较
# 返回: 0(相等), 1(v1>v2), 2(v1<v2)
# ─────────────────────────────────────────
function Compare-Versions {
param([string]$v1, [string]$v2)
$a = ($v1 -replace '^v','') -split '\.' | ForEach-Object { ($_ -replace '-.*','') -replace '[^0-9]','' }
$b = ($v2 -replace '^v','') -split '\.' | ForEach-Object { ($_ -replace '-.*','') -replace '[^0-9]','' }
for ($i = 0; $i -lt [Math]::Max($a.Count, $b.Count); $i++) {
$n1 = if ($i -lt $a.Count) { [int]$a[$i] } else { 0 }
$n2 = if ($i -lt $b.Count) { [int]$b[$i] } else { 0 }
if ($n1 -gt $n2) { return 1 }
if ($n1 -lt $n2) { return 2 }
}
return 0
}
# ─────────────────────────────────────────
# 检测已安装的 HAPI CLI
# ─────────────────────────────────────────
$INSTALLED_VERSION = $null
$INSTALLED_SOURCE = $null
function Detect-Installed {
$hapiCmd = Get-Command "hapi" -ErrorAction SilentlyContinue
if ($hapiCmd) {
$script:INSTALLED_VERSION = ((& hapi --version 2>$null) | Select-String -Pattern '\d+\.\d+\.\d+' | ForEach-Object { $_.Matches.Value } | Select-Object -First 1)
if (-not $script:INSTALLED_VERSION) { $script:INSTALLED_VERSION = "unknown" }
$path = $hapiCmd.Source
if ($path -like "$script:HAPI_BIN_DIR*") {
if (Test-Path $script:SOURCE_FILE) {
$script:INSTALLED_SOURCE = (Get-Content $script:SOURCE_FILE -Raw).Trim()
} else {
$script:INSTALLED_SOURCE = "github-unknown"
}
} else {
$npmPrefix = (& npm config get prefix 2>$null).Trim()
if ($path -like "$npmPrefix*") {
$script:INSTALLED_SOURCE = "npm"
} else {
$script:INSTALLED_SOURCE = "unknown"
}
}
if ($script:INSTALLED_VERSION -eq "unknown" -and (Test-Path $script:VERSION_FILE)) {
$script:INSTALLED_VERSION = (Get-Content $script:VERSION_FILE -Raw).Trim()
}
}
}
# ─────────────────────────────────────────
# 获取远程最新版本
# ─────────────────────────────────────────
function Get-RemoteNpmVersion {
try {
$ver = (& npm view $script:NPM_PACKAGE version 2>$null).Trim()
if ($ver) {
Write-Ok "npm registry 最新版本: v$ver"
return $ver
}
} catch {}
Write-Warn "无法从 npm registry 获取版本"
return $null
}
function Get-RemoteGithubVersion {
param([string]$repo)
$apiUrl = "https://api.github.com/repos/$repo/releases/latest"
try {
$response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -ErrorAction Stop
$ver = $response.tag_name -replace '^v',''
Write-Ok "GitHub Release 最新版本: v$ver"
return $ver
} catch {
if ($_.Exception.Response.StatusCode -eq 403) {
Write-Warn "GitHub API rate limit exceeded,请设置 GITHUB_TOKEN 或稍后重试"
} else {
Write-Warn "无法连接 GitHub API: $_"
}
return $null
}
}
# ─────────────────────────────────────────
# GitHub Release Asset 匹配
# ─────────────────────────────────────────
function Match-GithubAsset {
param([string]$repo)
$apiUrl = "https://api.github.com/repos/$repo/releases/latest"
try {
$release = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -ErrorAction Stop
} catch {
Write-Err "无法获取 GitHub Release 信息: $_"
return $null
}
$platformKw = switch ($script:DETECTED_PLATFORM) {
"windows" { @("windows", "win") }
default { @("windows", "win") }
}
$archKw = switch ($script:DETECTED_ARCH) {
"x64" { @("x64", "x86_64", "amd64") }
"arm64" { @("arm64", "aarch64") }
"x86" { @("x86", "i386", "i686", "ia32") }
"armv7l" { @("armv7l", "armv7", "arm") }
default { @("x64", "x86_64", "amd64") }
}
$excludeKw = @("source", "src", "sources", "sha256", "checksum", "asc", "sig")
$bestScore = -1
$bestAsset = $null
foreach ($asset in $release.assets) {
$name = $asset.name.ToLower()
# 排除源码包
$isExcluded = $false
foreach ($k in $excludeKw) { if ($name -like "*$k*") { $isExcluded = $true; break } }
if ($isExcluded) { continue }
$score = 0
# 平台匹配
foreach ($k in $platformKw) { if ($name -like "*$k*") { $score += 10; break } }
# 架构匹配
foreach ($k in $archKw) { if ($name -like "*$k*") { $score += 10; break } }
# 不是压缩包加分
if (-not ($name -match '\.(tar\.gz|tgz|zip|7z)$')) { $score += 5 }
# musl 减分
if ($name -like "*musl*") { $score -= 3 }
if ($score -gt $bestScore) {
$bestScore = $score
$bestAsset = $asset
}
}
if (-not $bestAsset) {
Write-Err "未找到适合 $($script:DETECTED_PLATFORM)-$($script:DETECTED_ARCH) 的 GitHub Release asset"
Write-Info "可用 assets:"
$release.assets | ForEach-Object { Write-Info " - $($_.name)" }
return $null
}
Write-Ok "匹配到 asset: $($bestAsset.name)"
return $bestAsset
}
# ─────────────────────────────────────────
# 从 GitHub Release 下载并安装
# ─────────────────────────────────────────
function Install-FromGithub {
param([string]$repo)
Write-Info "从 GitHub Release ($repo) 下载 HAPI CLI..."
$asset = Match-GithubAsset -repo $repo
if (-not $asset) { return $false }
New-Item -ItemType Directory -Force -Path $script:HAPI_BIN_DIR | Out-Null
$downloadPath = Join-Path $env:TEMP $asset.name
Write-Info "下载: $($asset.browser_download_url)"
try {
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $downloadPath -UseBasicParsing -TimeoutSec 60
} catch {
Write-Err "下载失败: $_"
return $false
}
Write-Ok "下载完成: $($asset.name)"
$binPath = Join-Path $script:HAPI_BIN_DIR "hapi.exe"
# 解压(如果需要)
if ($asset.name -match '\.(tar\.gz|tgz)$') {
Write-Info "解压 tar.gz..."
# PowerShell 7+ 支持 tar
if ($PSVersionTable.PSVersion.Major -ge 7) {
tar -xzf $downloadPath -C $script:HAPI_BIN_DIR
} else {
# 使用 7zip 或 expand
$tarPath = "$env:TEMP\hapi-temp"
New-Item -ItemType Directory -Force -Path $tarPath | Out-Null
# 尝试用 tar(Windows 10 1803+ 内置)
& tar -xzf $downloadPath -C $tarPath 2>$null
$extracted = Get-ChildItem -Path $tarPath -Filter "hapi.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($extracted) {
Move-Item $extracted.FullName $binPath -Force
}
Remove-Item $tarPath -Recurse -Force -ErrorAction SilentlyContinue
}
Remove-Item $downloadPath -ErrorAction SilentlyContinue
} elseif ($asset.name -match '\.zip$') {
Write-Info "解压 zip..."
$zipPath = "$env:TEMP\hapi-zip"
New-Item -ItemType Directory -Force -Path $zipPath | Out-Null
Expand-Archive -Path $downloadPath -DestinationPath $zipPath -Force
$extracted = Get-ChildItem -Path $zipPath -Filter "hapi.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($extracted) {
Move-Item $extracted.FullName $binPath -Force
}
Remove-Item $zipPath -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $downloadPath -ErrorAction SilentlyContinue
} else {
# 直接可执行文件
Move-Item $downloadPath $binPath -Force
}
if (-not (Test-Path $binPath)) {
Write-Err "安装后未找到 hapi.exe"
return $false
}
# 添加到 PATH
Add-ToPath -dir $script:HAPI_BIN_DIR
Update-EnvPath
# 验证
$installedVer = ((& $binPath --version 2>$null) | Select-String -Pattern '\d+\.\d+\.\d+' | ForEach-Object { $_.Matches.Value } | Select-Object -First 1)
if (-not $installedVer) {
Write-Warn "无法验证版本,使用远程版本号"
$installedVer = $script:REMOTE_VERSION
}
# 记录版本和来源
$installedVer | Set-Content $script:VERSION_FILE -Encoding UTF8
"github:$repo" | Set-Content $script:SOURCE_FILE -Encoding UTF8
Write-Ok "HAPI CLI v$installedVer 安装成功 (来源: github:$repo)"
return $true
}
# ─────────────────────────────────────────
# 从 npm 安装
# ─────────────────────────────────────────
function Install-FromNpm {
Write-Info "从 npm registry 安装 HAPI CLI..."
npm install -g $script:NPM_PACKAGE --registry=https://registry.npmjs.org | Out-File $LOG_FILE -Append
Update-EnvPath
$hapi = Get-Command "hapi" -ErrorAction SilentlyContinue
if (-not $hapi) {
$npmPrefix = (& npm config get prefix).Trim()
$npmBin = Join-Path $npmPrefix ""
[System.Environment]::SetEnvironmentVariable("Path", "$env:Path;$npmBin", "User")
Update-EnvPath
$hapi = Get-Command "hapi" -ErrorAction SilentlyContinue
}
if (-not $hapi) {
Write-Err "HAPI CLI 安装失败"
return $false
}
$installedVer = ((& hapi --version 2>$null) | Select-String -Pattern '\d+\.\d+\.\d+' | ForEach-Object { $_.Matches.Value } | Select-Object -First 1)
if (-not $installedVer) { $installedVer = "unknown" }
$installedVer | Set-Content $script:VERSION_FILE -Encoding UTF8
"npm" | Set-Content $script:SOURCE_FILE -Encoding UTF8
Write-Ok "HAPI CLI v$installedVer 安装成功 (来源: npm, 路径: $($hapi.Source))"
return $true
}
# ─────────────────────────────────────────
# 选择安装来源
# ─────────────────────────────────────────
$SELECTED_SOURCE = $null
$REMOTE_VERSION = $null
function Select-InstallSource {
# 如果参数指定了来源
if ($Source) {
$script:SELECTED_SOURCE = switch ($Source) {
"npm" { "npm" }
"github-official" { "github-official" }
"github-fork" { "github-fork" }
default { "npm" }
}
return
}
# 如果非交互式,默认 npm
if (-not [System.Console]::IsInputRedirected -and -not [Environment]::UserInteractive) {
Write-Info "非交互式环境,默认使用 npm 来源"
$script:SELECTED_SOURCE = "npm"
return
}
Write-Host ""
Write-Host "请选择 HAPI CLI 安装来源:" -ForegroundColor White
Write-Host ""
Write-Host " 1) npm registry (官方推荐)"
Write-Host " $NPM_PACKAGE"
Write-Host ""
Write-Host " 2) GitHub Release (官方仓库)"
Write-Host " https://github.com/$GITHUB_OFFICIAL_REPO/releases"
Write-Host ""
Write-Host " 3) GitHub Release (本仓库)"
Write-Host " https://github.com/$GITHUB_FORK_REPO/releases"
Write-Host ""
do {
$choice = Read-Host "请输入选项 [1-3]"
switch ($choice) {
"1" { $script:SELECTED_SOURCE = "npm"; break }
"2" { $script:SELECTED_SOURCE = "github-official"; break }
"3" { $script:SELECTED_SOURCE = "github-fork"; break }
default { Write-Warn "无效选项,请重新输入" }
}
} while (-not $script:SELECTED_SOURCE)
}
# ─────────────────────────────────────────
# 处理安装/升级逻辑
# ─────────────────────────────────────────
function Handle-HapiInstall {
Write-Step "处理 HAPI CLI 安装/升级"
# 1. 选择来源
Select-InstallSource
Write-Info "安装来源: $SELECTED_SOURCE"
# 2. 获取远程版本
$remoteVer = switch ($SELECTED_SOURCE) {
"npm" { Get-RemoteNpmVersion }
"github-official" { Get-RemoteGithubVersion -repo $GITHUB_OFFICIAL_REPO }
"github-fork" { Get-RemoteGithubVersion -repo $GITHUB_FORK_REPO }
}
if (-not $remoteVer) {
Write-Err "无法获取远程版本,安装中止"
return $false
}
$script:REMOTE_VERSION = $remoteVer
# 3. 检查已安装状态
Detect-Installed
if ($INSTALLED_VERSION -and $INSTALLED_VERSION -ne "unknown") {
Write-Host ""
Write-Info "检测到已安装 HAPI CLI:"
Write-Host " 版本: $INSTALLED_VERSION" -ForegroundColor White
Write-Host " 来源: $INSTALLED_SOURCE" -ForegroundColor White
Write-Host " 路径: $((Get-Command hapi -ErrorAction SilentlyContinue).Source || 'unknown')" -ForegroundColor White
Write-Host " 远程: v$remoteVer" -ForegroundColor White
Write-Host ""
$cmp = Compare-Versions -v1 $INSTALLED_VERSION -v2 $remoteVer
if ($cmp -eq 0) {
Write-Ok "当前已是最新版本 (v$INSTALLED_VERSION)"
if ($Force) {
Write-Info "-Force 已设置,强制重新安装"
} elseif ($Upgrade) {
Write-Info "-Upgrade 模式,已是最新,跳过"
return $true
} else {
if ($INSTALLED_SOURCE -ne $SELECTED_SOURCE) {
$confirm = Read-Host "是否切换到 $SELECTED_SOURCE 来源重新安装? [y/N]"
if ($confirm -notmatch '^[Yy]$') {
Write-Info "跳过 HAPI CLI 安装"
return $true
}
} else {
return $true
}
}
} elseif ($cmp -eq 1) {
Write-Warn "本地版本 (v$INSTALLED_VERSION) 高于远程 (v$remoteVer)"
if (-not $Force -and $Upgrade) { return $true }
if (-not $Force) {
$confirm = Read-Host "是否降级到远程版本 v$remoteVer? [y/N]"
if ($confirm -notmatch '^[Yy]$') { Write-Info "跳过"; return $true }
}
} else {
Write-Info "发现新版本: v$remoteVer (当前: v$INSTALLED_VERSION)"
if ($Upgrade) { Write-Info "执行升级..." }
}
} else {
Write-Info "未检测到已安装的 HAPI CLI,开始安装 v$remoteVer"
}
# 4. 执行安装
switch ($SELECTED_SOURCE) {
"npm" { Install-FromNpm }
"github-official" { Install-FromGithub -repo $GITHUB_OFFICIAL_REPO }
"github-fork" { Install-FromGithub -repo $GITHUB_FORK_REPO }
}
}
# ─────────────────────────────────────────
# 安装 Node.js v22
# ─────────────────────────────────────────
function Install-NodeJS {
if ($SELECTED_SOURCE -ne "npm") {
Write-Info "使用 GitHub Release 来源,跳过 Node.js 检查"
return
}
Write-Step "检查 Node.js"
Write-Info "检查 Node.js 版本..."
$node = Get-Command "node" -ErrorAction SilentlyContinue
if ($node) {
$verStr = (& node -v).Trim()
$major = [int]($verStr -replace '^v','' -split '\.' | Select-Object -First 1)
if ($major -ge $NODE_MIN_MAJOR) {
Write-Ok "Node.js $verStr 已满足要求"
return
}
Write-Warn "当前 Node.js $verStr,需要升级到 v$NODE_MIN_MAJOR+"
} else {
Write-Info "未检测到 Node.js,开始安装 v$NODE_MIN_MAJOR..."
}
$winget = Get-Command "winget" -ErrorAction SilentlyContinue
if ($winget) {
Write-Info "通过 winget 安装 Node.js LTS (v22)..."
winget install OpenJS.NodeJS.LTS --accept-source-agreements --accept-package-agreements | Out-File $LOG_FILE -Append
} else {
Write-Info "通过 MSI 安装 Node.js v22..."
$msiUrl = "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi"
$msiPath = "$env:TEMP\node-v22.msi"
Invoke-WebRequest -Uri $msiUrl -OutFile $msiPath -UseBasicParsing
Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" /qn /norestart" -Wait
Remove-Item $msiPath -ErrorAction SilentlyContinue
}
Update-EnvPath
$node = Get-Command "node" -ErrorAction SilentlyContinue
if (-not $node) {
$possiblePaths = @(
"C:\Program Files\nodejs\node.exe",
"C:\Program Files (x86)\nodejs\node.exe"
)
foreach ($p in $possiblePaths) {
if (Test-Path $p) {
$nodeDir = Split-Path $p
[System.Environment]::SetEnvironmentVariable("Path", "$env:Path;$nodeDir", "User")
Update-EnvPath
break
}
}
}
$node = Get-Command "node" -ErrorAction SilentlyContinue
if (-not $node) {
Write-Err "Node.js 安装失败"
return
}
Write-Ok "Node.js $(node -v) 安装成功"
}
# ─────────────────────────────────────────
# 检查 Docker
# ─────────────────────────────────────────
function Install-Docker {
Write-Step "检查 Docker"
$docker = Get-Command "docker" -ErrorAction SilentlyContinue
if ($docker) {
Write-Ok "Docker 已安装"
return
}
Write-Warn "未检测到 Docker"
Write-Warn "请手动安装 Docker Desktop: https://docs.docker.com/desktop/install/windows-install/"
Write-Warn "安装完成后请重新运行此脚本"
exit 1
}
# ─────────────────────────────────────────
# 配置 HAPI Hub
# ─────────────────────────────────────────
function Setup-Hub {
if ($SkipHub) { Write-Info "-SkipHub 已设置,跳过 Hub 配置"; return }
Write-Step "配置 HAPI Hub"
New-Item -ItemType Directory -Force -Path $INSTALL_DIR | Out-Null
New-Item -ItemType Directory -Force -Path $HAPI_DATA | Out-Null
$envPath = Join-Path $INSTALL_DIR ".env"
if (Test-Path $envPath) {
$content = Get-Content $envPath -Raw
$tokenMatch = [regex]::Match($content, 'CLI_API_TOKEN=(.+?)\r?\n')
if ($tokenMatch.Success) {
$script:CLI_TOKEN = $tokenMatch.Groups[1].Value.Trim()
}
Write-Ok "检测到已有配置文件"
} else {
$inputToken = Read-Host "请输入 CLI_API_TOKEN(留空自动生成)"
if ([string]::IsNullOrWhiteSpace($inputToken)) {
$script:CLI_TOKEN = -join ((1..64) | ForEach-Object { '{0:x}' -f (Get-Random -Minimum 0 -Maximum 16) })
} else {
$script:CLI_TOKEN = $inputToken
}
$publicIp = ""
try { $publicIp = Invoke-RestMethod -Uri "https://api.ipify.org" -TimeoutSec 5 }
catch { $publicIp = "localhost" }
$envContent = @"
# HAPI Hub 配置
# 生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
CLI_API_TOKEN=$script:CLI_TOKEN
HAPI_PUBLIC_URL=http://${publicIp}:3006
CORS_ORIGINS=*
TZ=Asia/Shanghai
"@
$envContent | Set-Content $envPath -Encoding UTF8
Write-Ok "已生成配置文件"
}
$composePath = Join-Path $INSTALL_DIR "docker-compose.yml"
$composeContent = @"
services:
hapi-hub:
image: ghcr.io/arkylin/hapi-docker:latest
container_name: hapi-hub
restart: unless-stopped
ports:
- `"3006:3006`"
volumes:
- ./hapi-data:/root/.hapi
env_file:
- .env
environment:
- HAPI_LISTEN_HOST=0.0.0.0
- HAPI_LISTEN_PORT=3006
"@
$composeContent | Set-Content $composePath -Encoding UTF8
Push-Location $INSTALL_DIR
docker compose pull | Out-File $LOG_FILE -Append
docker compose up -d | Out-File $LOG_FILE -Append
Pop-Location
Write-Info "等待 Hub 启动..."
for ($i = 0; $i -lt 30; $i++) {
try {
$resp = Invoke-WebRequest -Uri "http://localhost:3006" -TimeoutSec 2 -UseBasicParsing -ErrorAction SilentlyContinue
if ($resp.StatusCode -eq 200) { break }
} catch { }
Start-Sleep -Seconds 1
}
Write-Ok "HAPI Hub 已启动"
}
# ─────────────────────────────────────────
# 创建开机自启计划任务
# ─────────────────────────────────────────
function Setup-AutoStart {
if ($SkipHub) { Write-Info "-SkipHub 已设置,跳过计划任务"; return }
Write-Step "创建开机自启计划任务"
# Hub 启动脚本
$hubScriptPath = Join-Path $INSTALL_DIR "start-hub.ps1"
$hubScript = @"
Set-Location "$INSTALL_DIR"
docker compose up -d
"@
$hubScript | Set-Content $hubScriptPath -Encoding UTF8
# HAPI Hub 计划任务
$oldHub = Get-ScheduledTask -TaskName "HAPI Hub" -ErrorAction SilentlyContinue
if ($oldHub) { Unregister-ScheduledTask -TaskName "HAPI Hub" -Confirm:$false }
$trigger = New-ScheduledTaskTrigger -AtLogon
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$hubScriptPath`""
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 72) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
Register-ScheduledTask -TaskName "HAPI Hub" -Trigger $trigger -Action $action `
-Principal $principal -Settings $settings -Force | Out-Null
Write-Ok "HAPI Hub 计划任务已创建"
# HAPI Runner 计划任务
$oldRunner = Get-ScheduledTask -TaskName "HAPI Runner" -ErrorAction SilentlyContinue
if ($oldRunner) { Unregister-ScheduledTask -TaskName "HAPI Runner" -Confirm:$false }
$hapiHome = Join-Path $env:USERPROFILE ".hapi"
New-Item -ItemType Directory -Force -Path $hapiHome | Out-Null
$settingsJson = @"
{
`"apiUrl`": `"http://localhost:3006`",
`"cliApiToken`": `"$script:CLI_TOKEN`"
}
"@
$settingsJson | Set-Content (Join-Path $hapiHome "settings.json") -Encoding UTF8
$trigger2 = New-ScheduledTaskTrigger -AtLogon
$action2 = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument '-WindowStyle Hidden -Command "hapi runner start"'
$settings2 = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 72) -MultipleInstances IgnoreNew
$principal2 = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive
Register-ScheduledTask -TaskName "HAPI Runner" -Trigger $trigger2 -Action $action2 `
-Principal $principal2 -Settings $settings2 -Force | Out-Null
Write-Ok "HAPI Runner 计划任务已创建"
}
# ─────────────────────────────────────────
# 返回结果
# ─────────────────────────────────────────
function Print-Result {
$publicIp = "localhost"
try { $publicIp = Invoke-RestMethod -Uri "https://api.ipify.org" -TimeoutSec 5 }
catch {
$localIp = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {
$_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.254.*"
} | Select-Object -First 1 -ExpandProperty IPAddress
if ($localIp) { $publicIp = $localIp }
}
$hubTask = Get-ScheduledTask -TaskName "HAPI Hub" -ErrorAction SilentlyContinue
$runnerTask = Get-ScheduledTask -TaskName "HAPI Runner" -ErrorAction SilentlyContinue
$hubState = if ($hubTask) { $hubTask.State } else { "NotFound" }
$runnerState = if ($runnerTask) { $runnerTask.State } else { "NotFound" }
$hapiPath = (Get-Command hapi -ErrorAction SilentlyContinue).Source
if (-not $hapiPath -and (Test-Path (Join-Path $HAPI_BIN_DIR "hapi.exe"))) {
$hapiPath = Join-Path $HAPI_BIN_DIR "hapi.exe"
}
$hapiVer = if ($hapiPath) {
$rawVer = & $hapiPath --version 2>$null
($rawVer | Select-String -Pattern '\d+\.\d+\.\d+' | ForEach-Object { $_.Matches.Value } | Select-Object -First 1)
} else {
(Get-Content $VERSION_FILE -Raw).Trim()
}
$hapiSource = if (Test-Path $SOURCE_FILE) { (Get-Content $SOURCE_FILE -Raw).Trim() } else { "unknown" }
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "║ HAPI 安装完成 ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
Write-Host " HAPI CLI: v$hapiVer (来源: $hapiSource)"
Write-Host " CLI 路径: $hapiPath"
Write-Host " Hub 地址: http://${publicIp}:3006"
Write-Host " Token: $script:CLI_TOKEN"
Write-Host ""
Write-Host " 计划任务状态:"
Write-Host " HAPI Hub: $hubState"
Write-Host " HAPI Runner: $runnerState"
Write-Host ""
Write-Host " 常用命令:"
Write-Host " Get-ScheduledTask -TaskName 'HAPI Hub'"
Write-Host " Get-ScheduledTask -TaskName 'HAPI Runner'"
Write-Host " docker compose -f $composePath logs -f"
Write-Host ""
Write-Host " 升级命令:"
Write-Host " .\install.ps1 -Upgrade"
Write-Host " .\install.ps1 -Source github-official -Upgrade"
Write-Host ""
Write-Host " 日志文件: $LOG_FILE"
Write-Host ""
}
# ─────────────────────────────────────────
# 前置检查
# ─────────────────────────────────────────
function Check-Prerequisites {
Write-Step "前置检查"
Write-Ok "平台: $DETECTED_PLATFORM, 架构: $DETECTED_ARCH"
# 检查必要工具
foreach ($cmd in @("curl")) {
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
Write-Err "缺少必要工具: $cmd"
exit 1
}
}
Write-Ok "基础工具检查通过"
}
# ─────────────────────────────────────────
# 主流程
# ─────────────────────────────────────────
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════╗" -ForegroundColor White
Write-Host "║ HAPI 一键安装脚本 (Windows) ║" -ForegroundColor White
Write-Host "║ 多来源安装 | 版本检测 | 自动升级 | 开机自启 ║" -ForegroundColor White
Write-Host "╚══════════════════════════════════════════════════════════════════╝" -ForegroundColor White
Write-Host ""
"" | Out-File $LOG_FILE
Check-Prerequisites
Handle-HapiInstall
Install-NodeJS
Install-Docker
Setup-Hub
Setup-AutoStart
Print-Result
+1040
View File
File diff suppressed because it is too large Load Diff