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:
mei
2026-07-26 22:19:41 +08:00
co-authored by Claude Fable 5
parent 89e8b7fd66
commit 79774070fc
110 changed files with 8936 additions and 849 deletions
+67 -1
View File
@@ -3,14 +3,25 @@ package accountConfigChecker
import (
"encoding/json"
"fmt"
"sort"
"github.com/ssdomei232/goodBaby/internal/meta"
)
// AccountValidator 账号验证器接口
type AccountValidator interface {
// Validate 验证账号配置是否合法
Validate(account string) error
Validate(config string) error
// GetType 获取验证器支持的账号类型
GetType() string
// Meta 返回给 WebUI 渲染表单用的元数据
Meta() meta.AccountMeta
}
// AccountTester 可选接口:支持真实连通性测试的账号类型实现它
type AccountTester interface {
// Test 使用给定配置访问一次第三方服务,验证凭据是否可用
Test(config string) error
}
// ValidatorRegistry 验证器注册表
@@ -51,11 +62,66 @@ func (vr *ValidatorRegistry) Validate(accountType string, config string) error {
return validator.Validate(config)
}
// Test 对账号配置做一次真实的连通性测试
func (vr *ValidatorRegistry) Test(accountType string, config string) error {
validator, exists := vr.validators[accountType]
if !exists {
return fmt.Errorf("不支持的账号类型: %s", accountType)
}
if err := vr.Validate(accountType, config); err != nil {
return err
}
tester, ok := validator.(AccountTester)
if !ok {
return fmt.Errorf("账号类型 %s 暂不支持连通性测试", accountType)
}
return tester.Test(config)
}
// GetSupportedTypes 获取所有支持的账号类型
func (vr *ValidatorRegistry) GetSupportedTypes() []string {
types := make([]string, 0, len(vr.validators))
for t := range vr.validators {
types = append(types, t)
}
sort.Strings(types)
return types
}
// Metas 返回所有账号类型的元数据,按类型名排序
func (vr *ValidatorRegistry) Metas() []meta.AccountMeta {
metas := make([]meta.AccountMeta, 0, len(vr.validators))
for _, v := range vr.validators {
metas = append(metas, withTestable(v))
}
sort.Slice(metas, func(i, j int) bool { return metas[i].Type < metas[j].Type })
return metas
}
// MetaOf 返回指定账号类型的元数据
func (vr *ValidatorRegistry) MetaOf(accountType string) (meta.AccountMeta, bool) {
v, ok := vr.validators[accountType]
if !ok {
return meta.AccountMeta{}, false
}
return withTestable(v), true
}
// FieldsOf 返回指定账号类型的字段描述,类型不存在时返回 nil
func (vr *ValidatorRegistry) FieldsOf(accountType string) []meta.Field {
m, ok := vr.MetaOf(accountType)
if !ok {
return nil
}
return m.Fields
}
func withTestable(v AccountValidator) meta.AccountMeta {
m := v.Meta()
if _, ok := v.(AccountTester); ok {
m.Testable = true
}
return m
}
+17 -6
View File
@@ -1,20 +1,31 @@
package accountConfigChecker
import (
"sync"
"github.com/ssdomei232/goodBaby/drivers/bilibili"
"github.com/ssdomei232/goodBaby/drivers/email"
"github.com/ssdomei232/goodBaby/drivers/github"
"github.com/ssdomei232/goodBaby/drivers/onebot"
)
var (
once sync.Once
registry *ValidatorRegistry
)
// InitValidatorRegistry 返回全局唯一的账号验证器注册表
func InitValidatorRegistry() *ValidatorRegistry {
registry := NewValidatorRegistry()
once.Do(func() {
r := NewValidatorRegistry()
// 注册所有账号验证器
registry.Register(&bilibili.BilibiliAccountConfigValidator{})
registry.Register(&email.EmailAccountConfigValidator{})
registry.Register(&github.GitHubAccountConfigValidator{})
registry.Register(&onebot.OneBotAccountConfigValidator{})
// 注册所有账号验证器
r.Register(&bilibili.BilibiliAccountConfigValidator{})
r.Register(&email.EmailAccountConfigValidator{})
r.Register(&github.GitHubAccountConfigValidator{})
r.Register(&onebot.OneBotAccountConfigValidator{})
registry = r
})
return registry
}
+70
View File
@@ -0,0 +1,70 @@
package meta
import "encoding/json"
// MaskPlaceholder 是掩码后的占位值。
// 提交编辑时如果某个 secret 字段仍是这个值,服务端会保留原值。
const MaskPlaceholder = "********"
// Mask 把配置 JSON 中的敏感字段替换为占位符
func Mask(configJSON string, fields []Field) string {
secrets := SecretKeys(fields)
if len(secrets) == 0 || configJSON == "" {
return configJSON
}
var raw map[string]any
if err := json.Unmarshal([]byte(configJSON), &raw); err != nil {
return configJSON
}
for _, key := range secrets {
if v, ok := raw[key]; ok {
if s, isStr := v.(string); !isStr || s == "" {
continue
}
raw[key] = MaskPlaceholder
}
}
masked, err := json.Marshal(raw)
if err != nil {
return configJSON
}
return string(masked)
}
// Unmask 用旧配置补回被掩码的字段,返回可以落库的配置 JSON
func Unmask(newConfigJSON, oldConfigJSON string, fields []Field) string {
secrets := SecretKeys(fields)
if len(secrets) == 0 || newConfigJSON == "" || oldConfigJSON == "" {
return newConfigJSON
}
var newRaw, oldRaw map[string]any
if err := json.Unmarshal([]byte(newConfigJSON), &newRaw); err != nil {
return newConfigJSON
}
if err := json.Unmarshal([]byte(oldConfigJSON), &oldRaw); err != nil {
return newConfigJSON
}
restored := false
for _, key := range secrets {
if s, ok := newRaw[key].(string); ok && s == MaskPlaceholder {
if old, exists := oldRaw[key]; exists {
newRaw[key] = old
restored = true
}
}
}
if !restored {
return newConfigJSON
}
merged, err := json.Marshal(newRaw)
if err != nil {
return newConfigJSON
}
return string(merged)
}
+63
View File
@@ -0,0 +1,63 @@
// Package meta 定义驱动向前端暴露的元数据。
//
// WebUI 依靠这些描述动态渲染账号 / 规则的配置表单,
// 新增一个驱动时只要提供 Meta(),前端无需改动。
package meta
// FieldType 决定前端使用哪种输入控件
type FieldType string
const (
FieldString FieldType = "string" // 单行文本
FieldPassword FieldType = "password" // 密码框
FieldTextarea FieldType = "textarea" // 多行文本
FieldNumber FieldType = "number" // 数字
FieldStringList FieldType = "string-list" // 字符串数组
FieldNumberList FieldType = "number-list" // 数字数组
FieldBool FieldType = "bool" // 开关
)
// Field 描述配置 JSON 中的一个字段
type Field struct {
Key string `json:"key"`
Label string `json:"label"`
Type FieldType `json:"type"`
Required bool `json:"required"`
Placeholder string `json:"placeholder,omitempty"`
Help string `json:"help,omitempty"`
// Secret 为 true 的字段在读取接口中会被掩码
Secret bool `json:"secret,omitempty"`
Default any `json:"default,omitempty"`
}
// AccountMeta 账号类型的元数据
type AccountMeta struct {
Type string `json:"type"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Docs string `json:"docs,omitempty"`
Testable bool `json:"testable"`
Fields []Field `json:"fields"`
}
// RuleMeta 规则类型的元数据
type RuleMeta struct {
Type string `json:"type"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Docs string `json:"docs,omitempty"`
// AccountType 该规则需要的账号类型,为空表示不需要关联账号
AccountType string `json:"account_type"`
Fields []Field `json:"fields"`
}
// SecretKeys 返回需要掩码的字段名
func SecretKeys(fields []Field) []string {
keys := make([]string, 0, len(fields))
for _, f := range fields {
if f.Secret {
keys = append(keys, f.Key)
}
}
return keys
}
+46
View File
@@ -0,0 +1,46 @@
// Package retry 封装规则执行使用的指数退避重试
package retry
import (
"context"
"time"
"github.com/cenkalti/backoff/v5"
"github.com/ssdomei232/goodBaby/configs"
)
// 手动测试时使用的超时,避免用户在 WebUI 上等待数小时
const TestTimeout = 30 * time.Second
// Do 在 ctx 允许的时间内按指数退避执行 op
//
// 超时后返回最后一次真实的业务错误,而不是干巴巴的 context deadline exceeded。
func Do(ctx context.Context, op func() error) error {
var lastErr error
_, err := backoff.Retry(ctx, func() (struct{}, error) {
e := op()
if e != nil {
lastErr = e
}
return struct{}{}, e
}, backoff.WithBackOff(backoff.NewExponentialBackOff()))
if err != nil && lastErr != nil {
return lastErr
}
return err
}
// ExecutionTimeout 返回配置中的规则执行超时时间
func ExecutionTimeout() time.Duration {
config, err := configs.GetConfig()
if err != nil || config.TimeoutDurationHours <= 0 {
return 6 * time.Hour
}
return time.Duration(config.TimeoutDurationHours) * time.Hour
}
// ExecutionContext 返回一个带规则执行超时的 context
func ExecutionContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), ExecutionTimeout())
}
+25
View File
@@ -3,6 +3,9 @@ package ruleConfigChecker
import (
"encoding/json"
"fmt"
"sort"
"github.com/ssdomei232/goodBaby/internal/meta"
)
// RuleValidator 规则验证器接口
@@ -11,6 +14,8 @@ type RuleValidator interface {
Validate(configJSON string) error
// GetType 获取验证器支持的规则类型
GetType() string
// Meta 返回给 WebUI 渲染表单用的元数据
Meta() meta.RuleMeta
}
// ValidatorRegistry 验证器注册表
@@ -57,5 +62,25 @@ func (vr *ValidatorRegistry) GetSupportedTypes() []string {
for t := range vr.validators {
types = append(types, t)
}
sort.Strings(types)
return types
}
// Metas 返回所有规则类型的元数据,按类型名排序
func (vr *ValidatorRegistry) Metas() []meta.RuleMeta {
metas := make([]meta.RuleMeta, 0, len(vr.validators))
for _, v := range vr.validators {
metas = append(metas, v.Meta())
}
sort.Slice(metas, func(i, j int) bool { return metas[i].Type < metas[j].Type })
return metas
}
// MetaOf 返回指定规则类型的元数据
func (vr *ValidatorRegistry) MetaOf(ruleType string) (meta.RuleMeta, bool) {
v, ok := vr.validators[ruleType]
if !ok {
return meta.RuleMeta{}, false
}
return v.Meta(), true
}
+18 -8
View File
@@ -1,6 +1,8 @@
package ruleConfigChecker
import (
"sync"
"github.com/ssdomei232/goodBaby/drivers/bilibili"
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
"github.com/ssdomei232/goodBaby/drivers/email"
@@ -8,16 +10,24 @@ import (
"github.com/ssdomei232/goodBaby/drivers/onebot"
)
// InitValidatorRegistry 初始化验证器注册表并注册所有验证器
var (
once sync.Once
registry *ValidatorRegistry
)
// InitValidatorRegistry 返回全局唯一的规则验证器注册表
func InitValidatorRegistry() *ValidatorRegistry {
registry := NewValidatorRegistry()
once.Do(func() {
r := NewValidatorRegistry()
// 注册所有规则验证器
registry.Register(&bilibili.BilibiliDynamicRuleValidator{})
registry.Register(&email.EmailRuleValidator{})
registry.Register(&onebot.OneBotRuleValidator{})
registry.Register(&dingtalk.DingTalkRuleValidator{})
registry.Register(&github.GithubMakeRepositoryPublicRuleValidator{})
// 注册所有规则验证器
r.Register(&bilibili.BilibiliDynamicRuleValidator{})
r.Register(&email.EmailRuleValidator{})
r.Register(&onebot.OneBotRuleValidator{})
r.Register(&dingtalk.DingTalkRuleValidator{})
r.Register(&github.GithubMakeRepositoryPublicRuleValidator{})
registry = r
})
return registry
}