diff --git a/README.md b/README.md index f8bfb92..168bb10 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,20 @@ HTTP API、管理员鉴权和 `custom_menu_items` 接入 Core;插件不导入 两个插件都是独立 Go module,可以分别构建和发布。生产环境应使用独立的 低权限服务账号、HTTPS 反向代理、签名包和稳定的 Core API 兼容基线。 +## 推荐部署顺序 + +先单独部署官方 Sub2API Core,再部署本仓库的插件。官方 Core 的全新 Docker +部署命令(官方仓库和官方镜像): + +```sh +mkdir -p /opt/sub2api-core && cd /opt/sub2api-core +curl -fsSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash +docker compose -f docker-compose.local.yml up -d +``` + +Core 和插件使用不同的目录、服务、端口及数据边界;不要把插件源码复制到 +Core 仓库,也不要让插件连接 Core PostgreSQL/Redis。 + ## 快速验证 ```sh @@ -42,3 +56,22 @@ HTTP API、管理员鉴权和 `custom_menu_items` 接入 Core;插件不导入 Core 继续作为用户、余额、订阅、计费和用量账本的权威来源。插件浏览器端 不持有 Core JWT、Admin Key 或其他服务密钥。 + +## 一键部署与卸载 + +Linux + systemd 环境可直接使用仓库内的安装脚本: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \ + | sudo bash -s -- --plugin all +``` + +默认卸载并保留配置/数据: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/uninstall.sh \ + | sudo bash -s -- --plugin all +``` + +需要清除插件配置、数据和源码时,显式使用 `--purge --yes`。完整部署、反向 +代理、菜单应用和升级说明见 [`deploy/README.md`](deploy/README.md)。 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..2323e3e --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,104 @@ +# 部署与卸载 + +本仓库的插件是独立服务,不需要把源码复制进 Sub2API Core。安装脚本会构建 +插件二进制、创建低权限 systemd 服务、生成插件配置,并让服务监听各自的 +端口。Core 数据库、Redis 和 Core `data/` 目录始终由 Core 自己管理。 + +## 先部署官方 Core + +建议先使用官方仓库的全新部署脚本,单独放在自己的目录: + +```sh +mkdir -p /opt/sub2api-core && cd /opt/sub2api-core +curl -fsSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash +docker compose -f docker-compose.local.yml up -d +``` + +官方 Core 默认通过 `http://服务器IP:8080` 提供页面。确认 Core 登录和 +数据库初始化完成后,再安装下面的独立插件。插件不会修改 Core 数据库迁移。 + +## 一键安装 + +在一台已安装 Go 1.23+、Git 和 systemd 的 Linux 主机上: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \ + | sudo bash -s -- --plugin all +``` + +只安装控制面: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \ + | sudo bash -s -- --plugin plugin-admin +``` + +安装订阅插件: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/install.sh \ + | sudo bash -s -- --plugin subscription-admin +``` + +默认安装位置: + +| 内容 | 路径 | +| --- | --- | +| 源码 | `/opt/sub2api-add` | +| 二进制 | `/opt/sub2api-add//bin/` | +| 配置 | `/etc/sub2api-add/.env` | +| 插件数据 | `/var/lib/sub2api-add//` | +| systemd | `sub2api-plugin-admin.service`、`sub2api-subscription-admin.service` | + +安装后先编辑配置: + +```sh +sudoedit /etc/sub2api-add/plugin-admin.env +sudoedit /etc/sub2api-add/subscription-admin.env +sudo systemctl restart sub2api-plugin-admin sub2api-subscription-admin +``` + +至少确认 `CORE_BASE_URL`、插件端口、反向代理路径和 HTTPS Cookie 设置。生产 +环境应启用 `PLUGIN_COOKIE_SECURE=true`,并为 plugin-admin 配置稳定的 +`PLUGIN_CONFIG_KEY` 和受信发布者公钥。 + +## 菜单接入 + +1. 让反向代理把 `plugin-admin` 和业务插件分别转发到 `127.0.0.1:8090` + 与 `127.0.0.1:8091`。 +2. 登录 plugin-admin,上传业务插件包并完成签名/哈希/兼容性校验。 +3. 配置业务插件的 loopback `service_url`。 +4. 启用、健康检查、菜单预览,然后应用菜单。 + +订阅插件是可选项;未安装或未应用菜单时,Core 管理员菜单不会出现“订阅管理”。 + +## 一键卸载 + +默认卸载服务和二进制,但保留配置与插件数据,便于重新安装: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/uninstall.sh \ + | sudo bash -s -- --plugin all +``` + +确认同时删除插件配置、插件数据和源码: + +```sh +curl -fsSL https://git.awaioi.com/awaioi/sub2api-add/raw/branch/main/deploy/uninstall.sh \ + | sudo bash -s -- --plugin all --purge --yes +``` + +卸载脚本不会删除 PostgreSQL、Redis、Sub2API Core 数据或 Core 配置。若只删除 +订阅插件,使用 `--plugin subscription-admin`;控制面和其他插件保持不变。 + +## 本地安装与升级 + +从已克隆的仓库执行: + +```sh +sudo ./scripts/install-local.sh --plugin all +sudo ./deploy/uninstall.sh --plugin subscription-admin +``` + +再次运行安装脚本会重新构建并重启选定插件。安装脚本发现源码有未提交修改 +时会中止,避免升级覆盖本地改动。 diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100755 index 0000000..af7a8ef --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +REPO_URL=${PLUGIN_REPO_URL:-https://git.awaioi.com/awaioi/sub2api-add.git} +SOURCE_DIR=${PLUGIN_SOURCE_DIR:-/opt/sub2api-add} +REF=${PLUGIN_REF:-main} + +die() { printf '错误:%s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行" +command -v git >/dev/null 2>&1 || die "缺少 git" + +if [[ -e "$SOURCE_DIR" && ! -d "$SOURCE_DIR/.git" ]]; then + die "$SOURCE_DIR 已存在但不是本插件仓库;请设置 PLUGIN_SOURCE_DIR" +fi + +if [[ -d "$SOURCE_DIR/.git" ]]; then + if [[ -n "$(git -C "$SOURCE_DIR" status --porcelain)" ]]; then + die "$SOURCE_DIR 有未提交修改,先清理后再升级" + fi + git -C "$SOURCE_DIR" fetch --depth 1 origin "$REF" + git -C "$SOURCE_DIR" checkout --force FETCH_HEAD +else + git clone --depth 1 --branch "$REF" "$REPO_URL" "$SOURCE_DIR" +fi + +exec "$SOURCE_DIR/scripts/install-local.sh" "$@" diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh new file mode 100755 index 0000000..c88f7c4 --- /dev/null +++ b/deploy/uninstall.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ETC_DIR=${PLUGIN_ETC_DIR:-/etc/sub2api-add} +VAR_DIR=${PLUGIN_VAR_DIR:-/var/lib/sub2api-add} +PREFIX=${PLUGIN_INSTALL_PREFIX:-/opt/sub2api-add} +SOURCE_DIR=${PLUGIN_SOURCE_DIR:-/opt/sub2api-add} +RUN_USER=${PLUGIN_SYSTEM_USER:-sub2api-plugin} +SELECTION=all +PURGE=false +YES=false + +die() { printf '错误:%s\n' "$*" >&2; exit 1; } +[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行" + +while (($#)); do + case "$1" in + --plugin) (($# >= 2)) || die "--plugin 需要参数"; SELECTION=$2; shift 2 ;; + --purge) PURGE=true; shift ;; + --yes) YES=true; shift ;; + -h|--help) printf '%s\n' '用法:deploy/uninstall.sh [--plugin all|plugin-admin|subscription-admin] [--purge --yes]'; exit 0 ;; + *) die "未知参数:$1" ;; + esac +done + +case "$SELECTION" in + all) PLUGINS=(plugin-admin subscription-admin) ;; + plugin-admin|subscription-admin) PLUGINS=("$SELECTION") ;; + *) die "插件必须是 all、plugin-admin 或 subscription-admin" ;; +esac + +if $PURGE && ! $YES; then + [[ -t 0 ]] || die "非交互环境执行 --purge 时必须同时提供 --yes" + read -r -p "将删除插件配置和数据,继续?[y/N] " answer + [[ "$answer" == [yY] ]] || { printf '%s\n' '已取消'; exit 0; } +fi + +for plugin in "${PLUGINS[@]}"; do + unit="sub2api-$plugin.service" + systemctl disable --now "$unit" 2>/dev/null || true + rm -f "/etc/systemd/system/$unit" + rm -rf "$PREFIX/$plugin" + if $PURGE; then + rm -f "$ETC_DIR/$plugin.env" + rm -rf "$VAR_DIR/$plugin" + fi +done + +systemctl daemon-reload +if $PURGE && [[ -d "$ETC_DIR" ]]; then rmdir "$ETC_DIR" 2>/dev/null || true; fi +if $PURGE && [[ -d "$VAR_DIR" ]]; then rmdir "$VAR_DIR" 2>/dev/null || true; fi +if $PURGE && [[ -d "$SOURCE_DIR" ]]; then rm -rf "$SOURCE_DIR"; fi + +if $PURGE; then + userdel "$RUN_USER" 2>/dev/null || true + printf '%s\n' '插件、配置、数据和源码已删除;Core 数据库未被触碰。' +else + printf '%s\n' "插件服务已卸载;配置和数据仍保留在 $ETC_DIR 与 $VAR_DIR。" +fi diff --git a/scripts/install-local.sh b/scripts/install-local.sh new file mode 100755 index 0000000..4df876a --- /dev/null +++ b/scripts/install-local.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +PREFIX=${PLUGIN_INSTALL_PREFIX:-/opt/sub2api-add} +ETC_DIR=${PLUGIN_ETC_DIR:-/etc/sub2api-add} +VAR_DIR=${PLUGIN_VAR_DIR:-/var/lib/sub2api-add} +RUN_USER=${PLUGIN_SYSTEM_USER:-sub2api-plugin} +SELECTION=all + +usage() { + cat <<'EOF' +用法:scripts/install-local.sh [--plugin all|plugin-admin|subscription-admin] + +环境变量: + PLUGIN_INSTALL_PREFIX 二进制目录,默认 /opt/sub2api-add + PLUGIN_ETC_DIR 环境文件目录,默认 /etc/sub2api-add + PLUGIN_VAR_DIR 插件数据目录,默认 /var/lib/sub2api-add + PLUGIN_SYSTEM_USER systemd 用户,默认 sub2api-plugin +EOF +} + +die() { printf '错误:%s\n' "$*" >&2; exit 1; } +info() { printf '[sub2api-add] %s\n' "$*"; } + +while (($#)); do + case "$1" in + --plugin) + (($# >= 2)) || die "--plugin 需要参数" + SELECTION=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) die "未知参数:$1" ;; + esac +done + +[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 运行" +command -v go >/dev/null 2>&1 || die "缺少 Go;请安装 Go 1.23 或更高版本" +command -v systemctl >/dev/null 2>&1 || die "缺少 systemd/systemctl" +command -v install >/dev/null 2>&1 || die "缺少 install 命令" + +case "$SELECTION" in + all) PLUGINS=(plugin-admin subscription-admin) ;; + plugin-admin|subscription-admin) PLUGINS=("$SELECTION") ;; + *) die "插件必须是 all、plugin-admin 或 subscription-admin" ;; +esac + +if ! id -u "$RUN_USER" >/dev/null 2>&1; then + useradd --system --user-group --home-dir "$VAR_DIR" --create-home --shell /usr/sbin/nologin "$RUN_USER" +fi + +ensure_env_value() { + local file=$1 key=$2 value=$3 + if grep -q "^${key}=" "$file"; then + if command sed --version >/dev/null 2>&1; then + sed -i "s#^${key}=.*#${key}=${value}#" "$file" + else + sed -i '' "s#^${key}=.*#${key}=${value}#" "$file" + fi + else + printf '%s=%s\n' "$key" "$value" >> "$file" + fi +} + +random_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 32 + else + od -An -N32 -tx1 /dev/urandom | tr -d ' \n' + fi +} + +install_one() { + local name=$1 source="$ROOT/plugins/$1" env_file="$ETC_DIR/$1.env" + local binary_dir="$PREFIX/$1/bin" data_dir="$VAR_DIR/$1" + [[ -d "$source" ]] || die "插件目录不存在:$source" + install -d -m 0755 "$binary_dir" "$data_dir" "$ETC_DIR" + if [[ ! -f "$env_file" ]]; then + install -m 0600 "$source/.env.example" "$env_file" + fi + + if [[ "$name" == plugin-admin ]]; then + ensure_env_value "$env_file" PLUGIN_REGISTRY_DIR "$data_dir" + if grep -q '^PLUGIN_CONFIG_KEY=generate-and-replace-with-a-random-32-byte-secret$' "$env_file" || + ! grep -q '^PLUGIN_CONFIG_KEY=.' "$env_file"; then + ensure_env_value "$env_file" PLUGIN_CONFIG_KEY "$(random_secret)" + fi + fi + + local tmp="$binary_dir/.${name}.tmp" + info "构建 $name" + (cd "$source" && CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o "$tmp" .) + chmod 0755 "$tmp" + mv -f "$tmp" "$binary_dir/$name" + chown -R "$RUN_USER:$RUN_USER" "$data_dir" + chown "$RUN_USER:$RUN_USER" "$binary_dir/$name" + chmod 0600 "$env_file" + + cat > "/etc/systemd/system/sub2api-$name.service" <