Files
fanqie-go/internal/config/config.go
T
chenxin 9aec09bb40 ✨ feat(manga,device): 漫画免签名兜底与 phone 注册身份选项
- /manga 设备池签名路线全部失败时自动降级 wkbrowser 免签名路线(materials/skeleton 三段策略收集原图直链,无需设备/签名/解密),响应附 source: wkbrowser
- 新增配置 fanqie.device_identity(默认 pad 保持现状;phone 对齐书源真机注册 payload:host_bit 64 + 补 hardware/device_device/fingerprint 字段)
- README 与 openapi.yaml 同步文档,新增 manga_fallback 解析测试
2026-10-12 14:53:08 +08:00

322 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"log"
"os"
"sync"
"sync/atomic"
"time"
"github.com/spf13/viper"
)
// Config 应用配置结构
type Config struct {
Server ServerConfig `mapstructure:"server"`
Fanqie FanqieConfig `mapstructure:"fanqie"`
Cache CacheConfig `mapstructure:"cache"`
DeviceKeyPool DeviceKeyPoolConfig `mapstructure:"device_key_pool"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Port string `mapstructure:"port"`
Host string `mapstructure:"host"`
AccessKey string `mapstructure:"access_key"`
ProtectAll bool `mapstructure:"protect_all"` // 业务接口也要求访问密钥(公网部署建议开启)
RateLimitQPS float64 `mapstructure:"rate_limit_qps"` // 每 IP 每秒请求上限,0=不限制
RequestLog RequestLogConfig `mapstructure:"request_log"` // 请求日志(只记元数据与错误摘要)
}
// RequestLogConfig 请求日志配置
type RequestLogConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否记录业务接口请求元数据
RetentionDays int `mapstructure:"retention_days"` // 日志保留天数,<=0 视为 7
}
// FanqieConfig 番茄小说配置
type FanqieConfig struct {
Algorithm string `mapstructure:"algorithm"`
Signature string `mapstructure:"signature"` // 签名方法: sign2 / sign4
// 注册设备身份: pad(默认,历史行为)/ phone(对齐手机 App 书源注册 payload)
DeviceIdentity string `mapstructure:"device_identity"`
Content ContentConfig `mapstructure:"content"`
Manga MangaConfig `mapstructure:"manga"`
Video VideoConfig `mapstructure:"video"`
}
// ContentConfig 内容处理配置
type ContentConfig struct {
RemoveImages bool `mapstructure:"remove_images"`
RemoveImageDesc bool `mapstructure:"remove_image_desc"`
RemoveBlankLines bool `mapstructure:"remove_blank_lines"`
RemoveHTMLTags bool `mapstructure:"remove_html_tags"`
RemoveChapterTitle bool `mapstructure:"remove_chapter_title"`
AddParagraphIndent bool `mapstructure:"add_paragraph_indent"`
DoubleNewline bool `mapstructure:"double_newline"` // 段落间使用双换行
EnableReview bool `mapstructure:"enable_review"` // 是否启用段评功能
}
// MangaConfig 漫画处理配置
type MangaConfig struct {
OutputMode string `mapstructure:"output_mode"` // 输出模式: data_uri / base64 / url
}
// VideoConfig 视频处理配置
type VideoConfig struct {
OutputMode string `mapstructure:"output_mode"` // 输出模式: url(原始CDN链接,内容加密)/ proxy(本地解密代理,可直接播放)
Quality string `mapstructure:"quality"` // 默认清晰度: max(最高档)/ 360p/480p/... 或档位 key(video_1..N),留空返回全部
CacheEnabled bool `mapstructure:"cache_enabled"` // 解密后视频的磁盘缓存
CachePath string `mapstructure:"cache_path"` // 缓存目录
}
// CacheConfig 缓存配置
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
Backend string `mapstructure:"backend"` // 存储后端: sqlite | postgres
SQLitePath string `mapstructure:"sqlite_path"`
Postgres PostgresConfig `mapstructure:"postgres"`
Redis RedisConfig `mapstructure:"redis"`
CleanupEnabled bool `mapstructure:"cleanup_enabled"` // 是否启用缓存清理
CleanupSeconds int `mapstructure:"cleanup_seconds"` // 缓存清理时间(秒)
}
// PostgresConfig PostgreSQL 连接配置
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
SSLMode string `mapstructure:"sslmode"` // disable | prefer | require
}
// RedisConfig Redis 缓存配置(启用后正文缓存与音频代理缓存走 Redis,统计数据与设备池仍在 SQL 库)
type RedisConfig struct {
Enabled bool `mapstructure:"enabled"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// DeviceKeyPoolConfig 设备密钥池配置
type DeviceKeyPoolConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用设备密钥池
InitialSize int `mapstructure:"initial_size"` // 初始化设备数量
MinSize int `mapstructure:"min_size"` // 最小设备数量
MaxSize int `mapstructure:"max_size"` // 最大设备数量
}
// ConfigManager 配置管理器
type ConfigManager struct {
mu sync.RWMutex
configPath string
callbacks []func(*Config)
}
var (
// appConfig 全局配置实例(原子指针,热更新替换时读取侧无数据竞争)
appConfig atomic.Pointer[Config]
manager *ConfigManager
)
// GetAppConfig 获取当前配置,未加载时返回 nil
func GetAppConfig() *Config {
return appConfig.Load()
}
// NewConfigManager 创建配置管理器
func NewConfigManager() *ConfigManager {
return &ConfigManager{
callbacks: make([]func(*Config), 0),
}
}
// ApplyTimezone 按 TZ 环境变量/.env 设置进程时区。
// 必须在任何时间显示之前调用(time.Local 首次使用后不再重读 TZ)。
// 环境变量已设 TZ 时以环境变量为准(.env 不覆盖已有环境变量)。
func ApplyTimezone() {
loadDotenv("./.env")
tz := os.Getenv("TZ")
if tz == "" {
return
}
if _, err := time.LoadLocation(tz); err != nil {
log.Printf("[config/WARN] 无效时区 TZ=%s,沿用系统时区: %v", tz, err)
return // 不写入 TZ:无效值会让 time.Local 落到 UTC
}
os.Setenv("TZ", tz)
}
// LoadConfig 加载配置文件
func LoadConfig(configPath string) error {
// 初始化管理器
if manager == nil {
manager = NewConfigManager()
manager.configPath = configPath
}
// 加载 .env(若存在),敏感配置可由环境变量覆盖
loadDotenv("./.env")
viper.SetConfigFile(configPath)
viper.SetConfigType("yaml")
// 设置默认值
setDefaults()
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
newConfig := &Config{}
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
appConfig.Store(newConfig)
return nil
}
// setDefaults 设置默认值
func setDefaults() {
// 服务器配置
viper.SetDefault("server.port", "40180")
viper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("server.request_log.enabled", false)
viper.SetDefault("server.request_log.retention_days", 7)
// 番茄小说配置
viper.SetDefault("fanqie.algorithm", "gorgon")
viper.SetDefault("fanqie.signature", "sign2")
viper.SetDefault("fanqie.device_identity", "pad")
// 内容处理配置
viper.SetDefault("fanqie.content.remove_images", false)
viper.SetDefault("fanqie.content.remove_image_desc", false)
viper.SetDefault("fanqie.content.remove_blank_lines", true)
viper.SetDefault("fanqie.content.remove_html_tags", false)
viper.SetDefault("fanqie.content.remove_chapter_title", false)
viper.SetDefault("fanqie.content.add_paragraph_indent", false)
viper.SetDefault("fanqie.content.double_newline", true)
viper.SetDefault("fanqie.content.enable_review", true)
// 漫画处理配置
viper.SetDefault("fanqie.manga.output_mode", "url")
// 视频处理配置
viper.SetDefault("fanqie.video.output_mode", "proxy")
viper.SetDefault("fanqie.video.quality", "max")
viper.SetDefault("fanqie.video.cache_enabled", true)
viper.SetDefault("fanqie.video.cache_path", "./video_cache")
// 缓存配置
viper.SetDefault("cache.enabled", true)
viper.SetDefault("cache.backend", "sqlite")
viper.SetDefault("cache.sqlite_path", "./cache.db")
viper.SetDefault("cache.cleanup_enabled", false)
viper.SetDefault("cache.cleanup_seconds", 86400)
// PostgreSQL 配置
viper.SetDefault("cache.postgres.host", "127.0.0.1")
viper.SetDefault("cache.postgres.port", 5432)
viper.SetDefault("cache.postgres.user", "postgres")
viper.SetDefault("cache.postgres.password", "")
viper.SetDefault("cache.postgres.database", "fanqie")
viper.SetDefault("cache.postgres.sslmode", "disable")
// Redis 配置
viper.SetDefault("cache.redis.enabled", false)
viper.SetDefault("cache.redis.host", "127.0.0.1")
viper.SetDefault("cache.redis.port", 6379)
viper.SetDefault("cache.redis.password", "")
viper.SetDefault("cache.redis.db", 0)
// 设备密钥池配置
viper.SetDefault("device_key_pool.enabled", true)
viper.SetDefault("device_key_pool.initial_size", 3)
viper.SetDefault("device_key_pool.min_size", 2)
viper.SetDefault("device_key_pool.max_size", 10)
}
// GetConfig 获取配置实例
func GetConfig() *Config {
if cfg := appConfig.Load(); cfg != nil {
return cfg
}
log.Fatal("配置未初始化,请先调用 LoadConfig")
return nil
}
// ReloadConfig 重新加载配置文件
func ReloadConfig() error {
if manager == nil {
return fmt.Errorf("配置管理器未初始化")
}
manager.mu.RLock()
configPath := manager.configPath
manager.mu.RUnlock()
if configPath == "" {
return fmt.Errorf("配置文件路径为空")
}
// 重新读取配置文件
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
return fmt.Errorf("重新读取配置文件失败: %w", err)
}
newConfig := &Config{}
if err := viper.Unmarshal(newConfig); err != nil {
return fmt.Errorf("重新解析配置文件失败: %w", err)
}
applyEnvOverrides(newConfig)
// 更新配置
manager.mu.Lock()
var oldAlgorithm string
if old := appConfig.Load(); old != nil {
oldAlgorithm = old.Fanqie.Algorithm
}
appConfig.Store(newConfig)
callbacks := make([]func(*Config), len(manager.callbacks))
copy(callbacks, manager.callbacks)
manager.mu.Unlock()
// 通知配置更新
for _, callback := range callbacks {
callback(newConfig)
}
log.Printf("配置重新加载成功: %s", configPath)
if oldAlgorithm != newConfig.Fanqie.Algorithm {
log.Printf("算法已切换: %s -> %s", oldAlgorithm, newConfig.Fanqie.Algorithm)
}
return nil
}
// RegisterConfigCallback 注册配置更新回调
func RegisterConfigCallback(callback func(*Config)) {
if manager == nil {
log.Printf("警告: 配置管理器未初始化,无法注册回调")
return
}
manager.mu.Lock()
defer manager.mu.Unlock()
manager.callbacks = append(manager.callbacks, callback)
}
// GetConfigManager 获取配置管理器实例
func GetConfigManager() *ConfigManager {
return manager
}