feat: 完善后端并新增 Vue3 WebUI
后端: - 修复登录校验、DB 并发(单例+WAL+纯Go sqlite)、会话密钥持久化等核心缺陷 - 新增 Timer CRUD/签到/手动触发、执行日志、仪表盘、驱动元数据等 API - 统一驱动接口: 带 context 的执行器、真实错误返回、敏感字段掩码、连通性测试 - 补全钉钉执行器; Timer 触发/提醒去重; 配置支持默认值与环境变量覆盖 前端: - Vite+Vue3+TS+Pinia+Element Plus, 由驱动元数据动态渲染配置表单 - 登录/仪表盘(倒计时环)/定时器/规则/账号/日志/设置页面, 支持暗色模式 - 主题色 #66ccff, 信号波纹 logo, go:embed 单二进制部署 构建: Dockerfile 增加前端构建阶段; 补充各驱动配置文档 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+177
-13
@@ -1,31 +1,195 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// type DingtalkBot struct {
|
||||
// AccessToken string `json:"access_token"`
|
||||
// Secret string `json:"secret"`
|
||||
// }
|
||||
// 默认配置文件路径,可通过环境变量 GOODBABY_CONFIG 覆盖
|
||||
const defaultConfigPath = "config.json"
|
||||
|
||||
type Config struct {
|
||||
EnableRegistry bool `json:"enable_registry"`
|
||||
TimeoutDurationHours int `json:"timeout_duration_hours"`
|
||||
// HTTP 监听地址,如 ":8088"
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
// 是否开放注册
|
||||
EnableRegistry bool `json:"enable_registry"`
|
||||
// 规则执行的最长重试时间(小时),用于指数退避的整体超时
|
||||
TimeoutDurationHours int `json:"timeout_duration_hours"`
|
||||
// 检查 timer 的间隔(分钟)
|
||||
CheckIntervalMinutes int `json:"check_interval_minutes"`
|
||||
// sqlite 数据库文件路径
|
||||
DatabasePath string `json:"database_path"`
|
||||
// session 加密密钥,为空时自动生成并写回配置文件
|
||||
SessionSecret string `json:"session_secret"`
|
||||
// session 有效期(小时)
|
||||
SessionMaxAgeHours int `json:"session_max_age_hours"`
|
||||
// 允许跨域访问的来源,开发前端时使用,如 ["http://localhost:5173"]
|
||||
AllowedOrigins []string `json:"allowed_origins"`
|
||||
// 每个用户保留的执行日志条数,<=0 表示不限制
|
||||
LogRetainCount int `json:"log_retain_count"`
|
||||
}
|
||||
|
||||
// Get config from json file
|
||||
func GetConfig() (config Config, err error) {
|
||||
content, err := os.ReadFile("config.json")
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
ListenAddr: ":8088",
|
||||
EnableRegistry: true,
|
||||
TimeoutDurationHours: 6,
|
||||
CheckIntervalMinutes: 10,
|
||||
DatabasePath: "data.db",
|
||||
SessionMaxAgeHours: 24 * 7,
|
||||
AllowedOrigins: []string{},
|
||||
LogRetainCount: 500,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
cached Config
|
||||
loadErr error
|
||||
cacheMux sync.RWMutex
|
||||
)
|
||||
|
||||
// GetConfig 读取配置,只在首次调用时读盘,之后返回缓存。
|
||||
//
|
||||
// 配置文件不存在时会以默认值创建一份,缺失的字段会被补齐并写回。
|
||||
func GetConfig() (Config, error) {
|
||||
once.Do(func() {
|
||||
cfg, err := load()
|
||||
cacheMux.Lock()
|
||||
cached, loadErr = cfg, err
|
||||
cacheMux.Unlock()
|
||||
})
|
||||
|
||||
cacheMux.RLock()
|
||||
defer cacheMux.RUnlock()
|
||||
return cached, loadErr
|
||||
}
|
||||
|
||||
// MustGetConfig 与 GetConfig 相同,但在出错时直接退出,用于启动阶段
|
||||
func MustGetConfig() Config {
|
||||
cfg, err := GetConfig()
|
||||
if err != nil {
|
||||
return config, err
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
func configPath() string {
|
||||
if p := os.Getenv("GOODBABY_CONFIG"); p != "" {
|
||||
return p
|
||||
}
|
||||
return defaultConfigPath
|
||||
}
|
||||
|
||||
func load() (Config, error) {
|
||||
path := configPath()
|
||||
config := defaultConfig()
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
switch {
|
||||
case err == nil:
|
||||
if err := json.Unmarshal(content, &config); err != nil {
|
||||
return config, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
|
||||
}
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
log.Printf("配置文件 %s 不存在,使用默认配置创建", path)
|
||||
default:
|
||||
return config, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &config)
|
||||
if err != nil {
|
||||
return config, err
|
||||
changed := normalize(&config)
|
||||
applyEnvOverrides(&config)
|
||||
|
||||
if changed {
|
||||
if err := save(path, config); err != nil {
|
||||
// 只读挂载等场景下写回失败不应阻断启动
|
||||
log.Printf("警告: 写回配置文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// normalize 补齐缺失/非法的配置项,返回是否发生了修改
|
||||
func normalize(c *Config) bool {
|
||||
def := defaultConfig()
|
||||
changed := false
|
||||
|
||||
if c.ListenAddr == "" {
|
||||
c.ListenAddr, changed = def.ListenAddr, true
|
||||
}
|
||||
if c.TimeoutDurationHours <= 0 {
|
||||
c.TimeoutDurationHours, changed = def.TimeoutDurationHours, true
|
||||
}
|
||||
if c.CheckIntervalMinutes <= 0 {
|
||||
c.CheckIntervalMinutes, changed = def.CheckIntervalMinutes, true
|
||||
}
|
||||
if c.DatabasePath == "" {
|
||||
c.DatabasePath, changed = def.DatabasePath, true
|
||||
}
|
||||
if c.SessionMaxAgeHours <= 0 {
|
||||
c.SessionMaxAgeHours, changed = def.SessionMaxAgeHours, true
|
||||
}
|
||||
if c.SessionSecret == "" {
|
||||
// 随机生成一次并持久化,避免每次重启都让所有用户掉线
|
||||
c.SessionSecret, changed = randomHex(32), true
|
||||
}
|
||||
if c.AllowedOrigins == nil {
|
||||
c.AllowedOrigins, changed = def.AllowedOrigins, true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func applyEnvOverrides(c *Config) {
|
||||
if v := os.Getenv("GOODBABY_LISTEN_ADDR"); v != "" {
|
||||
c.ListenAddr = v
|
||||
}
|
||||
if v := os.Getenv("GOODBABY_DB_PATH"); v != "" {
|
||||
c.DatabasePath = v
|
||||
}
|
||||
if v := os.Getenv("GOODBABY_SESSION_SECRET"); v != "" {
|
||||
c.SessionSecret = v
|
||||
}
|
||||
if v := os.Getenv("GOODBABY_ENABLE_REGISTRY"); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
c.EnableRegistry = b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save(path string, c Config) error {
|
||||
content, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, content, 0o600)
|
||||
}
|
||||
|
||||
// Save 持久化配置并刷新缓存,供管理接口修改运行时配置使用
|
||||
func Save(c Config) error {
|
||||
normalize(&c)
|
||||
if err := save(configPath(), c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cacheMux.Lock()
|
||||
cached, loadErr = c, nil
|
||||
cacheMux.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomHex(n int) string {
|
||||
buf := make([]byte, n)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
panic(fmt.Sprintf("无法生成随机密钥: %v", err))
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user