Scout/使用说明.md

20 KiB
Raw Blame History

Scout 轻量化包管理程序

Scout 是一个使用 Go + Vue 构建的轻量化包管理程序,负责包上传、版本管理、客户端更新检查、实时更新通知和账户权限控制。

当前能力

  • 普通用户、开发者、管理员三种角色。
  • 用户注册、登录、退出和会话认证。
  • 密码使用带随机盐的 PBKDF2-HMAC-SHA256 方案保存,不保存明文密码。
  • 管理员可查看用户、授予开发者权限和启停账户。
  • 开发者或管理员可以上传包;旧版开发者令牌仍可兼容脚本调用。
  • 包名、版本号、文件名和空文件校验。
  • 重复版本拒绝覆盖,包文件保存后提供 SHA-256 校验值。
  • 包列表支持 query 搜索、page 分页和 page_size 条数限制。
  • 客户端通过 SSE 接收新包通知,通过下载接口获取包文件。
  • 邮件只用于管理员通知、上传用户操作通知和安全认证,不发送普通用户包更新订阅邮件。
  • 未配置 SMTP 时,邮件会以“待发送邮件”形式写入服务端日志,便于本地测试。

目录结构

Scout/
├── cmd/
│   ├── scout-server/       # Go 服务端
│   └── scout-client/       # Go 客户端命令行工具
├── internal/
│   ├── httpapi/             # HTTP API、认证、权限和通知
│   ├── notify/              # SMTP 邮件发送抽象
│   ├── registry/            # 包存储、元数据和版本比较
│   └── users/                # 用户、角色、密码哈希和会话
├── pkg/client/              # 可复用 Go 客户端模块
└── frontend/                # Vue 3 + Vite 管理界面

启动 Go 服务端

PowerShell 示例:

$env:SCOUT_DEVELOPER_TOKEN = "请替换为实际开发者令牌"
$env:SCOUT_ADMIN_EMAIL = "admin@example.com"
$env:SCOUT_ADMIN_NAME = "系统管理员"
$env:SCOUT_ADMIN_PASSWORD = "请使用长度至少 8 位的管理员密码"
$env:SCOUT_ADDR = ":8080"
$env:SCOUT_DATA_DIR = "data"
go run .\cmd\scout-server

服务端会在 data/users.json 保存用户和密码哈希,包文件仍保存在 data/包名/版本/。不要把 data/ 目录提交到远程仓库。

健康检查:

GET http://127.0.0.1:8080/healthz

SMTP 配置

需要真实发送邮件时配置:

$env:SCOUT_SMTP_HOST = "smtp.example.com"
$env:SCOUT_SMTP_PORT = "587"
$env:SCOUT_SMTP_USERNAME = "账号"
$env:SCOUT_SMTP_PASSWORD = "密码"
$env:SCOUT_SMTP_FROM = "Scout <noreply@example.com>"
$env:SCOUT_SMTP_TLS = "false"

SMTP 密码只通过环境变量或部署平台密钥管理提供,不要写入代码、配置文件或 Git 远程地址。生产环境建议在反向代理后使用 HTTPS、限制跨域来源、配置限流,并使用独立的开发者账户而不是兼容令牌。

启动 Vue 前端

打开第二个终端:

Set-Location .\frontend
npm.cmd install
npm.cmd run dev

浏览器打开 Vite 输出的地址。当前本地测试地址通常为:

http://127.0.0.1:5174/

页面顶部可以修改服务端地址。页面支持注册、登录、密码重置申请、包搜索、包下载和开发者上传。管理员登录后可以管理用户角色和账户状态。

HTTP 接口

账户和安全认证

方法 路径 用途 认证
POST /api/v1/auth/register 注册用户 无
POST /api/v1/auth/login 登录并获取会话令牌 无
POST /api/v1/auth/logout 退出当前会话 Bearer 会话令牌
GET /api/v1/auth/me 获取当前用户 Bearer 会话令牌
POST /api/v1/auth/password/request 申请密码重置邮件 无
POST /api/v1/auth/password/reset 使用一次性令牌重置密码 无
GET /api/v1/admin/users 查看用户列表 管理员
PATCH /api/v1/admin/users/{id} 更新角色或启停账户 管理员

登录请求示例:

{
  "email": "admin@example.com",
  "password": "管理员密码"
}

登录响应中的 session 用于后续请求:

Authorization: Bearer <session>

包管理

方法 路径 用途 认证
GET /healthz 健康检查 无
GET /api/v1/packages?query=demo&page=1&page_size=50 搜索和分页获取最新包 无
GET /api/v1/packages/{name} 获取指定包最新版本 无
GET /api/v1/packages/{name}/versions 获取指定包全部版本 无
GET /api/v1/packages/{name}/{version}/download 下载指定版本 无
GET /api/v1/updates/stream?packages=a,b 监听包更新 SSE 通知 无
POST /api/v1/packages/upload 上传新包 开发者/管理员会话或兼容令牌

上传接口使用 multipart/form-data,字段如下:

  • name:包名,只允许字母、数字、点、下划线和短横线。
  • version:版本号,只允许字母、数字、点、加号、下划线和短横线。
  • file:非空包文件,文件名必须安全且长度不超过 128。
  • description、platform、architecture:可选元数据。

兼容令牌请求头:

Authorization: Bearer <SCOUT_DEVELOPER_TOKEN>

邮件通知范围

  • 新用户注册:通知注册用户,并通知管理员。
  • 用户登录:向当前用户发送安全登录提醒。
  • 密码重置:向用户发送一次性重置令牌。
  • 密码修改和账户权限变更:通知相关用户。
  • 包上传成功或失败:通知执行上传的开发者/管理员。
  • 普通用户不会因为包版本更新收到邮件,仍通过 SSE 或客户端主动检查获取更新。

多项目发布中心

Scout 服务端是独立发布中心,不需要修改 SSL、Smail 或后续接入项目的源代码。每个项目使用独立的包目录、版本索引和 SSE 通知流;项目之间可以存在同名包而不会互相覆盖。

服务端默认保留 default 兼容项目,对应原有 /api/v1/packages 接口。新增项目使用项目级接口,例如项目标识为 ssl 时:

方法 路径 用途 认证
GET /api/v1/projects 查看项目列表 无
POST /api/v1/projects 创建项目 管理员
GET /api/v1/projects/{project} 查看项目配置 无
PATCH /api/v1/projects/{project} 修改项目名称、说明或状态 管理员
GET /api/v1/projects/{project}/packages 查看项目包列表 无
POST /api/v1/projects/{project}/packages/upload 上传项目包 开发者/管理员会话
GET /api/v1/projects/{project}/packages/{name}/{version}/download 下载项目包 无
GET /api/v1/projects/{project}/updates/stream 监听项目更新 无

项目状态为 disabled 时,项目配置仍可查询,但包列表、上传、下载和更新通知会被拒绝。项目目录位于:

data/projects/{project}/

接入 SSL、Smail 或后续项目时,只需在发布中心创建对应项目,再让宿主应用使用项目级客户端:

api, err := client.NewProject(
    "https://packages.example.com",
    "ssl",
    client.WithSessionToken(session),
)
if err != nil {
    return err
}
latest, available, err := api.CheckUpdate(ctx, "scert", currentVersion)

客户端不会修改接入项目;项目只通过 Scout API 和 pkg/client、pkg/updater 模块通信。

Go 客户端命令

编译命令行客户端:

go build -o .\scout-client.exe .\cmd\scout-client

检查更新:

.\scout-client.exe check --server http://127.0.0.1:8080 --project ssl --package demo-core --current 1.0.0

下载最新版本:

.\scout-client.exe download --server http://127.0.0.1:8080 --project ssl --package demo-core --output .\demo-core.pkg

监听更新通知:

.\scout-client.exe watch --server http://127.0.0.1:8080 --project ssl --packages demo-core

更新生命周期命令:

# 当前程序继续运行时下载并准备新版本
.\scout-client.exe prepare --server http://127.0.0.1:8080 --package demo-core --root .\.scout-update --current 1.0.0 --entrypoint demo-core
# 由用户决定后执行平台安装处理和版本切换
.\scout-client.exe install --server http://127.0.0.1:8080 --package demo-core --root .\.scout-update --current 1.0.0 --entrypoint demo-core
.\scout-client.exe activate --server http://127.0.0.1:8080 --package demo-core --root .\.scout-update --current 1.0.0 --entrypoint demo-core
# 新进程启动并确认健康后,Scout 才清理旧版本
.\scout-client.exe healthy --server http://127.0.0.1:8080 --package demo-core --root .\.scout-update --entrypoint demo-core

使用兼容令牌上传:

.\scout-client.exe upload `
  --server http://127.0.0.1:8080 `
  --token $env:SCOUT_DEVELOPER_TOKEN `
  --package demo-core `
  --version 1.2.0 `
  --file .\demo-core.pkg `
  --platform windows `
  --architecture amd64

Go 客户端模块

如果客户端本身也是 Go 程序,可以直接导入 scout/pkg/client。

登录账户:

api, err := client.New("http://127.0.0.1:8080")
if err != nil {
    return err
}
login, err := api.Login(ctx, "dev@example.com", "开发者密码")
if err != nil {
    return err
}
fmt.Println("登录用户:", login.User.Email)

登录后检查更新、下载和监听:

latest, available, err := api.CheckUpdate(ctx, "demo-core", "1.0.0")
if err != nil {
    return err
}
if available {
    _, err = api.Download(ctx, "demo-core", latest.Version, "./demo-core.pkg")
}
updates, err := api.WatchUpdates(ctx, "demo-core")
if err != nil {
    return err
}
for update := range updates {
    fmt.Println("收到更新:", update.Package.Name, update.Package.Version)
}

pkg/client 提供的方法包括:

方法 用途
New 创建客户端
WithDeveloperToken 配置兼容开发者令牌
WithSessionToken 配置登录会话令牌
Register 注册用户
Login 登录并自动保存会话令牌
Me 获取当前登录用户
Logout 退出并清理会话令牌
RequestPasswordReset 申请密码重置邮件
ResetPassword 使用令牌重置密码
ListPackages 获取包列表
GetLatest 获取指定包最新版本
ListVersions 获取指定包全部版本
CheckUpdate 比较当前版本并判断是否有更新
Download 下载并校验 SHA-256
WatchUpdates 监听 SSE 更新通知
Upload 使用兼容令牌或会话上传包

跨平台客户端集成

pkg/client 负责服务端协议,pkg/updater 负责下载、校验、暂存、激活、健康确认、回滚和旧版本清理。业务程序不需要依赖 internal 目录,只需注入 client.Client 创建 updater.Manager。

推荐由宿主程序按以下流程调用:

  1. 启动时创建 Manager,调用 RecoverStaleActivation 检查上一次更新是否超时。
  2. 后台调用 Check 或 Watch,收到对应平台和架构的更新通知。
  3. 在业务进程仍运行时调用 Prepare,完成下载、SHA-256 校验和暂存。
  4. 由用户决定立即重启还是稍后重启;立即重启前可调用 Install,再调用 Activate。
  5. 宿主程序退出并重新启动后调用 ConfirmHealthy,成功后才会清理旧版本。
  6. 启动失败或健康确认超时,调用 Rollback 恢复上一版本。

可运行的最小集成示例位于:

examples/client-integration/main.go

平台适配边界

updater.New 默认选择平台适配器,也可以通过 updater.Config.Adapter 注入宿主自己的实现:

  • Linux、Windows、macOS:默认适配器只启动已校验的版本入口,不执行系统级安装器,不停止当前进程。
  • Android:默认适配器只负责准备文件,Install 会返回 updater.ErrPlatformInstallRequired。宿主 Android 应用必须使用系统 PackageInstaller 或企业设备管理能力完成安装,并由系统授权流程决定是否允许安装。
  • macOS 应将 Entrypoint 指向 .app/Contents/MacOS/ 内的真实可执行文件,不能直接把 .app 目录当作入口。
  • Windows、Linux 和 macOS 的旧版本清理由健康确认后的 Cleanup 完成;清理前会保护当前版本和待激活版本。

Android 普通应用不能承诺无需用户确认的静默 APK 安装。Scout 不绕过系统授权,也不会通过提升权限实现静默安装;需要无人值守时必须由受控设备管理方案提供能力。

一个 Scout 包包含多个平台目标

.scout 文件本身是一个包含 scout-package.json 的 ZIP。开发者可以在同一个包中声明多个 targets,例如:

{
  "schema_version": 1,
  "name": "demo-core",
  "version": "1.2.0",
  "channel": "stable",
  "package_type": "full",
  "targets": [
    {
      "platform": "windows",
      "architecture": "amd64",
      "path": "targets/windows/amd64",
      "entrypoint": "targets/windows/amd64/demo-core.exe"
    },
    {
      "platform": "linux",
      "architecture": "amd64",
      "path": "targets/linux/amd64",
      "entrypoint": "targets/linux/amd64/demo-core"
    },
    {
      "platform": "macos",
      "architecture": "arm64",
      "path": "targets/macos/arm64",
      "entrypoint": "targets/macos/arm64/demo-core"
    },
    {
      "platform": "android",
      "architecture": "arm64",
      "path": "targets/android/arm64",
      "entrypoint": "targets/android/arm64/demo.apk"
    }
  ]
}

服务端上传时会解析每个目标并分类保存;客户端查询时只会收到匹配自身平台、架构、通道和当前版本的目标,其他客户端不会收到该更新。

跨平台构建示例

Go 客户端和服务端需要分别构建对应操作系统的二进制,不能把一个 Go 可执行文件直接运行在所有系统上;可以把这些二进制放进同一个 .scout 包,达到“一个包,多平台目标”的效果:

$env:CGO_ENABLED = "0"
$env:GOOS = "windows"; $env:GOARCH = "amd64"; go build -trimpath -o .\targets\windows\amd64\demo-core.exe .\cmd\scout-client
$env:GOOS = "linux"; $env:GOARCH = "amd64"; go build -trimpath -o .\targets\linux\amd64\demo-core .\cmd\scout-client
$env:GOOS = "darwin"; $env:GOARCH = "arm64"; go build -trimpath -o .\targets\macos\arm64\demo-core .\cmd\scout-client
Remove-Item Env:GOOS
Remove-Item Env:GOARCH
Remove-Item Env:CGO_ENABLED

Android APK 通常由 Android 工程和签名流水线生成,再放入 targets/android/<architecture>/;Scout 只负责分发和校验,不代替 Android 签名、系统安装授权或应用商店审核。

测试

go test ./...
go test -race ./...
Set-Location .\frontend
npm.cmd run build

服务器部署与反向代理

Scout 的服务端可以部署在 Linux 服务器上。推荐让 Scout 只监听内网地址,由现有的 Nginx、Caddy、云负载均衡或其他网关负责公网入口、域名、SSL 证书和 DNS。Scout 不会申请、续期或修改 SSL、DNS,也不会保存域名服务商凭据。

生产环境配置

复制示例配置并写入服务器的密钥管理位置:

cp deploy/scout.env.example /etc/scout/scout.env
chmod 600 /etc/scout/scout.env

必须替换以下占位内容:

  • SCOUT_PUBLIC_URL:客户端实际访问的完整地址,例如 https://packages.example.com。
  • SCOUT_ALLOWED_ORIGINS:允许访问 API 的前端来源,多个来源使用英文逗号分隔。
  • SCOUT_ALLOWED_HOSTS:反向代理转发到 Scout 的 Host,多个主机名使用英文逗号分隔。
  • SCOUT_ADMIN_EMAIL、SCOUT_ADMIN_PASSWORD:生产管理员邮箱和随机强密码。
  • SCOUT_SMTP_*:管理员、上传用户和安全认证通知使用的 SMTP 配置。

生产环境会拒绝默认测试管理员密码、缺少允许来源、缺少允许主机和缺少 SMTP 发件配置。SCOUT_ALLOW_LEGACY_TOKEN 默认关闭;推荐使用开发者账户会话上传,不要在公网长期保留兼容令牌。

原生 systemd 部署

建议在 Linux 服务器上直接运行 Go 编译后的二进制,并由 systemd 管理服务。

先在构建机编译服务端和前端:

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o scout-server ./cmd/scout-server
cd frontend
npm ci
npm run build
cd ..

将文件复制到服务器:

if ! id scout >/dev/null 2>&1; then
  sudo useradd --system --home /var/lib/scout --shell /usr/sbin/nologin scout
fi
sudo install -d -o scout -g scout /opt/scout /opt/scout/frontend /var/lib/scout /etc/scout
sudo install -m 0755 scout-server /opt/scout/scout-server
sudo cp -r frontend/dist /opt/scout/frontend/
sudo install -m 0600 deploy/scout.env.example /etc/scout/scout.env

编辑 /etc/scout/scout.env,替换所有生产占位符,然后设置权限:

sudo chown -R scout:scout /opt/scout /var/lib/scout
sudo chmod 600 /etc/scout/scout.env
sudo install -m 0644 deploy/scout.service /etc/systemd/system/scout.service
sudo systemctl daemon-reload
sudo systemctl enable --now scout.service
sudo systemctl status scout.service

deploy/scout.service 已配置自动重启、最小权限、只读系统保护和独立数据目录。停止或升级时使用:

sudo systemctl restart scout.service
sudo journalctl -u scout.service -f

Nginx 转发要求

deploy/nginx-native.conf 是 Nginx 参考配置,默认从 /opt/scout/frontend/dist 提供前端文件,并转发到本机 127.0.0.1:8080。部署时请将 server_name _; 按外部网关实际要求调整。

外部网关需要满足以下要求:

  • /api/ 转发到 Scout API。
  • /api/v1/updates/stream 关闭代理缓冲,并将读取超时设置为至少 1 小时。
  • 上传接口允许的请求体大小不小于 SCOUT_MAX_UPLOAD_BYTES。
  • 转发 Host、X-Real-IP、X-Forwarded-For 和 X-Forwarded-Proto。
  • 如果 SCOUT_TRUST_PROXY=true,只允许可信反向代理访问 Scout,不能让公网客户端直接伪造这些请求头。
  • SSL 证书、证书续期、DNS 解析和域名绑定由外部网关或云服务负责。

外部网关终止 HTTPS 后,必须将 X-Forwarded-Proto: https 传给 Scout;否则生产配置中的 SCOUT_FORCE_HTTPS=true 会拒绝请求。这是协议边界检查,不是 Scout 的证书管理功能。

deploy/Caddyfile 是 Caddy 反向代理参考配置,其中的示例主机名需要替换为实际访问地址。它直接代理本机 127.0.0.1:8080,并从 /opt/scout/frontend/dist 提供前端文件。它不会申请或配置 SSL 证书,也不会执行 DNS 解析、域名绑定或服务商 API 操作;如果 SSL 和 DNS 已由其他服务处理,请继续让该服务负责 TLS 终止,并把请求转发到 Scout 的入口。

健康检查、备份和升级

健康检查接口:

GET /healthz

返回 {"status":"ok"} 只代表进程可以响应。生产监控还应检查反向代理、磁盘空间、SMTP 可用性和实际包下载。

备份至少包括整个 SCOUT_DATA_DIR,其中包含用户数据、密码哈希、包元数据和包文件:

SCOUT_DATA_DIR=/var/lib/scout SCOUT_BACKUP_DIR=/var/backups/scout sh deploy/backup.sh

升级建议流程:

  1. 先执行备份并确认备份文件可读取。
  2. 在测试环境执行 go test ./... 和 npm run build。
  3. 编译并替换新的二进制文件和前端静态文件。
  4. 重启服务并检查 /healthz、用户登录、包列表、上传和下载。
  5. 观察日志和磁盘空间后再删除旧版本。
  6. 如升级异常,停止新版本并恢复旧二进制或旧前端文件,再恢复数据备份。

当前会话保存在内存中,服务端重启后已有登录会话会失效,客户端需要重新登录;用户和包数据不会因此丢失。