feat: add one-command plugin deployment and removal
Business Plugins CI / check (plugin-admin) (push) Successful in 1m36s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m37s

This commit is contained in:
Qiufeng
2026-08-27 23:48:18 +08:00
parent 5feae3ad41
commit 292dd329af
5 changed files with 362 additions and 0 deletions
+33
View File
@@ -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)。
+104
View File
@@ -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/<plugin>/bin/` |
| 配置 | `/etc/sub2api-add/<plugin>.env` |
| 插件数据 | `/var/lib/sub2api-add/<plugin>/` |
| 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
```
再次运行安装脚本会重新构建并重启选定插件。安装脚本发现源码有未提交修改
时会中止,避免升级覆盖本地改动。
+26
View File
@@ -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" "$@"
+59
View File
@@ -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
+140
View File
@@ -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" <<EOF
[Unit]
Description=Sub2API ${name} business plugin
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=${RUN_USER}
Group=${RUN_USER}
WorkingDirectory=${data_dir}
EnvironmentFile=${env_file}
ExecStart=${binary_dir}/${name}
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${data_dir}
[Install]
WantedBy=multi-user.target
EOF
}
for plugin in "${PLUGINS[@]}"; do
install_one "$plugin"
done
systemctl daemon-reload
for plugin in "${PLUGINS[@]}"; do
systemctl enable --now "sub2api-$plugin.service"
info "$plugin 已启动:$(systemctl is-active "sub2api-$plugin.service")"
done
info "安装完成。配置文件位于 $ETC_DIR;数据位于 $VAR_DIR。"
info "查看日志:journalctl -u sub2api-plugin-admin -u sub2api-subscription-admin -f"