237 lines
6.7 KiB
Go
237 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
// Config 主配置结构
|
|
type Config struct {
|
|
Storage StorageConfig `toml:"storage"`
|
|
Server ServerConfig `toml:"server"`
|
|
Channels []ChannelConfig `toml:"channels"`
|
|
}
|
|
|
|
// StorageConfig 存储配置
|
|
type StorageConfig struct {
|
|
Enabled bool `toml:"enabled"`
|
|
Path string `toml:"path"`
|
|
}
|
|
|
|
// ServerConfig 服务器配置
|
|
type ServerConfig struct {
|
|
Enabled bool `toml:"enabled"` // 是否启用 Web 服务器
|
|
Listen string `toml:"listen"` // 监听地址,如 ":8080"
|
|
AllowedOrigins []string `toml:"allowed_origins"` // 允许的 WebSocket Origin 列表,空则仅允许同源
|
|
}
|
|
|
|
// ChannelConfig 通道配置
|
|
type ChannelConfig struct {
|
|
Type string `toml:"type" json:"type"` // email, bark, gotify, serverchan, pushdeer, webhook
|
|
Enabled bool `toml:"enabled" json:"enabled"` // 是否启用,默认 false
|
|
|
|
// Common
|
|
RequestTimeoutSec int `toml:"request_timeout_sec" json:"request_timeout_sec"` // HTTP 请求超时秒数,默认 10
|
|
AllowPrivateNetwork bool `toml:"allow_private_network" json:"allow_private_network"` // 是否允许访问私网地址(仅 webhook)
|
|
|
|
// Email
|
|
Host string `toml:"host" json:"host"`
|
|
Port int `toml:"port" json:"port"`
|
|
Username string `toml:"username" json:"username"`
|
|
Password string `toml:"password" json:"password"`
|
|
From string `toml:"from" json:"from"`
|
|
To []string `toml:"to" json:"to"`
|
|
UseTLS bool `toml:"use_tls" json:"use_tls"`
|
|
|
|
// Bark
|
|
Endpoint string `toml:"endpoint" json:"endpoint"`
|
|
Title string `toml:"title" json:"title"`
|
|
|
|
// Gotify
|
|
Token string `toml:"token" json:"token"`
|
|
Priority int `toml:"priority" json:"priority"`
|
|
|
|
// ServerChan
|
|
SendKey string `toml:"send_key" json:"send_key"`
|
|
|
|
// Webhook
|
|
URL string `toml:"url" json:"url"`
|
|
Method string `toml:"method" json:"method"`
|
|
Headers map[string]string `toml:"headers" json:"headers"`
|
|
|
|
// WeCom (企业微信)
|
|
CorpID string `toml:"corp_id" json:"corp_id"`
|
|
CorpSecret string `toml:"corp_secret" json:"corp_secret"`
|
|
AgentID int `toml:"agent_id" json:"agent_id"`
|
|
ToUser string `toml:"to_user" json:"to_user"`
|
|
|
|
// 飞书 (Lark SDK)
|
|
AppID string `toml:"app_id" json:"app_id"` // 飞书应用 App ID
|
|
AppSecret string `toml:"app_secret" json:"app_secret"` // 飞书应用 App Secret
|
|
ReceiveID string `toml:"receive_id" json:"receive_id"` // 接收者 ID
|
|
ReceiveIDType string `toml:"receive_id_type" json:"receive_id_type"` // 接收者 ID 类型: open_id, user_id, union_id, email, chat_id
|
|
|
|
// 钉钉(Webhook 机器人)
|
|
WebhookURL string `toml:"webhook_url" json:"webhook_url"` // 钉钉机器人 Webhook 地址
|
|
SignSecret string `toml:"sign_secret" json:"sign_secret"` // 钉钉签名密钥(可选)
|
|
|
|
// Telegram Bot
|
|
BotToken string `toml:"bot_token" json:"bot_token"` // Telegram Bot Token
|
|
ChatID string `toml:"chat_id" json:"chat_id"` // Telegram 目标 Chat ID
|
|
APIURL string `toml:"api_url" json:"api_url"` // Telegram API 地址 (可选,默认 https://api.telegram.org)
|
|
}
|
|
|
|
// Load 加载配置文件
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("读取配置文件: %w", err)
|
|
}
|
|
|
|
var cfg Config
|
|
if err := toml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("解析配置文件: %w", err)
|
|
}
|
|
|
|
return &cfg, nil
|
|
}
|
|
|
|
// channelToMap 将通道配置转换为 map,只包含该类型通道的相关字段
|
|
func channelToMap(ch ChannelConfig) map[string]any {
|
|
m := map[string]any{
|
|
"type": ch.Type,
|
|
"enabled": ch.Enabled,
|
|
}
|
|
if ch.RequestTimeoutSec > 0 {
|
|
m["request_timeout_sec"] = ch.RequestTimeoutSec
|
|
}
|
|
|
|
switch ch.Type {
|
|
case "email":
|
|
m["host"] = ch.Host
|
|
m["port"] = ch.Port
|
|
m["username"] = ch.Username
|
|
m["password"] = ch.Password
|
|
m["from"] = ch.From
|
|
m["to"] = ch.To
|
|
if ch.UseTLS {
|
|
m["use_tls"] = ch.UseTLS
|
|
}
|
|
case "bark":
|
|
m["endpoint"] = ch.Endpoint
|
|
if ch.Title != "" {
|
|
m["title"] = ch.Title
|
|
}
|
|
case "gotify":
|
|
m["endpoint"] = ch.Endpoint
|
|
m["token"] = ch.Token
|
|
if ch.Priority != 0 {
|
|
m["priority"] = ch.Priority
|
|
}
|
|
case "serverchan":
|
|
m["send_key"] = ch.SendKey
|
|
case "webhook":
|
|
m["url"] = ch.URL
|
|
m["method"] = ch.Method
|
|
if ch.AllowPrivateNetwork {
|
|
m["allow_private_network"] = ch.AllowPrivateNetwork
|
|
}
|
|
if len(ch.Headers) > 0 {
|
|
m["headers"] = ch.Headers
|
|
}
|
|
case "wecom":
|
|
m["corp_id"] = ch.CorpID
|
|
m["corp_secret"] = ch.CorpSecret
|
|
m["agent_id"] = ch.AgentID
|
|
if ch.ToUser != "" {
|
|
m["to_user"] = ch.ToUser
|
|
}
|
|
case "feishu":
|
|
m["app_id"] = ch.AppID
|
|
m["app_secret"] = ch.AppSecret
|
|
m["receive_id"] = ch.ReceiveID
|
|
if ch.ReceiveIDType != "" {
|
|
m["receive_id_type"] = ch.ReceiveIDType
|
|
}
|
|
if ch.Title != "" {
|
|
m["title"] = ch.Title
|
|
}
|
|
case "dingtalk":
|
|
m["webhook_url"] = ch.WebhookURL
|
|
if ch.SignSecret != "" {
|
|
m["sign_secret"] = ch.SignSecret
|
|
}
|
|
if ch.Title != "" {
|
|
m["title"] = ch.Title
|
|
}
|
|
case "telegram":
|
|
m["bot_token"] = ch.BotToken
|
|
m["chat_id"] = ch.ChatID
|
|
if ch.APIURL != "" {
|
|
m["api_url"] = ch.APIURL
|
|
}
|
|
default:
|
|
// 未知类型:序列化所有非零字段
|
|
m["host"] = ch.Host
|
|
m["port"] = ch.Port
|
|
m["username"] = ch.Username
|
|
m["password"] = ch.Password
|
|
m["from"] = ch.From
|
|
m["to"] = ch.To
|
|
m["endpoint"] = ch.Endpoint
|
|
m["token"] = ch.Token
|
|
m["send_key"] = ch.SendKey
|
|
m["url"] = ch.URL
|
|
m["method"] = ch.Method
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Save 保存配置到文件(原子写入:先写临时文件,再重命名)
|
|
func (c *Config) Save(path string) error {
|
|
// 在目标文件同目录创建临时文件,确保同一文件系统以支持原子 Rename
|
|
dir := filepath.Dir(path)
|
|
tmpFile, err := os.CreateTemp(dir, ".config-*.toml.tmp")
|
|
if err != nil {
|
|
return fmt.Errorf("创建临时文件: %w", err)
|
|
}
|
|
tmpPath := tmpFile.Name()
|
|
|
|
// 构建用于保存的配置,过滤掉每个通道不相关的字段
|
|
saveConfig := struct {
|
|
Storage StorageConfig `toml:"storage"`
|
|
Server ServerConfig `toml:"server"`
|
|
Channels []any `toml:"channels"`
|
|
}{
|
|
Storage: c.Storage,
|
|
Server: c.Server,
|
|
Channels: make([]any, 0, len(c.Channels)),
|
|
}
|
|
|
|
for _, ch := range c.Channels {
|
|
saveConfig.Channels = append(saveConfig.Channels, channelToMap(ch))
|
|
}
|
|
|
|
encoder := toml.NewEncoder(tmpFile)
|
|
if err := encoder.Encode(saveConfig); err != nil {
|
|
tmpFile.Close()
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("编码配置文件: %w", err)
|
|
}
|
|
|
|
if err := tmpFile.Close(); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("关闭临时文件: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(tmpPath, path); err != nil {
|
|
os.Remove(tmpPath)
|
|
return fmt.Errorf("重命名配置文件: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|