20 KiB
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。
推荐由宿主程序按以下流程调用:
- 启动时创建
Manager,调用RecoverStaleActivation检查上一次更新是否超时。 - 后台调用
Check或Watch,收到对应平台和架构的更新通知。 - 在业务进程仍运行时调用
Prepare,完成下载、SHA-256 校验和暂存。 - 由用户决定立即重启还是稍后重启;立即重启前可调用
Install,再调用Activate。 - 宿主程序退出并重新启动后调用
ConfirmHealthy,成功后才会清理旧版本。 - 启动失败或健康确认超时,调用
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
升级建议流程:
- 先执行备份并确认备份文件可读取。
- 在测试环境执行
go test ./...和npm run build。 - 编译并替换新的二进制文件和前端静态文件。
- 重启服务并检查
/healthz、用户登录、包列表、上传和下载。 - 观察日志和磁盘空间后再删除旧版本。
- 如升级异常,停止新版本并恢复旧二进制或旧前端文件,再恢复数据备份。
当前会话保存在内存中,服务端重启后已有登录会话会失效,客户端需要重新登录;用户和包数据不会因此丢失。