docs/backend-migration/plans/2026-05-07-m9-install-web-script.md
迁移目标: 创建 scripts/install-web.sh 一键安装脚本,从 GitHub releases 下载 aionui-web tarball,解压到指定目录,配置 PATH 环境变量,提供 aionui-web CLI 命令,为用户提供无 Electron 的 WebUI 独立安装方式。
前提条件:
aionui-web-{version}-{platform}-{arch}.tar.gz + .sha256 在 CI 中产出aionui-web/
├── bin/aionui-web.js # CLI 入口
├── dist/ # TypeScript 编译产物
├── bundled-aionui-backend/ # Backend 二进制
├── bundled-bun/ # Bun 运行时
├── static/ # 前端静态文件
└── package.json
核心任务:
scripts/install-web.sh bash 脚本,支持 5 个平台(darwin-arm64/x64, linux-x86_64/aarch64, win-x86_64)~/.local/share/aionui-web/)~/.local/bin/aionui-web 指向 CLI 入口--version, --mirror, --install-dir, --no-symlink, --no-pathinstall-web.sh 作为 release artifact 上传curl | bash 安装流程(linux-x86_64)__VERSION__ 占位符 + CI 中 sed 替换为实际版本号目的: 确认 M8 交付物可用,记录 M9 起点状态。
操作:
确认分支基于 M8:
git fetch origin
git checkout -b feat/m9-install-web-script origin/feat/m8-web-cli-tarball
git log --oneline -1
验证 M8 交付物:
# 检查 M8 handoff 文档(如果存在)
ls -la docs/backend-migration/handoffs/M8-outcome.md
# 检查 M8 产出的 pack-web-cli 脚本
ls -la scripts/pack-web-cli.js
# 检查 CI workflow
grep -n "pack-web-cli" .github/workflows/*.yml
确认 tarball 命名约定:
aionui-web-{version}-{platform}-{arch}.tar.gz检查现有 install-ubuntu.sh 脚本(作为参考):
cat scripts/install-ubuntu.sh | grep -A 5 "resolve_version\|download_deb\|detect_arch"
产出:
feat/m9-install-web-script 基于 M8目的: 创建 scripts/install-web.sh 骨架,定义命令行参数和核心函数结构。
操作:
创建 scripts/install-web.sh:
#!/usr/bin/env bash
# ============================================================================
# AionUi WebUI — One-Click Installation Script
# ============================================================================
# Usage:
# curl -fsSL https://raw.githubusercontent.com/iOfficeAI/AionUi/main/scripts/install-web.sh | bash
# # Or specify version:
# VERSION=1.0.0 bash install-web.sh
# # Or install to custom directory:
# INSTALL_DIR=/opt/aionui-web bash install-web.sh
# ============================================================================
set -euo pipefail
# ─── 默认配置 ───────────────────────────────────────────────────────────────
VERSION="${VERSION:-__VERSION__}" # CI 中会被 sed 替换为实际版本
INSTALL_DIR="${INSTALL_DIR:-${HOME}/.local/share/aionui-web}"
BIN_DIR="${BIN_DIR:-${HOME}/.local/bin}"
MIRROR="${MIRROR:-https://github.com/iOfficeAI/AionUi/releases/download}"
CREATE_SYMLINK="${CREATE_SYMLINK:-1}"
UPDATE_PATH="${UPDATE_PATH:-1}"
# ─── 颜色定义 ───────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ─── 辅助函数 ───────────────────────────────────────────────────────────────
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[✓]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
die() { error "$*"; exit 1; }
banner() {
echo -e "${CYAN}${BOLD}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ AionUi WebUI Installer (No Electron) ║"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"
}
# ─── 解析命令行参数 ─────────────────────────────────────────────────────────
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--version)
VERSION="$2"
shift 2
;;
--mirror)
MIRROR="$2"
shift 2
;;
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
--no-symlink)
CREATE_SYMLINK=0
shift
;;
--no-path)
UPDATE_PATH=0
shift
;;
--help)
show_help
exit 0
;;
*)
warn "Unknown option: $1"
show_help
exit 1
;;
esac
done
}
show_help() {
cat <<EOF
Usage: install-web.sh [OPTIONS]
Options:
--version <version> Specify version to install (default: latest or CI-embedded)
--mirror <url> Specify mirror URL (default: GitHub releases)
--install-dir <path> Specify installation directory (default: ~/.local/share/aionui-web)
--no-symlink Do not create symlink in ~/.local/bin
--no-path Do not add PATH to shell profile
--help Show this help message
Environment Variables:
VERSION Version to install (same as --version)
INSTALL_DIR Installation directory (same as --install-dir)
MIRROR Mirror URL (same as --mirror)
Examples:
# Install latest version
curl -fsSL https://raw.githubusercontent.com/iOfficeAI/AionUi/main/scripts/install-web.sh | bash
# Install specific version
VERSION=1.0.0 bash install-web.sh
# Install to custom directory
INSTALL_DIR=/opt/aionui-web bash install-web.sh
# Use local file mirror (for offline installation)
MIRROR=file:///path/to/releases bash install-web.sh
EOF
}
# ─── 待实现函数 ─────────────────────────────────────────────────────────────
detect_platform_arch() {
# TODO: Phase 2
:
}
resolve_version() {
# TODO: Phase 3
:
}
download_tarball() {
# TODO: Phase 4
:
}
verify_checksum() {
# TODO: Phase 5
:
}
extract_tarball() {
# TODO: Phase 6
:
}
create_symlink() {
# TODO: Phase 7
:
}
update_shell_profile() {
# TODO: Phase 8
:
}
print_summary() {
# TODO: Phase 9
:
}
# ─── 主流程 ───────────────────────────────────────────────────────────────────
main() {
banner
parse_args "$@"
# Step 1: Detect platform and architecture
detect_platform_arch
# Step 2: Resolve version (if VERSION is __VERSION__ or latest)
resolve_version
# Step 3: Download tarball
download_tarball
# Step 4: Verify SHA256 checksum
verify_checksum
# Step 5: Extract tarball
extract_tarball
# Step 6: Create symlink
if [[ "$CREATE_SYMLINK" == "1" ]]; then
create_symlink
fi
# Step 7: Update shell profile PATH
if [[ "$UPDATE_PATH" == "1" ]]; then
update_shell_profile
fi
# Step 8: Print summary
print_summary
}
# 执行
main "$@"
设置执行权限:
chmod +x scripts/install-web.sh
测试骨架:
./scripts/install-web.sh --help
# 预期:显示 help 信息
产出:
scripts/install-web.sh 骨架创建完成目的: 检测运行平台和架构,映射到 tarball 命名约定。
操作:
实现 detect_platform_arch() 函数:
detect_platform_arch() {
local os_type="$(uname -s)"
local machine="$(uname -m)"
# 映射 OS 类型
case "$os_type" in
Darwin)
PLATFORM="darwin"
;;
Linux)
PLATFORM="linux"
;;
MINGW*|MSYS*|CYGWIN*)
PLATFORM="win"
;;
*)
die "Unsupported OS: $os_type (only Darwin, Linux, Windows supported)"
;;
esac
# 映射架构
case "$machine" in
x86_64|amd64)
ARCH="x86_64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
die "Unsupported architecture: $machine (only x86_64/amd64 and aarch64/arm64 supported)"
;;
esac
info "Detected platform: ${BOLD}${PLATFORM}-${ARCH}${NC}"
# 构建 tarball 文件名
TARBALL_NAME="aionui-web-${VERSION}-${PLATFORM}-${ARCH}.tar.gz"
CHECKSUM_NAME="${TARBALL_NAME}.sha256"
}
测试平台检测:
# 在 macOS 上
./scripts/install-web.sh
# 预期输出:Detected platform: darwin-arm64 (或 darwin-x86_64)
# 在 Linux 上
docker run --rm -v $(pwd):/workspace -w /workspace debian:bookworm-slim bash scripts/install-web.sh
# 预期输出:Detected platform: linux-x86_64
产出:
detect_platform_arch() 函数实现完成目的: 解析 VERSION 变量,如果是 __VERSION__ 或 latest,则从 GitHub API 查询最新版本。
操作:
实现 resolve_version() 函数:
resolve_version() {
# 如果 VERSION 是 __VERSION__(CI 占位符)或 latest,查询 GitHub API
if [[ "$VERSION" == "__VERSION__" || "$VERSION" == "latest" ]]; then
info "Resolving latest version from GitHub API..."
if command -v curl &>/dev/null; then
VERSION=$(curl -fsSL "https://api.github.com/repos/iOfficeAI/AionUi/releases/latest" \
| grep '"tag_name"' | head -1 | sed 's/.*"v\([^"]*\)".*/\1/')
elif command -v wget &>/dev/null; then
VERSION=$(wget -qO- "https://api.github.com/repos/iOfficeAI/AionUi/releases/latest" \
| grep '"tag_name"' | head -1 | sed 's/.*"v\([^"]*\)".*/\1/')
else
die "curl or wget is required to resolve version. Please install curl or wget."
fi
if [[ -z "$VERSION" ]]; then
die "Failed to resolve latest version. Please specify version manually: VERSION=1.0.0 bash $0"
fi
info "Latest version: ${BOLD}v${VERSION}${NC}"
else
info "Using specified version: ${BOLD}v${VERSION}${NC}"
fi
# 重新构建 tarball 名称(因为 VERSION 可能已更新)
TARBALL_NAME="aionui-web-${VERSION}-${PLATFORM}-${ARCH}.tar.gz"
CHECKSUM_NAME="${TARBALL_NAME}.sha256"
}
测试版本解析:
# 测试 latest
VERSION=latest ./scripts/install-web.sh
# 预期:查询 GitHub API 并显示最新版本号
# 测试指定版本
VERSION=1.0.0 ./scripts/install-web.sh
# 预期:使用 1.0.0
# 测试 __VERSION__ 占位符
./scripts/install-web.sh
# 预期:查询 GitHub API(因为默认值是 __VERSION__)
产出:
resolve_version() 函数实现完成__VERSION__, latest, 和明确版本号目的: 从 MIRROR URL 下载 tarball 和 SHA256 校验和。
操作:
实现 download_tarball() 函数:
download_tarball() {
# 创建临时目录
TEMP_DIR="$(mktemp -d)"
TARBALL_PATH="${TEMP_DIR}/${TARBALL_NAME}"
CHECKSUM_PATH="${TEMP_DIR}/${CHECKSUM_NAME}"
# 构建下载 URL
# MIRROR 格式:
# - GitHub: https://github.com/iOfficeAI/AionUi/releases/download
# - file: file:///path/to/releases
if [[ "$MIRROR" == file://* ]]; then
# 本地文件镜像(用于离线安装或测试)
local base_path="${MIRROR#file://}"
TARBALL_URL="file://${base_path}/v${VERSION}/${TARBALL_NAME}"
CHECKSUM_URL="file://${base_path}/v${VERSION}/${CHECKSUM_NAME}"
else
# GitHub releases
TARBALL_URL="${MIRROR}/v${VERSION}/${TARBALL_NAME}"
CHECKSUM_URL="${MIRROR}/v${VERSION}/${CHECKSUM_NAME}"
fi
info "Downloading ${BOLD}${TARBALL_NAME}${NC}..."
info "URL: $TARBALL_URL"
# 下载 tarball
if [[ "$TARBALL_URL" == file://* ]]; then
# 本地文件:直接复制
local src_path="${TARBALL_URL#file://}"
if [[ ! -f "$src_path" ]]; then
die "Tarball not found at local mirror: $src_path"
fi
cp "$src_path" "$TARBALL_PATH"
else
# 远程文件:使用 curl 或 wget
if command -v curl &>/dev/null; then
curl -fSL --progress-bar -o "$TARBALL_PATH" "$TARBALL_URL" || die "Download failed"
elif command -v wget &>/dev/null; then
wget --show-progress -q -O "$TARBALL_PATH" "$TARBALL_URL" || die "Download failed"
else
die "curl or wget is required. Please install curl or wget."
fi
fi
local size
size=$(du -h "$TARBALL_PATH" | cut -f1)
success "Downloaded tarball ($size)"
# 下载 SHA256 校验和
info "Downloading ${BOLD}${CHECKSUM_NAME}${NC}..."
if [[ "$CHECKSUM_URL" == file://* ]]; then
local src_path="${CHECKSUM_URL#file://}"
if [[ ! -f "$src_path" ]]; then
die "Checksum file not found at local mirror: $src_path"
fi
cp "$src_path" "$CHECKSUM_PATH"
else
if command -v curl &>/dev/null; then
curl -fSL -o "$CHECKSUM_PATH" "$CHECKSUM_URL" || die "Checksum download failed"
elif command -v wget &>/dev/null; then
wget -q -O "$CHECKSUM_PATH" "$CHECKSUM_URL" || die "Checksum download failed"
fi
fi
success "Downloaded checksum"
}
测试下载逻辑(需要 mock 本地文件):
# 准备 mock tarball
mkdir -p /tmp/mock-releases/v1.0.0
echo "mock tarball content" > /tmp/mock-releases/v1.0.0/aionui-web-1.0.0-darwin-arm64.tar.gz
shasum -a 256 /tmp/mock-releases/v1.0.0/aionui-web-1.0.0-darwin-arm64.tar.gz > /tmp/mock-releases/v1.0.0/aionui-web-1.0.0-darwin-arm64.tar.gz.sha256
# 测试本地 file:// mirror
VERSION=1.0.0 MIRROR=file:///tmp/mock-releases ./scripts/install-web.sh
# 预期:从本地文件复制 tarball 和 checksum
产出:
download_tarball() 函数实现完成目的: 验证下载的 tarball SHA256 校验和,确保文件完整性。
操作:
实现 verify_checksum() 函数:
verify_checksum() {
info "Verifying SHA256 checksum..."
# 读取预期的 checksum(从 .sha256 文件)
local expected_checksum
expected_checksum=$(awk '{print $1}' "$CHECKSUM_PATH")
if [[ -z "$expected_checksum" ]]; then
die "Failed to read checksum from $CHECKSUM_NAME"
fi
# 计算实际的 checksum
local actual_checksum
if command -v shasum &>/dev/null; then
actual_checksum=$(shasum -a 256 "$TARBALL_PATH" | awk '{print $1}')
elif command -v sha256sum &>/dev/null; then
actual_checksum=$(sha256sum "$TARBALL_PATH" | awk '{print $1}')
else
warn "shasum/sha256sum not found, skipping checksum verification"
return
fi
if [[ "$actual_checksum" != "$expected_checksum" ]]; then
error "Checksum mismatch!"
error "Expected: $expected_checksum"
error "Actual: $actual_checksum"
die "Tarball may be corrupted. Please try again."
fi
success "Checksum verified: ${expected_checksum:0:16}..."
}
测试校验和验证:
# 准备正确的 checksum
echo "mock tarball" > /tmp/test.tar.gz
shasum -a 256 /tmp/test.tar.gz > /tmp/test.tar.gz.sha256
# 测试验证逻辑(手动调用函数)
TARBALL_PATH=/tmp/test.tar.gz
CHECKSUM_PATH=/tmp/test.tar.gz.sha256
source scripts/install-web.sh
verify_checksum
# 预期:Checksum verified
# 测试 checksum mismatch
echo "corrupted" > /tmp/test.tar.gz
verify_checksum
# 预期:Checksum mismatch! 并退出
产出:
verify_checksum() 函数实现完成目的: 解压 tarball 到安装目录,清理旧版本(如果存在)。
操作:
实现 extract_tarball() 函数:
extract_tarball() {
info "Installing to ${BOLD}${INSTALL_DIR}${NC}..."
# 如果安装目录已存在,备份旧版本
if [[ -d "$INSTALL_DIR" ]]; then
local backup_dir="${INSTALL_DIR}.backup.$(date +%s)"
warn "Installation directory exists, creating backup: $backup_dir"
mv "$INSTALL_DIR" "$backup_dir"
fi
# 创建安装目录的父目录
mkdir -p "$(dirname "$INSTALL_DIR")"
# 解压 tarball
# tarball 中的根目录是 aionui-web/,解压后重命名为 INSTALL_DIR
local extract_temp="${TEMP_DIR}/extract"
mkdir -p "$extract_temp"
info "Extracting tarball..."
tar -xzf "$TARBALL_PATH" -C "$extract_temp" || die "Failed to extract tarball"
# 移动到最终安装位置
if [[ -d "${extract_temp}/aionui-web" ]]; then
mv "${extract_temp}/aionui-web" "$INSTALL_DIR"
else
die "Tarball structure is invalid (missing aionui-web/ directory)"
fi
success "Extracted to $INSTALL_DIR"
# 设置可执行权限
chmod +x "${INSTALL_DIR}/bin/aionui-web.js" 2>/dev/null || true
# 验证安装
if [[ ! -f "${INSTALL_DIR}/bin/aionui-web.js" ]]; then
die "Installation failed: ${INSTALL_DIR}/bin/aionui-web.js not found"
fi
success "Installation completed"
# 清理临时文件
rm -rf "$TEMP_DIR"
}
测试解压逻辑(需要真实的 tarball 结构):
# 创建 mock tarball 结构
mkdir -p /tmp/mock-tarball/aionui-web/{bin,dist,bundled-aionui-backend,bundled-bun,static}
echo '#!/usr/bin/env node' > /tmp/mock-tarball/aionui-web/bin/aionui-web.js
echo '{"version":"1.0.0"}' > /tmp/mock-tarball/aionui-web/package.json
tar -czf /tmp/aionui-web-1.0.0-darwin-arm64.tar.gz -C /tmp/mock-tarball aionui-web
shasum -a 256 /tmp/aionui-web-1.0.0-darwin-arm64.tar.gz > /tmp/aionui-web-1.0.0-darwin-arm64.tar.gz.sha256
# 测试安装
VERSION=1.0.0 MIRROR=file:///tmp INSTALL_DIR=/tmp/test-install ./scripts/install-web.sh
# 预期:解压到 /tmp/test-install/
# 验证结构
ls -la /tmp/test-install/
# 预期:bin/ dist/ bundled-aionui-backend/ bundled-bun/ static/ package.json
产出:
extract_tarball() 函数实现完成目的: 在 ~/.local/bin/ 创建 symlink,使 aionui-web 命令全局可用。
操作:
实现 create_symlink() 函数:
create_symlink() {
local symlink_path="${BIN_DIR}/aionui-web"
local target_path="${INSTALL_DIR}/bin/aionui-web.js"
info "Creating symlink: ${BOLD}${symlink_path}${NC} -> ${target_path}"
# 创建 BIN_DIR(如果不存在)
mkdir -p "$BIN_DIR"
# 如果 symlink 已存在,删除旧的
if [[ -L "$symlink_path" ]]; then
warn "Symlink already exists, removing old symlink"
rm "$symlink_path"
elif [[ -e "$symlink_path" ]]; then
die "File already exists at $symlink_path (not a symlink). Please remove it manually."
fi
# 创建 symlink
ln -s "$target_path" "$symlink_path" || die "Failed to create symlink"
success "Symlink created: $symlink_path"
}
测试 symlink 创建:
# 使用上一步的 mock 安装
BIN_DIR=/tmp/test-bin CREATE_SYMLINK=1 VERSION=1.0.0 MIRROR=file:///tmp INSTALL_DIR=/tmp/test-install ./scripts/install-web.sh
# 验证 symlink
ls -la /tmp/test-bin/aionui-web
# 预期:链接到 /tmp/test-install/bin/aionui-web.js
产出:
create_symlink() 函数实现完成目的: 将 ~/.local/bin/ 添加到 PATH 环境变量(如果尚未添加)。
操作:
实现 update_shell_profile() 函数:
update_shell_profile() {
# 检查 BIN_DIR 是否已在 PATH 中
if [[ ":$PATH:" == *":${BIN_DIR}:"* ]]; then
info "PATH already contains ${BOLD}${BIN_DIR}${NC}"
return
fi
info "Adding ${BOLD}${BIN_DIR}${NC} to PATH in shell profile..."
# 检测当前 shell
local shell_name
shell_name="$(basename "$SHELL")"
local profile_file=""
case "$shell_name" in
bash)
if [[ -f "$HOME/.bashrc" ]]; then
profile_file="$HOME/.bashrc"
elif [[ -f "$HOME/.bash_profile" ]]; then
profile_file="$HOME/.bash_profile"
fi
;;
zsh)
profile_file="$HOME/.zshrc"
;;
fish)
profile_file="$HOME/.config/fish/config.fish"
;;
*)
warn "Unknown shell: $shell_name. Please manually add ${BIN_DIR} to PATH."
return
;;
esac
if [[ -z "$profile_file" ]]; then
warn "Shell profile not found. Please manually add ${BIN_DIR} to PATH."
return
fi
# 添加 PATH 配置
local path_line="export PATH=\"${BIN_DIR}:\$PATH\""
# 检查是否已有相同配置
if grep -q "${BIN_DIR}" "$profile_file" 2>/dev/null; then
info "PATH configuration already exists in $profile_file"
return
fi
# 添加到 profile
echo "" >> "$profile_file"
echo "# Added by aionui-web installer" >> "$profile_file"
echo "$path_line" >> "$profile_file"
success "Added PATH to $profile_file"
warn "Please restart your shell or run: source $profile_file"
}
测试 PATH 更新:
# 创建 mock shell profile
echo "# test bashrc" > /tmp/test-bashrc
# 测试添加 PATH
HOME=/tmp BIN_DIR=/tmp/test-bin SHELL=/bin/bash ./scripts/install-web.sh
# 验证 PATH 添加
cat /tmp/test-bashrc
# 预期:包含 export PATH="/tmp/test-bin:$PATH"
产出:
update_shell_profile() 函数实现完成目的: 打印安装摘要,提供使用说明和卸载方法。
操作:
实现 print_summary() 函数:
print_summary() {
echo ""
echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════${NC}"
echo -e "${GREEN}${BOLD} 🎉 AionUi WebUI v${VERSION} Installed!${NC}"
echo -e "${GREEN}${BOLD}══════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}📍 Installation directory:${NC} ${INSTALL_DIR}"
if [[ "$CREATE_SYMLINK" == "1" ]]; then
echo -e " ${BOLD}📍 Symlink:${NC} ${BIN_DIR}/aionui-web"
fi
echo ""
echo -e " ${BOLD}🚀 Usage:${NC}"
echo ""
if [[ "$CREATE_SYMLINK" == "1" && ":$PATH:" == *":${BIN_DIR}:"* ]]; then
echo " # Start AionUi WebUI"
echo " aionui-web start"
echo ""
echo " # Check version"
echo " aionui-web version"
else
echo " # Start AionUi WebUI (using full path)"
echo " ${INSTALL_DIR}/bin/aionui-web.js start"
echo ""
echo " # Or add symlink to PATH:"
if [[ "$CREATE_SYMLINK" == "1" ]]; then
echo " export PATH=\"${BIN_DIR}:\$PATH\""
else
echo " ln -s ${INSTALL_DIR}/bin/aionui-web.js ~/.local/bin/aionui-web"
echo " export PATH=\"~/.local/bin:\$PATH\""
fi
fi
echo ""
echo -e " ${BOLD}📖 Documentation:${NC} https://github.com/iOfficeAI/AionUi"
echo -e " ${BOLD}🐛 Report issues:${NC} https://github.com/iOfficeAI/AionUi/issues"
echo ""
echo -e " ${BOLD}🗑️ Uninstall:${NC}"
echo ""
echo " # Remove installation directory"
echo " rm -rf ${INSTALL_DIR}"
if [[ "$CREATE_SYMLINK" == "1" ]]; then
echo ""
echo " # Remove symlink"
echo " rm ${BIN_DIR}/aionui-web"
fi
if [[ "$UPDATE_PATH" == "1" ]]; then
echo ""
echo " # Remove PATH configuration from shell profile"
echo " # (manually edit ~/.bashrc or ~/.zshrc)"
fi
echo ""
}
测试完整安装流程:
# 完整测试(使用 mock tarball)
VERSION=1.0.0 MIRROR=file:///tmp INSTALL_DIR=/tmp/aionui-web-test BIN_DIR=/tmp/test-bin ./scripts/install-web.sh
# 验证输出
# 预期:显示完整的安装摘要
产出:
print_summary() 函数实现完成目的: 在 CI 中将 install-web.sh 作为 release artifact 上传,并替换 __VERSION__ 占位符。
操作:
修改 .github/workflows/pack-web-cli.yml(或创建新 workflow):
pack-web-cli job 后添加 prepare-install-script jobprepare-install-script:
name: Prepare install-web.sh for release
runs-on: ubuntu-latest
needs: pack-web-cli
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Get version from package.json
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Replace __VERSION__ placeholder in install-web.sh
run: |
mkdir -p dist-scripts
sed "s/__VERSION__/${{ steps.version.outputs.version }}/g" scripts/install-web.sh > dist-scripts/install-web.sh
chmod +x dist-scripts/install-web.sh
- name: Upload install-web.sh artifact
uses: actions/upload-artifact@v6
with:
name: install-web-script
path: dist-scripts/install-web.sh
retention-days: 7
修改 .github/workflows/build-and-release.yml:
release job 的 files: 部分添加 install-web.sh- name: Download install-web.sh artifact
uses: actions/download-artifact@v7
with:
name: install-web-script
path: build-artifacts/install-web-script
- name: Prepare release assets (include install-web.sh)
shell: bash
run: |
bash scripts/prepare-release-assets.sh build-artifacts release-assets
# Copy install-web.sh to release-assets
cp build-artifacts/install-web-script/install-web.sh release-assets/
- name: Create Release
uses: softprops/action-gh-release@v2
with:
# ...
files: |
release-assets/**/*.exe
release-assets/**/*.msi
release-assets/**/*.dmg
release-assets/**/*.deb
release-assets/**/*.zip
release-assets/**/*.yml
release-assets/install-web.sh
本地测试 sed 替换:
VERSION=1.2.3
sed "s/__VERSION__/${VERSION}/g" scripts/install-web.sh > /tmp/install-web-test.sh
grep "VERSION=" /tmp/install-web-test.sh | head -1
# 预期:VERSION="${VERSION:-1.2.3}"
产出:
__VERSION__ 占位符在 CI 中被替换为实际版本号目的: 在 CI 中添加容器冒烟测试,验证 curl | bash 安装流程工作正常。
操作:
创建 scripts/smoke-test-install-web.sh:
#!/bin/bash
# ============================================================================
# Smoke test for install-web.sh
# Tests the full installation flow in a container environment
# ============================================================================
set -euo pipefail
MIRROR="${1:-}"
VERSION="${2:-}"
if [[ -z "$MIRROR" ]]; then
echo "Usage: $0 <mirror-url> [version]"
echo "Example: $0 file:///tmp/releases 1.0.0"
exit 1
fi
echo "========================================"
echo "Smoke test for install-web.sh"
echo "========================================"
echo "MIRROR: $MIRROR"
echo "VERSION: ${VERSION:-latest}"
# 1. Download install-web.sh
echo ""
echo "1. Downloading install-web.sh..."
if [[ "$MIRROR" == file://* ]]; then
# Local mirror: copy from filesystem
local base_path="${MIRROR#file://}"
cp "${base_path}/install-web.sh" /tmp/install-web.sh
else
# Remote mirror: use curl
curl -fsSL "${MIRROR}/install-web.sh" -o /tmp/install-web.sh
fi
chmod +x /tmp/install-web.sh
# 2. Run installation
echo ""
echo "2. Running installation..."
export MIRROR="$MIRROR"
export VERSION="${VERSION:-latest}"
export INSTALL_DIR="/tmp/aionui-web-smoke-test"
export BIN_DIR="/tmp/smoke-bin"
export CREATE_SYMLINK=1
export UPDATE_PATH=0 # 不修改 shell profile(容器环境)
bash /tmp/install-web.sh --no-path
# 3. Verify installation
echo ""
echo "3. Verifying installation..."
if [[ ! -d "$INSTALL_DIR" ]]; then
echo "❌ Installation directory not found: $INSTALL_DIR"
exit 1
fi
echo "✓ Installation directory exists"
if [[ ! -f "${INSTALL_DIR}/bin/aionui-web.js" ]]; then
echo "❌ CLI entry point not found: ${INSTALL_DIR}/bin/aionui-web.js"
exit 1
fi
echo "✓ CLI entry point exists"
if [[ ! -L "${BIN_DIR}/aionui-web" ]]; then
echo "❌ Symlink not found: ${BIN_DIR}/aionui-web"
exit 1
fi
echo "✓ Symlink created"
# 4. Test version command
echo ""
echo "4. Testing version command..."
export PATH="${BIN_DIR}:$PATH"
VERSION_OUTPUT=$(aionui-web version 2>&1 || echo "")
if [[ -z "$VERSION_OUTPUT" ]]; then
echo "❌ version command returned empty"
exit 1
fi
echo "✓ Version: $VERSION_OUTPUT"
# Cleanup
rm -rf "$INSTALL_DIR" "$BIN_DIR" /tmp/install-web.sh
echo ""
echo "========================================"
echo "✅ Smoke test passed!"
echo "========================================"
在 .github/workflows/pack-web-cli.yml 中添加 smoke-test job:
smoke-test-install:
name: Smoke test install-web.sh (Linux x86_64)
runs-on: ubuntu-latest
needs: [pack-web-cli, prepare-install-script]
container:
image: debian:bookworm-slim
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Install dependencies
run: |
apt-get update
apt-get install -y curl tar gzip nodejs coreutils
- name: Download linux-x86_64 tarball
uses: actions/download-artifact@v7
with:
name: web-cli-linux-x64
path: /tmp/releases/v1.0.0
- name: Download install-web.sh
uses: actions/download-artifact@v7
with:
name: install-web-script
path: /tmp/releases
- name: Run smoke test
shell: bash
run: |
chmod +x scripts/smoke-test-install-web.sh
bash scripts/smoke-test-install-web.sh file:///tmp/releases 1.0.0
本地测试 smoke test 脚本:
# 准备 mock 环境(使用 Phase 6 的 mock tarball)
mkdir -p /tmp/mock-releases/v1.0.0
cp /tmp/aionui-web-1.0.0-darwin-arm64.tar.gz /tmp/mock-releases/v1.0.0/
cp /tmp/aionui-web-1.0.0-darwin-arm64.tar.gz.sha256 /tmp/mock-releases/v1.0.0/
cp scripts/install-web.sh /tmp/mock-releases/
sed "s/__VERSION__/1.0.0/g" /tmp/mock-releases/install-web.sh > /tmp/mock-releases/install-web.sh.tmp
mv /tmp/mock-releases/install-web.sh.tmp /tmp/mock-releases/install-web.sh
# 运行 smoke test
bash scripts/smoke-test-install-web.sh file:///tmp/mock-releases 1.0.0
产出:
scripts/smoke-test-install-web.sh 创建完成目的: 记录 M9 的交付物和使用说明,为用户提供完整的安装指南。
操作:
创建 docs/backend-migration/handoffs/M9-outcome.md:
# M9 Outcome: Install-Web Script
## 交付物
1. **安装脚本**:
- `scripts/install-web.sh` — 一键安装脚本,支持 curl | bash
- GitHub release artifact: `install-web.sh`(已替换 `__VERSION__` 占位符)
2. **支持的平台**:
- darwin-arm64, darwin-x86_64
- linux-x86_64, linux-aarch64
- win-x86_64
3. **安装选项**:
- `--version <version>`: 指定版本(默认:latest 或 CI 嵌入版本)
- `--mirror <url>`: 指定镜像 URL(默认:GitHub releases)
- `--install-dir <path>`: 指定安装目录(默认:`~/.local/share/aionui-web`)
- `--no-symlink`: 不创建 symlink
- `--no-path`: 不添加 PATH 到 shell profile
4. **安装流程**:
1. 自动检测平台和架构
2. 下载 tarball + SHA256 校验和
3. 验证 SHA256
4. 解压到安装目录
5. 创建 symlink(`~/.local/bin/aionui-web`)
6. 添加 PATH 到 shell profile
5. **CI 集成**:
- `pack-web-cli.yml`: 产出 tarball + SHA256
- `prepare-install-script`: 替换 `__VERSION__` 并上传 install-web.sh
- `smoke-test-install`: 在 debian:slim 容器中测试安装流程
## 使用方式
### 在线安装(推荐)
```bash
# 安装最新版本
curl -fsSL https://raw.githubusercontent.com/iOfficeAI/AionUi/main/scripts/install-web.sh | bash
# 安装指定版本
curl -fsSL https://raw.githubusercontent.com/iOfficeAI/AionUi/main/scripts/install-web.sh | VERSION=1.0.0 bash
```
# 1. 下载 release assets 到本地目录
mkdir -p /path/to/releases/v1.0.0
cd /path/to/releases/v1.0.0
wget https://github.com/iOfficeAI/AionUi/releases/download/v1.0.0/aionui-web-1.0.0-linux-x86_64.tar.gz
wget https://github.com/iOfficeAI/AionUi/releases/download/v1.0.0/aionui-web-1.0.0-linux-x86_64.tar.gz.sha256
# 2. 下载 install-web.sh
cd /path/to/releases
wget https://github.com/iOfficeAI/AionUi/releases/download/v1.0.0/install-web.sh
# 3. 运行安装(使用 file:// mirror)
MIRROR=file:///path/to/releases VERSION=1.0.0 bash install-web.sh
# 删除安装目录
rm -rf ~/.local/share/aionui-web
# 删除 symlink
rm ~/.local/bin/aionui-web
# 删除 PATH 配置(手动编辑 ~/.bashrc 或 ~/.zshrc)
~/.local/ 不需要 root,但其他目录可能需要 sudo如果 install-web.sh 有问题,用户可手动下载 tarball 并解压:
# 1. 下载 tarball
curl -LO https://github.com/iOfficeAI/AionUi/releases/download/v1.0.0/aionui-web-1.0.0-linux-x86_64.tar.gz
# 2. 验证 checksum(可选)
curl -LO https://github.com/iOfficeAI/AionUi/releases/download/v1.0.0/aionui-web-1.0.0-linux-x86_64.tar.gz.sha256
shasum -a 256 -c aionui-web-1.0.0-linux-x86_64.tar.gz.sha256
# 3. 解压
tar -xzf aionui-web-1.0.0-linux-x86_64.tar.gz
mv aionui-web ~/.local/share/aionui-web
# 4. 创建 symlink
ln -s ~/.local/share/aionui-web/bin/aionui-web.js ~/.local/bin/aionui-web
# 5. 添加 PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Commit handoff 文档:
git add docs/backend-migration/handoffs/M9-outcome.md
git commit -m "docs(backend-migration): add M9 handoff document"
git push origin feat/m9-install-web-script
产出:
docs/backend-migration/handoffs/M9-outcome.md 已创建M9 完成的标志:
scripts/install-web.sh 结构完整,支持所有命令行参数__VERSION__, latest, 和明确版本号__VERSION__ 被替换curl | bash 安装流程docs/backend-migration/handoffs/M9-outcome.md 已创建| 风险 | 影响 | 缓解方案 |
|---|---|---|
| GitHub API rate limit | 版本解析失败 | 提供 --version 参数绕过 API 查询;添加 retry 逻辑 |
| tarball 下载失败 | 安装中断 | 添加 retry 逻辑;提供 file:// mirror 支持 |
| SHA256 工具缺失 | 无法验证校验和 | 检测 shasum/sha256sum 缺失时警告但不失败 |
| Windows 兼容性 | install-web.sh 无法在 Windows 原生运行 | 文档中说明需要 Git Bash / WSL;考虑提供 PowerShell 版本(M10+) |
| 权限不足 | 无法写入安装目录 | 默认使用 ~/.local/,不需要 root;提供 --install-dir 自定义 |
| 阶段 | 预计时间 |
|---|---|
| Phase 0: Baseline & Pre-Flight | 5 分钟 |
| Phase 1: Create install-web.sh Skeleton | 15 分钟 |
| Phase 2: Implement detect_platform_arch() | 10 分钟 |
| Phase 3: Implement resolve_version() | 10 分钟 |
| Phase 4: Implement download_tarball() | 15 分钟 |
| Phase 5: Implement verify_checksum() | 10 分钟 |
| Phase 6: Implement extract_tarball() | 15 分钟 |
| Phase 7: Implement create_symlink() | 10 分钟 |
| Phase 8: Implement update_shell_profile() | 15 分钟 |
| Phase 9: Implement print_summary() | 10 分钟 |
| Phase 10: Add CI Integration | 20 分钟 |
| Phase 11: Add Container Smoke Test | 20 分钟 |
| Phase 12: Document & Handoff | 15 分钟 |
| 总计 | ~2.5 小时 |
(实际时间可能因调试、网络速度等因素浮动)
scripts/install-ubuntu.sh — Ubuntu/Debian 安装脚本(参考实现)docs/backend-migration/handoffs/M8-outcome.md — M8 tarball 产物和接口约定docs/backend-migration/plans/2026-05-07-m8-web-cli-tarball.md — M8 详细计划(tarball 结构).github/workflows/build-and-release.yml — CI release 流程scripts/verify-release-assets.sh — Release asset 验证脚本本计划由 plan-writer-m9 生成,基于 M1/M7/M8 格式模板和源码探查结果。