Documentation
¶
Overview ¶
API Key 临期站内通知(波次3):定期扫描 7 天内到期的有效 Key, 给属主发站内信(带 Webhook 外推),并打去重标记。 通知失败不阻塞扫描(下轮重试);标记成功才视为已通知,绝不重复打扰。
datamigrate.go — 启动期数据迁移(2026-10-26 兼容层清理)。
为什么要 Go 侧迁移而不是 migrations/*.sql: 版本化迁移只在 database.migrate=true 时执行;默认路径走 GORM AutoMigrate, 而 AutoMigrate **不会**做数据回填(也不会删列)。本轮退役的兼容层里有 「存量数据必须先补齐,代码才能删掉兜底」的部分(子表回填),必须两条路径 都生效 ⇒ 实现为幂等的 Go 侧迁移。
全部步骤都幂等:可重复执行,失败只记日志不阻断启动(数据类迁移失败不应 让站点起不来;但读取侧会因缺数据而报错,故日志必须响亮)。
setup_gate.go —— 首启向导(/setup)的暴露面加固(2026-10-22)。
问题 ¶
/setup 是**无鉴权**的建号端点,唯一的护栏是「库里还没有 admin」。 ⇒ 一个尚未初始化的实例,**任何能访问到它的人都可以抢先完成首启**并拿到 管理员(setup-race)。此前四个打包面把「没配密码 → 走 /setup」设为默认 (hub/ugreen/terramaster 的 compose entrypoint、qnap 的 prepare_env), 这条默认把暴露面从「弱口令 admin/admin123」换成了「先到先得的管理员」—— 在**内网**是体验升级,在**公网**是新的暴露面。
三道加固(都不改变「不配密码也能用」的默认体验) ¶
- **可选令牌**:设 PB_SETUP_TOKEN 后,建号端点需带匹配的 X-Setup-Token 头 (或 ?token= 查询参数 —— 便于把「服务启动日志里的那条链接」直接粘进 浏览器完成首启)。未设 = 不要求(保持默认体验不变)。
- **按 IP 失败锁定**:复用 middleware.Lockout(Redis 共享计数,多副本 生效;无 Redis 退进程内)。**只把「令牌不匹配」计为失败**——校验 参数不合法(弱口令等)是用户自己的输入问题,不该把他锁在门外。
- **启动告警**:未初始化且未设令牌时,响亮提示暴露面与两种处置办法。
为什么不用「必须令牌」 ¶
强制令牌会把「打开浏览器就走完首启」这条最顺的路变成「先翻日志抄令牌」, 与「多部署方式下的第一天体验」这个目标相反。故做成**可选加固**: 内网自托管(NAS)默认零摩擦;公网部署按需一行 env 打开。
Index ¶
- Variables
- func Bootstrap(configPath string) (*server.Hertz, error)
- func BootstrapWithOptions(configPath string, opts ...Option) (*server.Hertz, error)
- func CORS() app.HandlerFunc
- func Cleanup()
- func CreateDefaultAdmin(database *gorm.DB, cfg *conf.AppConfiguration) error
- func InitDatabase(config *conf.DatabaseConfig) (*gorm.DB, error)
- func NewOfflineBackupService(cfg *conf.AppConfiguration, database *gorm.DB) (*backup.Service, error)
- func ReapplyEnvOverrides()
- func RunDataMigrations(gormDB *gorm.DB)
- type CapabilitySnapshot
- type Config
- type DiagnosticsDatabase
- type DiagnosticsRedis
- type DiagnosticsReport
- type DiagnosticsSetup
- type DiagnosticsSite
- type DiagnosticsStorage
- type Option
- type Options
- type PublicCapabilities
Constants ¶
This section is empty.
Variables ¶
var ErrInsecureDefaultAdmin = errors.New("refusing to create default admin with known default password in production mode")
ErrInsecureDefaultAdmin 生产模式下拒绝以已知默认口令创建默认管理员时返回 (errors.Is 可判)。逃生门:PB_ADMIN_PASSWORD 注入强密码,或 PB_DISABLE_DEFAULT_ADMIN=true 走 /setup 首启向导。
Functions ¶
func Bootstrap ¶
Bootstrap 应用程序启动入口(兼容入口,等价于无选项的 BootstrapWithOptions)。 configPath 为空时依次回退到 CONFIG_PATH 环境变量、默认 configs/config.yaml。
func BootstrapWithOptions ¶
BootstrapWithOptions 带函数选项的启动入口。 可用选项见 options.go(如 WithStaticDir 覆盖前端静态资源目录)。
func CORS ¶
func CORS() app.HandlerFunc
CORS 跨域中间件(配置化)。
安全策略:
- 配置了 allow_origins 白名单时,仅放行白名单内的 Origin(生产推荐)
- 未配置白名单时,退化为反射 Origin(便于本地开发,等同于宽松模式)
- allow_credentials=true 时,绝不返回 "*",而是精确匹配的 Origin
同时允许 X-Trace-Id / X-API-Key 等自定义请求头跨域。 allow_origins 来源:yaml 的 security.cors.allow_origins(数组)或 环境变量 PB_CORS_ALLOW_ORIGINS(逗号分隔,如 "https://a.com,https://b.com")。
func Cleanup ¶
func Cleanup()
Cleanup 清理资源(优雅停机编排,oekit core/lifecycle——Manager 原语自 logx 迁来,logx 依赖随之移除):teardown 先取消后台任务根 ctx(拦下一轮),随后 各资源按注册序、各自独立超时预算排水——panic 隔离,单个资源卡死不拖垮整体 停机;在飞轮次由 bg-tasks 预算内等其跑完。停机失败经 Errors() 落回 zap (Manager 内部日志走自身门面,宿主观测以 Errors 为准)。
func CreateDefaultAdmin ¶
func CreateDefaultAdmin(database *gorm.DB, cfg *conf.AppConfiguration) error
CreateDefaultAdmin 创建默认管理员。
密码来源(优先级):PB_ADMIN_PASSWORD 环境变量 > 默认 admin123。 密码用 bcrypt 现场哈希(此前硬编码的哈希与 admin123 不匹配,导致管理员无法登录)。 生产模式(app.production 或 server.mode=release)下若未注入 PB_ADMIN_PASSWORD 且库中无管理员,返回 ErrInsecureDefaultAdmin 由调用方终止启动——已知弱口令 不允许上线(2026-10-08 审计:此前仅 Warn,与 JWT secret 的全环境 fail-fast 不对称)。 仅在"即将创建"时校验:库中已有 admin 的存量部署升级不受影响(每次启动都查会 打断未配 env 的老部署)。cfg 允许 nil(视为非生产,保持旧行为)。
func InitDatabase ¶
func InitDatabase(config *conf.DatabaseConfig) (*gorm.DB, error)
InitDatabase 初始化数据库。
迁移策略(按配置):
- database.migrate=true:先执行版本化迁移(migrations/*.sql),适合生产/需要版本控制
- database.auto_migrate(默认 true):GORM AutoMigrate,开发友好、自动补表/列
- 两者可共存:版本化迁移建表后,AutoMigrate 兜底补充新字段
func NewOfflineBackupService ¶ added in v0.32.38
func NewOfflineBackupService(cfg *conf.AppConfiguration, database *gorm.DB) (*backup.Service, error)
NewOfflineBackupService 构造**离线**备份服务(导出 / 恢复用;不启动 HTTP 服务)。
为什么是「离线」而不是管理面端点 ¶
导出与恢复天然是**同机**操作(要读本地 DB 与本地存储):
· 做成 HTTP 端点 ⇒ 多一层「服务端正在运行 ⇒ 恢复会砸自己的脚」的风险; · `pb` 是**纯 HTTP 客户端**(刻意不链接 core)⇒ 让 `pb backup` 落地就得把 core 拖进这个轻量 CLI(体积 + 数十 MB)。
⇒ 形态定为**同机离线子命令**(server 二进制:`pigeonbox backup export` / `pigeonbox restore`),与「恢复必须停服」的事实一致。
前置 ¶
调用方必须先完成:InitConfig → conf.SetGlobalConfig → InitDatabase (否则 DB 与存储后端都没就绪)。本函数只做**装配**,不负责初始化。
func ReapplyEnvOverrides ¶ added in v0.22.0
func ReapplyEnvOverrides()
ReapplyEnvOverrides 把非空 env 绑定值重新施加到全局 conf(幂等)。
由 config service 的 OnOverlayApplied 钩子在每次 DB overlay 后调用; 启动期因钩子注册晚于首次 RestoreAdminSettings,wiring 在注册后显式补调一次。
⚠️ **copy-on-write**(2026-10-31 更正) ¶
此前本函数**原地改写**调用方传入的 conf 指针(bootstrap 包级 config)。 自全局配置改为 copy-on-write(原子换指针)后:
- 传入的 bootstrap.config 是**被换掉的旧对象** ⇒ env 覆盖**根本没作用到 当前快照** ⇒ 「env 恒覆盖」这条配置优先级规则**静默失效**;
- 更糟:旧对象被改后再被 COW 的浅拷贝带走 ⇒ env 覆盖值会**污染后续所有 快照**(看起来像生效了,实际是脏值扩散)。
⇒ 现在走 conf.UpdateGlobalConfig:在**当前快照的副本**上反射改字段, 再原子换指针 ⇒ env 覆盖既生效、又不会造成撕裂读。
func RunDataMigrations ¶ added in v0.32.14
RunDataMigrations 启动期数据迁移入口(幂等)。
调用时机:InitDatabase 之后(AutoMigrate 已补齐表结构)、对外服务之前 (读取侧必须先于迁移完成——子表是文件清单的唯一真相源)。
Types ¶
type CapabilitySnapshot ¶ added in v0.32.30
type CapabilitySnapshot struct {
// DeploymentMode 部署模式:standalone | public | admin。
// 多副本拓扑下前端据此知道「本副本是公开面/管理面」,不再各面各猜。
DeploymentMode string `json:"deployment_mode"`
// PublicURLSet 站点公网地址是否已配置(base.public_url 或 server.base_url)。
// 未配置时分享链接按**请求来源**拼 ⇒ 内网部署会把内网 IP 发给外部用户。
PublicURLSet bool `json:"public_url_set"`
// SetupTokenRequired 首启建号是否需要令牌(自动生成的一次性令牌或 PB_SETUP_TOKEN)。
// true = 未初始化实例的 /setup 会被门禁拦下(前端据此提示「令牌在启动日志里」)。
SetupTokenRequired bool `json:"setup_token_required"`
// Federation 联邦是否启用(**生效值**:多副本下自动降级关闭)。
Federation bool `json:"federation"`
// RedisMode redis | memory(redis.host 空 = 单机内存模式)。
RedisMode string `json:"redis_mode"`
// StorageBackend 当前存储后端(**生效值**,含远端构造失败后的降级结果)。
StorageBackend string `json:"storage_backend"`
// StorageDegraded 是否发生降级(配置的后端 ≠ 生效后端 ⇒ 远端构造失败回落 local)。
// ⚠️ 这是**真实故障的信号**:用户以为在用 S3,实际全落在本地盘上。
StorageDegraded bool `json:"storage_degraded"`
}
CapabilitySnapshot 描述「本实例实际支持什么」。
⚠️ 字段语义一律是**生效值**(runtime 真实状态),不是配置回显—— 后者会与实际漂移(如后端启动后才起 Redis、远端存储构造失败降级 local)。
type Config ¶
type Config = conf.AppConfiguration
Config 与 internal/conf 包中的统一配置类型互为别名。
func InitConfig ¶
InitConfig 初始化配置。
配置来源优先级(高 → 低):
- 环境变量(PB_ 前缀完整名 / 文档化的短名,见 bindEnvironment)
- 配置文件(yaml,路径由 configPath 或 CONFIG_PATH 决定)
- 代码内默认值(SetDefault)
这样容器化部署(K8s/Docker)可通过 env 注入敏感配置(jwt_secret、db 密码等), 而无需修改镜像内的配置文件,符合 12-factor。
type DiagnosticsDatabase ¶ added in v0.32.30
type DiagnosticsDatabase struct {
// Driver 驱动名(sqlite / mysql / postgres)。
Driver string `json:"driver"`
// Reachable 是否可连通(Ping 成功)。
Reachable bool `json:"reachable"`
// Error 连通失败原因(reachable=true 时为空)。
Error string `json:"error,omitempty"`
}
DiagnosticsDatabase 数据库连通性。
type DiagnosticsRedis ¶ added in v0.32.30
type DiagnosticsRedis struct {
// Mode redis | memory。
Mode string `json:"mode"`
// Address Redis 地址(mode=memory 时为空)。
Address string `json:"address,omitempty"`
}
DiagnosticsRedis 缓存模式。
type DiagnosticsReport ¶ added in v0.32.30
type DiagnosticsReport struct {
// Version **构建版本**(oekit/core/version.Version,由 ldflags 注入)。
// ⚠️ 不是 config.App.Version —— 那是 yaml 里的 app.version(缺省 1.0.0,
// 常年不更新)⇒ 真机上 doctor 会把 4.2.0 的实例报成「1.0.0」。
// 版本披露用于 CVE 匹配,必须与 /version 端点**同源**。
Version string `json:"version"`
// BuildCommit 构建 commit(版本对齐用)。
BuildCommit string `json:"build_commit,omitempty"`
// BuildTime 构建时间。
BuildTime string `json:"build_time,omitempty"`
// DeploymentMode 部署模式:standalone | public | admin。
DeploymentMode string `json:"deployment_mode"`
// ServerTime 服务端当前时间(CLI 用它与本机时钟比对 ⇒ 时钟偏差)。
ServerTime time.Time `json:"server_time"`
// Database 数据库连通性。
Database DiagnosticsDatabase `json:"database"`
// Storage 存储后端状态。
Storage DiagnosticsStorage `json:"storage"`
// Redis 缓存模式。
Redis DiagnosticsRedis `json:"redis"`
// Site 站点对外地址状态。
Site DiagnosticsSite `json:"site"`
// Setup 首启状态。
Setup DiagnosticsSetup `json:"setup"`
// Capabilities 能力快照(生效值)。
Capabilities CapabilitySnapshot `json:"capabilities"`
}
DiagnosticsReport 部署自检报告(一份快照)。
type DiagnosticsSetup ¶ added in v0.32.30
type DiagnosticsSetup struct {
// Initialized 是否已初始化(已有 admin)。
Initialized bool `json:"initialized"`
// TokenRequired 建号是否需要令牌。
TokenRequired bool `json:"token_required"`
// TokenSource 令牌来源:env | auto | none。
TokenSource string `json:"token_source"`
}
DiagnosticsSetup 首启状态。
type DiagnosticsSite ¶ added in v0.32.30
type DiagnosticsSite struct {
// PublicURL 生效的站点公网地址(未配置时为空)。
PublicURL string `json:"public_url,omitempty"`
// PublicURLSet 是否已配置。
PublicURLSet bool `json:"public_url_set"`
}
DiagnosticsSite 站点对外地址状态。
type DiagnosticsStorage ¶ added in v0.32.30
type DiagnosticsStorage struct {
// Backend **生效**后端(远端构造失败会降级 local ⇒ 这里与 Configured 可能不同)。
Backend string `json:"backend"`
// Configured 配置里指定的后端。
Configured string `json:"configured"`
// Degraded 是否降级(Backend ≠ Configured ⇒ 远端构造失败回落 local)。
Degraded bool `json:"degraded"`
// DataPath 本地存储根目录(远端模式下仍承载分片合并副本)。
DataPath string `json:"data_path,omitempty"`
// InitError 降级原因(无降级时为空)。
InitError string `json:"init_error,omitempty"`
}
DiagnosticsStorage 存储后端状态。
type Option ¶
type Option func(*Options)
Option 是 Bootstrap 的可选配置项(函数选项模式)。
用法:
h, err := bootstrap.BootstrapWithOptions(configPath,
bootstrap.WithStaticDir("/opt/pb/ui"),
)
兼容性:Bootstrap(configPath) 等价于无选项的 BootstrapWithOptions, 既有调用方(server、fnos)不受影响。
type Options ¶
type Options struct {
// StaticDir 是前端构建产物的根目录。
// 该目录需包含 index.html 与 assets/(Vite 构建产物布局)。
// 默认 "./static"(相对进程工作目录)。
// 典型消费者:fnos 单容器部署时把前端内嵌到自定义路径。
StaticDir string
}
Options 控制 Bootstrap 的行为。零值字段取默认。
type PublicCapabilities ¶ added in v0.32.31
type PublicCapabilities struct {
// DeploymentMode 部署模式:standalone | public | admin。
DeploymentMode string `json:"deployment_mode"`
// PublicURLSet 站点公网地址是否已配置。
PublicURLSet bool `json:"public_url_set"`
// SetupTokenRequired 首启建号是否需要令牌。
SetupTokenRequired bool `json:"setup_token_required"`
}
PublicCapabilities 公开面(/api/config)下发的能力子集。
⚠️ **独立结构体**,不复用 CapabilitySnapshot:复用会把管理面字段 (redis_mode / storage_backend / …)以**空串**形态泄到公开 JSON 里 ⇒ 客户端读到的 `redis_mode: ""` 既不是事实、也不是"未知"的可辨识形态。