feat(gateway): 优化gateway相关功能
Quality check / Web UI (push) Successful in 9m19s

This commit is contained in:
mei
2026-09-12 15:32:27 +08:00
parent 0d89b7fe8f
commit 9b656cebee
84 changed files with 2769 additions and 825 deletions
+3 -12
View File
@@ -1,7 +1,6 @@
package alidns
import (
"encoding/json"
"fmt"
"github.com/alibabacloud-go/alidns-20150109/v5/client"
@@ -13,7 +12,7 @@ type AliDNSAccountConfigValidator struct{}
func (v *AliDNSAccountConfigValidator) GetType() string { return AccountType }
func (v *AliDNSAccountConfigValidator) Validate(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -23,21 +22,13 @@ func (v *AliDNSAccountConfigValidator) Validate(config string) error {
return nil
}
func parseAccount(config string) (*AliDNSAccount, error) {
var cfg AliDNSAccount
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析阿里云账号配置失败: %v", err)
}
return &cfg, nil
}
func (v *AliDNSAccountConfigValidator) Test(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
aliDNSClient, err := getAliDNSClient(cfg.AK, cfg.SK)
aliDNSClient, err := newClient(cfg.AK, cfg.SK)
if err != nil {
return err
}
+6 -11
View File
@@ -4,25 +4,20 @@ import (
"context"
"fmt"
"github.com/alibabacloud-go/alidns-20150109/v5/client"
alidns "github.com/alibabacloud-go/alidns-20150109/v5/client"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// deleteAliDNSRecord deletes a DNS record from Alibaba Cloud DNS based on the provided rule.
func deleteAliDNSRecord(ctx context.Context, rule *model.Rule) error {
aliDNSClient, err := getAliDNSClientFromRule(rule)
if err != nil {
return err
}
deleteRecordConfig, err := getDeleteRecordConfig(rule)
// DeleteRecord 用给定账号删除一条阿里云 DNS 解析记录
func DeleteRecord(ctx context.Context, account *AliDNSAccount, config *DeleteRecordConfig) error {
client, err := newClient(account.AK, account.SK)
if err != nil {
return err
}
if err := retry.Do(ctx, func() error {
_, err := aliDNSClient.DeleteDomainRecord(&client.DeleteDomainRecordRequest{
RecordId: &deleteRecordConfig.RecordID,
_, err := client.DeleteDomainRecord(&alidns.DeleteDomainRecordRequest{
RecordId: &config.RecordID,
})
return err
}); err != nil {
+3 -4
View File
@@ -1,7 +1,6 @@
package alidns
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -15,9 +14,9 @@ func (v *AliDNSDeleteRecordRuleValidator) GetType() string {
}
func (v *AliDNSDeleteRecordRuleValidator) Validate(configJSON string) error {
var config DeleteRecordConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析阿里云删除记录规则配置失败: %v", err)
config, err := ParseDeleteRecordConfig(configJSON)
if err != nil {
return err
}
if config.RecordID == "" {
+11 -4
View File
@@ -2,7 +2,6 @@ package alidns
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
@@ -14,8 +13,16 @@ func (e *DeleteAliDNSRecordExecutor) GetType() string {
return RuleTypeDeleteRecord
}
func (e *DeleteAliDNSRecordExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行删除阿里云DNS记录规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *DeleteAliDNSRecordExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return deleteAliDNSRecord(ctx, rule)
config, err := ParseDeleteRecordConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return DeleteRecord(ctx, account, config)
}
+27 -31
View File
@@ -2,54 +2,50 @@ package alidns
import (
"encoding/json"
"fmt"
alidns "github.com/alibabacloud-go/alidns-20150109/v5/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
"github.com/aliyun/credentials-go/credentials"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// getAliDNSClient initializes and returns an Alibaba Cloud DNS client based on the provided rule's account configuration. It retrieves the account configuration from the database, sets up the necessary credentials, and creates a new client instance for interacting with Alibaba Cloud DNS services.
func getAliDNSClientFromRule(rule *model.Rule) (client *alidns.Client, err error) {
var accountConfig AliDNSAccount
// get config
if err := db.LoadAccountConfig(rule.AccountID, &accountConfig); err != nil {
return nil, err
// ParseAccountConfig 解析阿里云账号配置
func ParseAccountConfig(config string) (*AliDNSAccount, error) {
if config == "" {
return nil, fmt.Errorf("解析阿里云账号配置失败: 规则没有关联账号")
}
return getAliDNSClient(accountConfig.AK, accountConfig.SK)
var account AliDNSAccount
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析阿里云账号配置失败: %v", err)
}
return &account, nil
}
func getAliDNSClient(ak string, sk string) (client *alidns.Client, err error) {
// init aliyun account config
// ParseDeleteRecordConfig 解析删除解析记录规则配置
func ParseDeleteRecordConfig(configJSON string) (*DeleteRecordConfig, error) {
var config DeleteRecordConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析阿里云删除记录规则配置失败: %v", err)
}
return &config, nil
}
// newClient 用 AccessKey 创建阿里云 DNS 客户端
func newClient(ak string, sk string) (*alidns.Client, error) {
credentialsConfig := new(credentials.Config).
SetType("access_key").
SetAccessKeyId(ak).
SetAccessKeySecret(sk)
akCredential, err := credentials.NewCredential(credentialsConfig)
if err != nil {
return nil, err
}
config := &openapi.Config{}
config.Credential = akCredential
config.Endpoint = tea.String("alidns.aliyuncs.com")
// create aliyun account client
client, _ = alidns.NewClient(config)
return client, nil
}
// getDeleteRecordConfig retrieves the configuration for deleting DNS records from the provided rule. It unmarshals the rule's configuration JSON into a DeleteRecordConfig structure and returns it.
func getDeleteRecordConfig(rule *model.Rule) (*DeleteRecordConfig, error) {
var deleteRecordConfig DeleteRecordConfig
if err := json.Unmarshal([]byte(rule.ConfigJson), &deleteRecordConfig); err != nil {
return nil, err
return nil, fmt.Errorf("创建阿里云凭据失败: %w", err)
}
return &deleteRecordConfig, nil
return alidns.NewClient(&openapi.Config{
Credential: akCredential,
Endpoint: tea.String("alidns.aliyuncs.com"),
})
}
+2 -11
View File
@@ -1,7 +1,6 @@
package bilibili
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -15,7 +14,7 @@ func (v *BilibiliAccountConfigValidator) GetType() string {
}
func (v *BilibiliAccountConfigValidator) Validate(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -29,7 +28,7 @@ func (v *BilibiliAccountConfigValidator) Validate(config string) error {
// Test 用 cookie 拉一次账号信息,验证 cookie 是否还有效
func (v *BilibiliAccountConfigValidator) Test(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -64,11 +63,3 @@ func (v *BilibiliAccountConfigValidator) Meta() meta.AccountMeta {
},
}
}
func parseAccount(config string) (*BiliAccount, error) {
var cfg BiliAccount
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析B站账号配置失败: %v", err)
}
return &cfg, nil
}
+6 -16
View File
@@ -6,32 +6,22 @@ import (
"github.com/CuteReimu/bilibili/v2"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// SendBiliDynamicMsg 在 Bilibili 动态发送消息
// SendDynamic 用给定账号发送一条 B 站动态
//
// 暂时没有处理 429 和 403 的区别对待
func SendBiliDynamicMsg(ctx context.Context, rule *model.Rule) error {
biliClient, err := getBiliClient(rule)
if err != nil {
return fmt.Errorf("获取B站客户端失败: %w", err)
}
biliDynamicConfig, err := getBiliDynamicConfig(rule)
if err != nil {
return fmt.Errorf("获取B站动态配置失败: %w", err)
}
dynamicParams := bilibili.CreateDynamicParam{
func SendDynamic(ctx context.Context, account *BiliAccount, config *BiliDynamicConfig) error {
client := newClient(account.RawCookies)
params := bilibili.CreateDynamicParam{
DynamicId: 0,
Type: 4,
Rid: 0,
Content: biliDynamicConfig.Msg,
Content: config.Msg,
}
if err := retry.Do(ctx, func() error {
_, err := biliClient.CreateDynamic(dynamicParams)
_, err := client.CreateDynamic(params)
return err
}); err != nil {
return fmt.Errorf("发送B站动态失败: %w", err)
+38 -44
View File
@@ -2,54 +2,34 @@ package bilibili
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/CuteReimu/bilibili/v2"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// 私信消息类型:1 为文字
// privateMsgTypeText 私信消息类型:1 为文字
const privateMsgTypeText = 1
// SendBiliPrivateMessage 向配置中的每个 UID 发送一条 B 站私信
func SendBiliPrivateMessage(ctx context.Context, rule *model.Rule) error {
client, err := getBiliClient(rule)
if err != nil {
return fmt.Errorf("获取B站客户端失败: %w", err)
}
// SendPrivateMessage 用给定账号向配置中的每个 UID 发送一条 B 站私信
//
// 每个接收者单独重试、互不阻塞,最后汇总失败信息交给上层记录。
func SendPrivateMessage(ctx context.Context, account *BiliAccount, config *BiliPrivateMessageConfig) error {
client := newClient(account.RawCookies)
config, err := getPrivateMessageConfig(rule)
// 发私信需要带上自己的 UID,从账号信息里取
selfUID, err := fetchSelfUID(ctx, client)
if err != nil {
return err
}
// 发私信需要带上自己的 UID,从账号信息里取
account, err := client.GetAccountInformation()
if err != nil {
return fmt.Errorf("获取B站账号信息失败(cookie 可能已失效): %w", err)
}
if account == nil || account.Mid == 0 {
return fmt.Errorf("B站 cookie 无效或已过期")
}
var fails []string
for _, uid := range config.ReceiverUids {
err := retry.Do(ctx, func() error {
_, err := client.SendPrivateMessage(bilibili.SendPrivateMessageParam{
SenderUid: account.Mid,
ReceiverId: int(uid),
ReceiverType: 1,
MsgType: privateMsgTypeText,
Timestamp: int(time.Now().Unix()),
Content: buildTextContent(config.Msg),
})
return err
})
if err != nil {
if err := retry.Do(ctx, func() error {
return sendPrivateMessage(client, selfUID, uid, config.Msg)
}); err != nil {
fails = append(fails, fmt.Sprintf("UID %d: %v", uid, err))
}
}
@@ -61,20 +41,34 @@ func SendBiliPrivateMessage(ctx context.Context, rule *model.Rule) error {
return nil
}
// buildTextContent 文字私信的 content 是一个 JSON 字符串 {"content":"..."}
func buildTextContent(msg string) string {
payload, err := json.Marshal(map[string]string{"content": msg})
if err != nil {
// msg 是普通字符串,序列化不会失败;兜底也返回合法 JSON
return `{"content":""}`
// fetchSelfUID 取当前账号的 UID,私信接口需要它
func fetchSelfUID(ctx context.Context, client *bilibili.Client) (int, error) {
var selfUID int
if err := retry.Do(ctx, func() error {
info, err := client.GetAccountInformation()
if err != nil {
return err
}
if info == nil || info.Mid == 0 {
return retry.Permanent(fmt.Errorf("B站 cookie 无效或已过期"))
}
selfUID = info.Mid
return nil
}); err != nil {
return 0, fmt.Errorf("获取B站账号信息失败: %w", err)
}
return string(payload)
return selfUID, nil
}
func getPrivateMessageConfig(rule *model.Rule) (*BiliPrivateMessageConfig, error) {
var config BiliPrivateMessageConfig
if err := json.Unmarshal([]byte(rule.ConfigJson), &config); err != nil {
return nil, fmt.Errorf("解析B站私信规则配置失败: %w", err)
}
return &config, nil
// sendPrivateMessage 发送一条私信
func sendPrivateMessage(client *bilibili.Client, senderUID int, receiverUID int64, msg string) error {
_, err := client.SendPrivateMessage(bilibili.SendPrivateMessageParam{
SenderUid: senderUID,
ReceiverId: int(receiverUID),
ReceiverType: 1,
MsgType: privateMsgTypeText,
Timestamp: int(time.Now().Unix()),
Content: buildTextContent(msg),
})
return err
}
+13 -13
View File
@@ -1,7 +1,6 @@
package bilibili
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -15,9 +14,9 @@ func (v *BilibiliDynamicRuleValidator) GetType() string {
}
func (v *BilibiliDynamicRuleValidator) Validate(configJSON string) error {
var config BiliDynamicConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析B站动态规则配置失败: %v", err)
config, err := ParseDynamicConfig(configJSON)
if err != nil {
return err
}
if config.Msg == "" {
@@ -39,9 +38,9 @@ func (v *BilibiliPrivateMessageRuleValidator) GetType() string {
}
func (v *BilibiliPrivateMessageRuleValidator) Validate(configJSON string) error {
var config BiliPrivateMessageConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析B站私信规则配置失败: %v", err)
config, err := ParsePrivateMessageConfig(configJSON)
if err != nil {
return err
}
if config.Msg == "" {
@@ -77,7 +76,7 @@ func (v *BilibiliPrivateMessageRuleValidator) Meta() meta.RuleMeta {
Placeholder: "2",
Help: "对方空间地址 space.bilibili.com/ 后面的那串数字",
},
{Key: "msg", Label: "私信内容", Type: meta.FieldTextarea, Required: true},
{Key: "msg", Label: "私信内容", Type: meta.FieldTextarea, Required: true, GatewayMessage: true},
},
}
}
@@ -91,11 +90,12 @@ func (v *BilibiliDynamicRuleValidator) Meta() meta.RuleMeta {
AccountType: AccountType,
Fields: []meta.Field{
{
Key: "msg",
Label: "动态内容",
Type: meta.FieldTextarea,
Required: true,
Placeholder: "要发送的动态正文",
Key: "msg",
Label: "动态内容",
Type: meta.FieldTextarea,
Required: true,
GatewayMessage: true,
Placeholder: "要发送的动态正文",
},
},
}
+22 -7
View File
@@ -2,7 +2,6 @@ package bilibili
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
@@ -14,10 +13,18 @@ func (e *BilibiliDynamicExecutor) GetType() string {
return RuleTypeDynamic
}
func (e *BilibiliDynamicExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行B站动态规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *BilibiliDynamicExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return SendBiliDynamicMsg(ctx, rule)
config, err := ParseDynamicConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return SendDynamic(ctx, account, config)
}
// BilibiliPrivateMessageExecutor B站私信执行器
@@ -27,8 +34,16 @@ func (e *BilibiliPrivateMessageExecutor) GetType() string {
return RuleTypePrivateMessage
}
func (e *BilibiliPrivateMessageExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行B站私信规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *BilibiliPrivateMessageExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return SendBiliPrivateMessage(ctx, rule)
config, err := ParsePrivateMessageConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return SendPrivateMessage(ctx, account, config)
}
+40 -19
View File
@@ -2,34 +2,55 @@ package bilibili
import (
"encoding/json"
"fmt"
"github.com/CuteReimu/bilibili/v2"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// ParseAccountConfig 解析 B 站账号配置
func ParseAccountConfig(config string) (*BiliAccount, error) {
if config == "" {
return nil, fmt.Errorf("解析B站账号配置失败: 规则没有关联账号")
}
var account BiliAccount
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析B站账号配置失败: %v", err)
}
return &account, nil
}
// ParseDynamicConfig 解析 B 站动态规则配置
func ParseDynamicConfig(configJSON string) (*BiliDynamicConfig, error) {
var config BiliDynamicConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析B站动态规则配置失败: %v", err)
}
return &config, nil
}
// ParsePrivateMessageConfig 解析 B 站私信规则配置
func ParsePrivateMessageConfig(configJSON string) (*BiliPrivateMessageConfig, error) {
var config BiliPrivateMessageConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析B站私信规则配置失败: %v", err)
}
return &config, nil
}
// newClient 按账号配置创建 B 站客户端
func newClient(rawCookies string) *bilibili.Client {
client := bilibili.New()
client.SetRawCookies(rawCookies)
return client
}
// 获取 Bilibili Client
func getBiliClient(rule *model.Rule) (*bilibili.Client, error) {
var accountConfig BiliAccount
if err := db.LoadAccountConfig(rule.AccountID, &accountConfig); err != nil {
return nil, err
// buildTextContent 文字私信的 content 是一个 JSON 字符串 {"content":"..."}
func buildTextContent(msg string) string {
payload, err := json.Marshal(map[string]string{"content": msg})
if err != nil {
// msg 是普通字符串,序列化不会失败;兜底也返回合法 JSON
return `{"content":""}`
}
return newClient(accountConfig.RawCookies), nil
}
// 获取 Bilibili Dynamic Config
func getBiliDynamicConfig(rule *model.Rule) (*BiliDynamicConfig, error) {
var biliDynamicConfig BiliDynamicConfig
if err := json.Unmarshal([]byte(rule.ConfigJson), &biliDynamicConfig); err != nil {
return nil, err
}
return &biliDynamicConfig, nil
return string(payload)
}
+30 -11
View File
@@ -2,32 +2,51 @@
package dingtalk
import (
"context"
"fmt"
"github.com/CatchZeng/dingtalk/pkg/dingtalk"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// ErrNoUserConfig 表示用户没有配置钉钉提醒
var ErrNoUserConfig = fmt.Errorf("用户未配置钉钉机器人")
// SendDingTalkMsg 使用用户配置的钉钉机器人发送提醒
//
// 用户没有配置钉钉机器人时返回 ErrNoUserConfig
func SendDingTalkMsg(user *model.User, title string, msg string) error {
config := getDingTalkConfigFromUser(user)
if config == nil {
if user.DingTalkConfig == nil {
return ErrNoUserConfig
}
return send(config, title, msg)
}
// ErrNoUserConfig 表示用户没有配置钉钉提醒
var ErrNoUserConfig = fmt.Errorf("用户未配置钉钉机器人")
config, err := ParseAccountConfig(*user.DingTalkConfig)
if err != nil {
return err
}
func send(config *DingTalkConfig, title, msg string) error {
client := dingtalk.NewClient(config.AccessToken, config.Secret)
message := dingtalk.NewMarkdownMessage().SetMarkdown(title, msg)
if _, _, err := client.Send(message); err != nil {
if err := send(config, title, msg); err != nil {
return fmt.Errorf("发送钉钉消息失败: %w", err)
}
return nil
}
// SendMessage 用规则里配置的机器人发送消息
func SendMessage(ctx context.Context, config *DingTalkRuleConfig) error {
if err := retry.Do(ctx, func() error {
return send(&config.DingTalkConfig, config.Title, config.Msg)
}); err != nil {
return fmt.Errorf("发送钉钉消息失败: %w", err)
}
return nil
}
// send 调用一次机器人接口
func send(config *DingTalkConfig, title, msg string) error {
client := dingtalk.NewClient(config.AccessToken, config.Secret)
message := dingtalk.NewMarkdownMessage().SetMarkdown(title, msg)
_, _, err := client.Send(message)
return err
}
+2 -12
View File
@@ -1,7 +1,6 @@
package dingtalk
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -42,17 +41,8 @@ func (v *DingTalkRuleValidator) Meta() meta.RuleMeta {
Fields: []meta.Field{
{Key: "access_token", Label: "Access Token", Type: meta.FieldPassword, Required: true, Secret: true, Help: "机器人 Webhook 中 access_token 参数的值"},
{Key: "secret", Label: "加签 Secret", Type: meta.FieldPassword, Secret: true, Help: "机器人安全设置选择“加签”时填写"},
{Key: "title", Label: "消息标题", Type: meta.FieldString, Required: true},
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, Help: "支持 Markdown"},
{Key: "title", Label: "消息标题", Type: meta.FieldString, Required: true, GatewayMessage: true},
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, GatewayMessage: true, Help: "支持 Markdown"},
},
}
}
// ParseRuleConfig 解析钉钉规则配置
func ParseRuleConfig(configJSON string) (*DingTalkRuleConfig, error) {
var config DingTalkRuleConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析钉钉规则配置失败: %v", err)
}
return &config, nil
}
+4 -9
View File
@@ -2,9 +2,7 @@ package dingtalk
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
@@ -15,15 +13,12 @@ func (e *DingTalkExecutor) GetType() string {
return RuleType
}
func (e *DingTalkExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行钉钉规则: %s (ID: %d)", rule.Name, rule.ID)
config, err := ParseRuleConfig(rule.ConfigJson)
func (e *DingTalkExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
// 钉钉机器人凭据直接写在规则配置里,不需要关联账号
config, err := ParseRuleConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return retry.Do(ctx, func() error {
return send(&config.DingTalkConfig, config.Title, config.Msg)
})
return SendMessage(ctx, config)
}
+18 -10
View File
@@ -2,19 +2,27 @@ package dingtalk
import (
"encoding/json"
"github.com/ssdomei232/goodBaby/model"
"fmt"
)
func getDingTalkConfigFromUser(user *model.User) *DingTalkConfig {
if user.DingTalkConfig == nil {
return nil
// ParseAccountConfig 解析钉钉机器人凭据配置
func ParseAccountConfig(config string) (*DingTalkConfig, error) {
if config == "" {
return nil, fmt.Errorf("解析钉钉机器人配置失败: 配置为空")
}
var config DingTalkConfig
err := json.Unmarshal([]byte(*user.DingTalkConfig), &config)
if err != nil {
return nil
var account DingTalkConfig
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析钉钉机器人配置失败: %v", err)
}
return &config
return &account, nil
}
// ParseRuleConfig 解析钉钉规则配置
func ParseRuleConfig(configJSON string) (*DingTalkRuleConfig, error) {
var config DingTalkRuleConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析钉钉规则配置失败: %v", err)
}
return &config, nil
}
+2 -11
View File
@@ -2,7 +2,6 @@ package email
import (
"context"
"encoding/json"
"fmt"
"net/mail"
@@ -18,7 +17,7 @@ func (v *EmailAccountConfigValidator) GetType() string {
}
func (v *EmailAccountConfigValidator) Validate(config string) error {
cfg, err := parseAccountConfig(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -56,7 +55,7 @@ func (v *EmailAccountConfigValidator) Validate(config string) error {
// Test 连接 SMTP 服务器并完成认证;填写了测试收件地址时会真的发一封测试邮件
func (v *EmailAccountConfigValidator) Test(config string) error {
cfg, err := parseAccountConfig(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -103,11 +102,3 @@ func (v *EmailAccountConfigValidator) Meta() meta.AccountMeta {
},
}
}
func parseAccountConfig(config string) (*EmailAccountConfig, error) {
var cfg EmailAccountConfig
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析邮箱账号配置失败: %v", err)
}
return &cfg, nil
}
+12 -17
View File
@@ -3,38 +3,31 @@ package email
import (
"context"
"fmt"
"sort"
"strings"
"sync"
"github.com/ssdomei232/goodBaby/model"
"github.com/ssdomei232/goodBaby/internal/retry"
)
// SendMail 向规则配置里的所有地址发送邮件。
// SendMail 用给定 SMTP 账号向规则配置里的所有地址发送邮件。
//
// 每个地址单独重试,互不阻塞;全部失败/部分失败都会汇总成错误返回,
// 每个地址单独重试、互不阻塞;全部失败/部分失败都会汇总成错误返回,
// 由 runner 记录到执行日志中。
func SendMail(ctx context.Context, rule *model.Rule) error {
emailRule, err := GetEmailRuleFromRule(rule)
if err != nil {
return fmt.Errorf("获取邮件规则配置失败: %w", err)
}
accountConfig, err := GetEmailAccountFromRule(rule)
if err != nil {
return fmt.Errorf("获取邮件账户配置失败: %w", err)
}
func SendMail(ctx context.Context, account *EmailAccountConfig, config *EmailRule) error {
var (
wg sync.WaitGroup
mu sync.Mutex
fails []string
)
for _, destination := range emailRule.Destinations {
for _, destination := range config.Destinations {
wg.Add(1)
go func(address string) {
defer wg.Done()
if err := sendMailMsgWithRetry(ctx, accountConfig, emailRule, address); err != nil {
if err := retry.Do(ctx, func() error {
return sendMail(ctx, account, config, address)
}); err != nil {
mu.Lock()
fails = append(fails, fmt.Sprintf("%s: %v", address, err))
mu.Unlock()
@@ -44,8 +37,10 @@ func SendMail(ctx context.Context, rule *model.Rule) error {
wg.Wait()
if len(fails) > 0 {
// 并发发送的顺序不确定,排序后错误信息才是稳定的
sort.Strings(fails)
return fmt.Errorf("%d/%d 封邮件发送失败: %s",
len(fails), len(emailRule.Destinations), strings.Join(fails, "; "))
len(fails), len(config.Destinations), strings.Join(fails, "; "))
}
return nil
}
+5 -6
View File
@@ -1,7 +1,6 @@
package email
import (
"encoding/json"
"fmt"
"net/mail"
@@ -16,9 +15,9 @@ func (v *EmailRuleValidator) GetType() string {
}
func (v *EmailRuleValidator) Validate(configJSON string) error {
var config EmailRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析邮件规则配置失败: %v", err)
config, err := ParseRuleConfig(configJSON)
if err != nil {
return err
}
if config.Title == "" {
@@ -51,8 +50,8 @@ func (v *EmailRuleValidator) Meta() meta.RuleMeta {
Docs: "docs/email-config.md",
AccountType: AccountType,
Fields: []meta.Field{
{Key: "title", Label: "邮件标题", Type: meta.FieldString, Required: true},
{Key: "msg", Label: "邮件正文", Type: meta.FieldTextarea, Required: true},
{Key: "title", Label: "邮件标题", Type: meta.FieldString, Required: true, GatewayMessage: true},
{Key: "msg", Label: "邮件正文", Type: meta.FieldTextarea, Required: true, GatewayMessage: true},
{Key: "destinations", Label: "收件人", Type: meta.FieldStringList, Required: true, Placeholder: "someone@example.com"},
},
}
+11 -4
View File
@@ -2,7 +2,6 @@ package email
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
@@ -14,8 +13,16 @@ func (e *EmailExecutor) GetType() string {
return RuleType
}
func (e *EmailExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行邮件规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *EmailExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return SendMail(ctx, rule)
config, err := ParseRuleConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return SendMail(ctx, account, config)
}
+20 -23
View File
@@ -5,28 +5,29 @@ import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
"github.com/wneessen/go-mail"
)
// GetEmailAccountFromRule 从 Rule 中获取 EmailAccount 配置
func GetEmailAccountFromRule(rule *model.Rule) (*EmailAccountConfig, error) {
var emailAccountConfig EmailAccountConfig
if err := db.LoadAccountConfig(rule.AccountID, &emailAccountConfig); err != nil {
return nil, err
// ParseAccountConfig 解析邮箱账号配置
func ParseAccountConfig(config string) (*EmailAccountConfig, error) {
if config == "" {
return nil, fmt.Errorf("解析邮箱账号配置失败: 规则没有关联账号")
}
return &emailAccountConfig, nil
var account EmailAccountConfig
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析邮箱账号配置失败: %v", err)
}
return &account, nil
}
// GetEmailRuleFromRule 从 Rule 中获取 EmailRule 配置
func GetEmailRuleFromRule(rule *model.Rule) (*EmailRule, error) {
var emailRule EmailRule
if err := json.Unmarshal([]byte(rule.ConfigJson), &emailRule); err != nil {
return nil, err
// ParseRuleConfig 解析邮件规则配置
func ParseRuleConfig(configJSON string) (*EmailRule, error) {
var config EmailRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析邮件规则配置失败: %v", err)
}
return &emailRule, nil
return &config, nil
}
// newSMTPClient 按账号配置里的加密方式创建 SMTP 客户端
@@ -54,6 +55,7 @@ func newSMTPClient(cfg *EmailAccountConfig) (*mail.Client, error) {
return client, nil
}
// buildMessage 组装一封邮件
func buildMessage(cfg *EmailAccountConfig, address, title, body string) (*mail.Msg, error) {
message := mail.NewMsg()
if err := message.From(cfg.FromOrDefault()); err != nil {
@@ -67,19 +69,14 @@ func buildMessage(cfg *EmailAccountConfig, address, title, body string) (*mail.M
return message, nil
}
func sendMailMsgWithRetry(ctx context.Context, cfg *EmailAccountConfig, rule *EmailRule, address string) error {
return retry.Do(ctx, func() error {
return sendMailMsg(ctx, cfg, rule, address)
})
}
func sendMailMsg(ctx context.Context, cfg *EmailAccountConfig, rule *EmailRule, address string) error {
// sendMail 给一个地址发送一封邮件
func sendMail(ctx context.Context, cfg *EmailAccountConfig, config *EmailRule, address string) error {
client, err := newSMTPClient(cfg)
if err != nil {
return err
}
message, err := buildMessage(cfg, address, rule.Title, rule.Msg)
message, err := buildMessage(cfg, address, config.Title, config.Msg)
if err != nil {
return err
}
+17 -10
View File
@@ -1,26 +1,33 @@
package fwalert
import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/ssdomei232/goodBaby/internal/retry"
)
// send 发送消息
// Send 发送一条饭碗警告
func Send(ctx context.Context, config *FwAlertRuleConfig) error {
if err := retry.Do(ctx, func() error {
return send(config)
}); err != nil {
return fmt.Errorf("发送饭碗警告失败: %w", err)
}
return nil
}
// send 调用一次 Webhook
func send(config *FwAlertRuleConfig) error {
restyClient := resty.New()
req := restyClient.R()
req.SetBody(FwAlertRequest{
Message: config.Msg,
})
resp, err := req.Execute(resty.MethodPost, config.WebhookURL)
resp, err := resty.New().R().
SetBody(FwAlertRequest{Message: config.Msg}).
Execute(resty.MethodPost, config.WebhookURL)
if err != nil {
return err
}
if resp.StatusCode() != 200 {
return fmt.Errorf("发送饭碗警告失败: %s", resp.Status())
return fmt.Errorf("Webhook 返回状态码 %s", resp.Status())
}
return nil
}
+2 -1
View File
@@ -1,6 +1,7 @@
package fwalert
const RuleTypeFwalert = "fwalert"
// RuleType 饭碗警告规则类型标识
const RuleType = "fwalert"
type FwAlertRuleConfig struct {
WebhookURL string `json:"webhook_url"`
@@ -1,23 +1,23 @@
package fwalert
import (
"encoding/json"
"fmt"
"net/url"
"github.com/ssdomei232/goodBaby/internal/meta"
)
// FwalertRuleValidator 饭碗警告规则验证器
type FwalertRuleValidator struct{}
func (v *FwalertRuleValidator) GetType() string {
return RuleTypeFwalert
return RuleType
}
func (v *FwalertRuleValidator) Validate(configJSON string) error {
var config FwAlertRuleConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析饭碗警告规则配置失败: %v", err)
config, err := ParseRuleConfig(configJSON)
if err != nil {
return err
}
parsed, err := url.Parse(config.WebhookURL)
@@ -40,23 +40,14 @@ func (v *FwalertRuleValidator) Validate(configJSON string) error {
func (v *FwalertRuleValidator) Meta() meta.RuleMeta {
return meta.RuleMeta{
Type: RuleTypeFwalert,
Type: RuleType,
Label: "发送饭碗警告",
Description: "触发时发送饭碗警告消息。",
Docs: "docs/fwalert-config.md",
AccountType: "",
Fields: []meta.Field{
{Key: "webhook_url", Label: "Webhook URL", Type: meta.FieldString, Required: true, Help: "饭碗警告的触发地址", Placeholder: "https://fwalert.com/xxx-xxx-xxx-xxx-xxx"},
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, Help: "最大长度 1000 个字符"},
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, GatewayMessage: true, Help: "最大长度 1000 个字符"},
},
}
}
// ParseRuleConfig 解析饭碗警告规则配置
func ParseRuleConfig(configJSON string) (*FwAlertRuleConfig, error) {
var config FwAlertRuleConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析饭碗警告规则配置失败: %v", err)
}
return &config, nil
}
+5 -10
View File
@@ -2,9 +2,7 @@ package fwalert
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
@@ -12,18 +10,15 @@ import (
type FwalertExecutor struct{}
func (e *FwalertExecutor) GetType() string {
return RuleTypeFwalert
return RuleType
}
func (e *FwalertExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行饭碗警告规则: %s (ID: %d)", rule.Name, rule.ID)
config, err := ParseRuleConfig(rule.ConfigJson)
func (e *FwalertExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
// 饭碗警告的凭据就是 Webhook 地址,直接写在规则配置里
config, err := ParseRuleConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return retry.Do(ctx, func() error {
return send(config)
})
return Send(ctx, config)
}
+15
View File
@@ -0,0 +1,15 @@
package fwalert
import (
"encoding/json"
"fmt"
)
// ParseRuleConfig 解析饭碗警告规则配置
func ParseRuleConfig(configJSON string) (*FwAlertRuleConfig, error) {
var config FwAlertRuleConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析饭碗警告规则配置失败: %v", err)
}
return &config, nil
}
+2 -11
View File
@@ -2,7 +2,6 @@ package github
import (
"context"
"encoding/json"
"fmt"
"github.com/google/go-github/v84/github"
@@ -18,7 +17,7 @@ func (v *GitHubAccountConfigValidator) GetType() string {
}
func (v *GitHubAccountConfigValidator) Validate(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -35,7 +34,7 @@ func (v *GitHubAccountConfigValidator) Validate(config string) error {
// Test 用 token 拉一次当前用户信息,验证 token 是否有效
func (v *GitHubAccountConfigValidator) Test(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -65,11 +64,3 @@ func (v *GitHubAccountConfigValidator) Meta() meta.AccountMeta {
},
}
}
func parseAccount(config string) (*GithubAccount, error) {
var cfg GithubAccount
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析GitHub账号配置失败: %v", err)
}
return &cfg, nil
}
+9 -14
View File
@@ -6,29 +6,24 @@ import (
"strings"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// MakeRepositoryPublic 将规则中配置的仓库设置为 public
func MakeRepositoryPublic(ctx context.Context, rule *model.Rule) error {
reposConfig, account, err := GetGithubReposAndAccountFromRule(rule)
if err != nil {
return err
}
// MakeRepositoryPublic 把配置里的仓库逐个设置为 public
//
// 每个仓库单独重试、互不阻塞,最后汇总失败信息交给上层记录。
func MakeRepositoryPublic(ctx context.Context, account *GithubAccount, config *GithubReposConfig) error {
var fails []string
for _, repo := range reposConfig.Repos {
err := retry.Do(ctx, func() error {
for _, repo := range config.Repos {
if err := retry.Do(ctx, func() error {
return SetRepositoryPublic(ctx, account.Token, account.Owner, repo)
})
if err != nil {
fails = append(fails, fmt.Sprintf("%s: %v", repo, err))
}); err != nil {
fails = append(fails, err.Error())
}
}
if len(fails) > 0 {
return fmt.Errorf("%d/%d 个仓库设置为 public 失败: %s",
len(fails), len(reposConfig.Repos), strings.Join(fails, "; "))
len(fails), len(config.Repos), strings.Join(fails, "; "))
}
return nil
}
+3 -4
View File
@@ -1,7 +1,6 @@
package github
import (
"encoding/json"
"fmt"
"strings"
@@ -15,9 +14,9 @@ func (v *GithubMakeRepositoryPublicRuleValidator) GetType() string {
}
func (v *GithubMakeRepositoryPublicRuleValidator) Validate(configJSON string) error {
var config GithubReposConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析 GitHub 规则配置失败: %v", err)
config, err := ParseReposConfig(configJSON)
if err != nil {
return err
}
if len(config.Repos) == 0 {
+11 -4
View File
@@ -2,7 +2,6 @@ package github
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
@@ -14,8 +13,16 @@ func (e *GithubMakeRepoPublicExecutor) GetType() string {
return RuleTypeMakeRepoPublic
}
func (e *GithubMakeRepoPublicExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行GitHub仓库公开规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *GithubMakeRepoPublicExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return MakeRepositoryPublic(ctx, rule)
config, err := ParseReposConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return MakeRepositoryPublic(ctx, account, config)
}
+22 -18
View File
@@ -6,33 +6,37 @@ import (
"fmt"
"github.com/google/go-github/v84/github"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
)
// GetGithubReposAndAccountFromRule 从 rule 中获取 GithubReposConfig 和 GithubAccount
func GetGithubReposAndAccountFromRule(rule *model.Rule) (*GithubReposConfig, *GithubAccount, error) {
var githubReposConfig GithubReposConfig
if err := json.Unmarshal([]byte(rule.ConfigJson), &githubReposConfig); err != nil {
return nil, nil, fmt.Errorf("解析 GitHub 规则配置失败: %w", err)
// ParseAccountConfig 解析 GitHub 账号配置
func ParseAccountConfig(config string) (*GithubAccount, error) {
if config == "" {
return nil, fmt.Errorf("解析GitHub账号配置失败: 规则没有关联账号")
}
var githubAccount GithubAccount
if err := db.LoadAccountConfig(rule.AccountID, &githubAccount); err != nil {
return nil, nil, err
var account GithubAccount
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析GitHub账号配置失败: %v", err)
}
return &githubReposConfig, &githubAccount, nil
return &account, nil
}
// SetRepositoryPublic 将仓库设置为 public
// ParseReposConfig 解析仓库规则配置
func ParseReposConfig(configJSON string) (*GithubReposConfig, error) {
var config GithubReposConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析GitHub规则配置失败: %v", err)
}
return &config, nil
}
// SetRepositoryPublic 把一个仓库设置为 public
func SetRepositoryPublic(ctx context.Context, token, owner, repo string) error {
client := github.NewClient(nil).WithAuthToken(token)
opts := &github.Repository{
if _, _, err := client.Repositories.Edit(ctx, owner, repo, &github.Repository{
Visibility: github.Ptr("public"),
}); err != nil {
return fmt.Errorf("设置仓库 %s 为 public 失败: %w", repo, err)
}
_, _, err := client.Repositories.Edit(ctx, owner, repo, opts)
return err
return nil
}
+2 -11
View File
@@ -1,7 +1,6 @@
package onebot
import (
"encoding/json"
"fmt"
"net/url"
"strings"
@@ -17,7 +16,7 @@ func (v *OneBotAccountConfigValidator) GetType() string {
}
func (v *OneBotAccountConfigValidator) Validate(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -39,7 +38,7 @@ func (v *OneBotAccountConfigValidator) Validate(config string) error {
// Test 调用 get_login_info 验证地址与 Token
func (v *OneBotAccountConfigValidator) Test(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -62,11 +61,3 @@ func (v *OneBotAccountConfigValidator) Meta() meta.AccountMeta {
},
}
}
func parseAccount(config string) (*OneBotAccount, error) {
var cfg OneBotAccount
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析OneBot账号配置失败: %v", err)
}
return &cfg, nil
}
+12 -23
View File
@@ -6,40 +6,29 @@ import (
"strings"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
)
// SendOneBotMsg 向规则中配置的群和好友发送消息
func SendOneBotMsg(ctx context.Context, rule *model.Rule) error {
oneBotConfig, err := getOneBotConfigFromRule(rule)
if err != nil {
return err
}
oneBotAccount, err := getOneBotAccountFromRule(rule)
if err != nil {
return fmt.Errorf("获取OneBot账户失败: %w", err)
}
// SendMessage 向规则中配置的群和好友发送消息
//
// 每个目标单独重试、互不阻塞,最后汇总失败信息交给上层记录。
func SendMessage(ctx context.Context, account *OneBotAccount, config *OneBotConfig) error {
var fails []string
total := 0
for _, groupID := range oneBotConfig.SendGroups {
for _, groupID := range config.SendGroups {
total++
err := retry.Do(ctx, func() error {
return sendGroupMsg(oneBotAccount, groupID, oneBotConfig.Msg)
})
if err != nil {
if err := retry.Do(ctx, func() error {
return sendGroupMsg(account, groupID, config.Msg)
}); err != nil {
fails = append(fails, fmt.Sprintf("群 %d: %v", groupID, err))
}
}
for _, userID := range oneBotConfig.SendUsers {
for _, userID := range config.SendUsers {
total++
err := retry.Do(ctx, func() error {
return sendPrivateMsg(oneBotAccount, userID, oneBotConfig.Msg)
})
if err != nil {
if err := retry.Do(ctx, func() error {
return sendPrivateMsg(account, userID, config.Msg)
}); err != nil {
fails = append(fails, fmt.Sprintf("好友 %d: %v", userID, err))
}
}
+4 -5
View File
@@ -1,7 +1,6 @@
package onebot
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -15,9 +14,9 @@ func (v *OneBotRuleValidator) GetType() string {
}
func (v *OneBotRuleValidator) Validate(configJSON string) error {
var config OneBotConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析OneBot规则配置失败: %v", err)
config, err := ParseRuleConfig(configJSON)
if err != nil {
return err
}
if config.Msg == "" {
@@ -44,7 +43,7 @@ func (v *OneBotRuleValidator) Meta() meta.RuleMeta {
Description: "触发时通过 OneBot 向指定的群或好友发送消息。",
AccountType: AccountType,
Fields: []meta.Field{
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true},
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, GatewayMessage: true},
{Key: "send_groups", Label: "群号", Type: meta.FieldNumberList, Placeholder: "123456789"},
{Key: "send_users", Label: "好友 QQ 号", Type: meta.FieldNumberList, Placeholder: "123456789"},
},
+11 -4
View File
@@ -2,7 +2,6 @@ package onebot
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
@@ -14,8 +13,16 @@ func (e *OneBotExecutor) GetType() string {
return RuleType
}
func (e *OneBotExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行OneBot规则: %s (ID: %d)", rule.Name, rule.ID)
func (e *OneBotExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return SendOneBotMsg(ctx, rule)
config, err := ParseRuleConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return SendMessage(ctx, account, config)
}
+29 -25
View File
@@ -4,37 +4,37 @@ import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
napcat "github.com/ssdomei232/napcat-http-go-sdk"
)
// ParseAccountConfig 解析 OneBot 账号配置
func ParseAccountConfig(config string) (*OneBotAccount, error) {
if config == "" {
return nil, fmt.Errorf("解析OneBot账号配置失败: 规则没有关联账号")
}
var account OneBotAccount
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析OneBot账号配置失败: %v", err)
}
return &account, nil
}
// ParseRuleConfig 解析 OneBot 规则配置
func ParseRuleConfig(configJSON string) (*OneBotConfig, error) {
var config OneBotConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析OneBot规则配置失败: %v", err)
}
return &config, nil
}
// newClient 创建 OneBot HTTP 客户端
func newClient(account *OneBotAccount) *napcat.Client {
return napcat.NewClient(account.Token, account.URL)
}
// 从 Rule 中获取 OneBotConfig
func getOneBotConfigFromRule(rule *model.Rule) (*OneBotConfig, error) {
var oneBotConfig OneBotConfig
if err := json.Unmarshal([]byte(rule.ConfigJson), &oneBotConfig); err != nil {
return nil, fmt.Errorf("解析OneBot规则配置失败: %w", err)
}
return &oneBotConfig, nil
}
// 从 Rule 中获取 OneBotAccount
func getOneBotAccountFromRule(rule *model.Rule) (*OneBotAccount, error) {
var oneBotAccount OneBotAccount
if err := db.LoadAccountConfig(rule.AccountID, &oneBotAccount); err != nil {
return nil, err
}
return &oneBotAccount, nil
}
// 发送一条群消息
//
// SDK 只返回传输层错误,这里自己解析 OneBot 的响应信封,
// 否则 token 错误、群号不存在这类失败会被当成发送成功。
// sendGroupMsg 发送一条群消息
func sendGroupMsg(account *OneBotAccount, groupID int64, msg string) error {
return call(account, "/send_group_msg", map[string]any{
"group_id": groupID,
@@ -42,7 +42,7 @@ func sendGroupMsg(account *OneBotAccount, groupID int64, msg string) error {
})
}
// 发送一条私聊消息
// sendPrivateMsg 发送一条私聊消息
func sendPrivateMsg(account *OneBotAccount, userID int64, msg string) error {
return call(account, "/send_private_msg", map[string]any{
"user_id": userID,
@@ -50,6 +50,10 @@ func sendPrivateMsg(account *OneBotAccount, userID int64, msg string) error {
})
}
// call 调用一次 OneBot HTTP API。
//
// SDK 只返回传输层错误,这里自己解析 OneBot 的响应信封,
// 否则 token 错误、群号不存在这类失败会被当成发送成功。
func call(account *OneBotAccount, endpoint string, payload map[string]any) error {
var resp apiResponse
if err := newClient(account).DoRequest("POST", endpoint, payload, &resp); err != nil {
+2 -11
View File
@@ -1,7 +1,6 @@
package rainyun
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -16,7 +15,7 @@ func (v *RainyunAccountConfigValidator) GetType() string { return AccountType }
func (v *RainyunAccountConfigValidator) Validate(config string) error {
// 解析配置
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
@@ -28,16 +27,8 @@ func (v *RainyunAccountConfigValidator) Validate(config string) error {
return nil
}
func parseAccount(config string) (*RainyunAccount, error) {
var cfg RainyunAccount
if err := json.Unmarshal([]byte(config), &cfg); err != nil {
return nil, fmt.Errorf("解析雨云账号配置失败: %v", err)
}
return &cfg, nil
}
func (v *RainyunAccountConfigValidator) Test(config string) error {
cfg, err := parseAccount(config)
cfg, err := ParseAccountConfig(config)
if err != nil {
return err
}
+6
View File
@@ -8,6 +8,12 @@ const (
RuleTypeRunAway = "rainyun-runaway"
)
// pageSize 拉取云服务器列表时每页的条数
const pageSize = 20
// runAwayConfirmText 跑路规则要求用户输入的免责声明
const runAwayConfirmText = "我已知晓"
type RainyunWorkOrderRule struct {
Title string `json:"title"`
Msg string `json:"msg"`
+114 -88
View File
@@ -6,121 +6,147 @@ import (
"strings"
"github.com/ssdomei232/goodBaby/internal/retry"
"github.com/ssdomei232/goodBaby/model"
rain "github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/common"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/rcs"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/rgs"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/workorder"
)
func SendRainyunWorkorderMsg(ctx context.Context, rule *model.Rule) error {
rainyunConfig, err := getRainyunConfigFromRule(rule)
if err != nil {
return err
// SendWorkOrder 用给定账号发送一条雨云工单
func SendWorkOrder(ctx context.Context, account *RainyunAccount, config *RainyunWorkOrderRule) error {
if err := retry.Do(ctx, func() error {
return createWorkOrder(account, config)
}); err != nil {
return fmt.Errorf("发送雨云工单失败: %w", err)
}
rainyunAccount, err := getRainyunAccountFromRule(rule)
if err != nil {
return err
}
err = retry.Do(ctx, func() error {
return sendRainyunWorkorderMsg(rainyunConfig, rainyunAccount)
})
if err != nil {
return err
}
return nil
}
func sendRainyunWorkorderMsg(rainyunRuleConfig *RainyunWorkOrderRule, rainyunAccount *RainyunAccount) error {
client := rain.NewClient(rainyunAccount.APIKey)
workorderClient := workorder.Client{Client: client}
// createWorkOrder 调用一次工单接口
func createWorkOrder(account *RainyunAccount, config *RainyunWorkOrderRule) error {
workorderClient := workorder.Client{Client: rain.NewClient(account.APIKey)}
_, err := workorderClient.CreateWorkOrder(&workorder.CreateWorkerorderRequest{
Content: rainyunRuleConfig.Msg,
Content: config.Msg,
IsAuthed: false,
IsUrgent: 1,
Title: rainyunRuleConfig.Title,
Title: config.Title,
Type: "reward",
})
if err != nil {
return err
}
return nil
return err
}
func RainyunRunAway(ctx context.Context, rule *model.Rule) error {
rainyunAccount, err := getRainyunAccountFromRule(rule)
// RunAway 重装账号下所有云服务器(一键跑路)
func RunAway(ctx context.Context, account *RainyunAccount) error {
client := rain.NewClient(account.APIKey)
rcsFails, rcsTotal, err := reinstallAllRcs(ctx, client)
if err != nil {
return err
}
client := rain.NewClient(rainyunAccount.APIKey)
var fails []string
total := 0
// 重装所有RCS
rcsClient := rcs.Client{Client: client}
options := rain.EncodingStandardQueryParameters(1, 20)
rcsList, err := rcsClient.GetRcsList(options)
rgsFails, rgsTotal, err := reinstallAllRgs(ctx, client)
if err != nil {
return err
}
if rcsList.Data.TotalRecords == 0 {
return nil
} else if rcsList.Data.TotalRecords > 20 {
rcsTotal := rcsList.Data.TotalRecords
options := rain.EncodingStandardQueryParameters(1, rcsTotal)
rcsList, err = rcsClient.GetRcsList(options)
if err != nil {
return err
}
}
for _, rcs := range rcsList.Data.Records {
total++
err := retry.Do(context.Background(), func() error {
return reinstallOneRCS(rcs.ID, &rcsClient)
})
if err != nil {
fails = append(fails, fmt.Sprintf("RCS %d: %v", rcs.ID, err))
}
}
// 重装所有RGS
rgsClient := rgs.Client{Client: client}
rgsOptions := rain.EncodingStandardQueryParameters(1, 20)
rgsList, err := rgsClient.GetRgsList(rgsOptions)
if err != nil {
return err
}
if rgsList.Data.TotalRecords == 0 {
return nil
} else if rgsList.Data.TotalRecords > 20 {
rgsTotal := rgsList.Data.TotalRecords
rgsOptions := rain.EncodingStandardQueryParameters(1, rgsTotal)
rgsList, err = rgsClient.GetRgsList(rgsOptions)
if err != nil {
return err
}
}
for _, rgs := range rgsList.Data.Records {
total++
err := retry.Do(context.Background(), func() error {
return reinstallOneRGS(rgs.ID, &rgsClient)
})
if err != nil {
fails = append(fails, fmt.Sprintf("RGS %d: %v", rgs.ID, err))
}
}
fails := append(rcsFails, rgsFails...)
total := rcsTotal + rgsTotal
if len(fails) > 0 {
return fmt.Errorf("%d/%d 台云服务器重装失败: %s", len(fails), total, strings.Join(fails, "; "))
}
return nil
}
// reinstallAllRcs 重装全部 RCS,返回失败信息与云服务器总数
func reinstallAllRcs(ctx context.Context, client *rain.Client) ([]string, int, error) {
rcsClient := rcs.Client{Client: client}
ids, err := listRcsIDs(&rcsClient)
if err != nil {
return nil, 0, err
}
var fails []string
for _, id := range ids {
if err := retry.Do(ctx, func() error {
return reinstallOneRCS(id, &rcsClient)
}); err != nil {
fails = append(fails, fmt.Sprintf("RCS %d: %v", id, err))
}
}
return fails, len(ids), nil
}
// reinstallAllRgs 重装全部 RGS,返回失败信息与云服务器总数
func reinstallAllRgs(ctx context.Context, client *rain.Client) ([]string, int, error) {
rgsClient := rgs.Client{Client: client}
ids, err := listRgsIDs(&rgsClient)
if err != nil {
return nil, 0, err
}
var fails []string
for _, id := range ids {
if err := retry.Do(ctx, func() error {
return reinstallOneRGS(id, &rgsClient)
}); err != nil {
fails = append(fails, fmt.Sprintf("RGS %d: %v", id, err))
}
}
return fails, len(ids), nil
}
// listRcsIDs 取账号下全部 RCS 的 ID,超过一页时重新按总数拉取
func listRcsIDs(client *rcs.Client) ([]int, error) {
first, err := client.GetRcsList(rain.EncodingStandardQueryParameters(1, pageSize))
if err != nil {
return nil, err
}
ids := make([]int, 0, len(first.Data.Records))
for _, server := range first.Data.Records {
ids = append(ids, server.ID)
}
if first.Data.TotalRecords <= pageSize {
return ids, nil
}
all, err := client.GetRcsList(rain.EncodingStandardQueryParameters(1, first.Data.TotalRecords))
if err != nil {
return nil, err
}
ids = ids[:0]
for _, server := range all.Data.Records {
ids = append(ids, server.ID)
}
return ids, nil
}
// listRgsIDs 取账号下全部 RGS 的 ID,超过一页时重新按总数拉取
func listRgsIDs(client *rgs.Client) ([]int, error) {
first, err := client.GetRgsList(rain.EncodingStandardQueryParameters(1, pageSize))
if err != nil {
return nil, err
}
ids := make([]int, 0, len(first.Data.Records))
for _, server := range first.Data.Records {
ids = append(ids, server.ID)
}
if first.Data.TotalRecords <= pageSize {
return ids, nil
}
all, err := client.GetRgsList(rain.EncodingStandardQueryParameters(1, first.Data.TotalRecords))
if err != nil {
return nil, err
}
ids = ids[:0]
for _, server := range all.Data.Records {
ids = append(ids, server.ID)
}
return ids, nil
}
+7 -8
View File
@@ -1,7 +1,6 @@
package rainyun
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
@@ -15,9 +14,9 @@ func (v *RainyunWorkorderRuleValidator) GetType() string {
}
func (v *RainyunWorkorderRuleValidator) Validate(configJSON string) error {
var config RainyunWorkOrderRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析雨云规则配置失败: %v", err)
config, err := ParseWorkOrderConfig(configJSON)
if err != nil {
return err
}
if config.Title == "" {
@@ -51,12 +50,12 @@ func (v *RainyunRunAwayRuleValidator) GetType() string {
}
func (v *RainyunRunAwayRuleValidator) Validate(configJSON string) error {
var config RainyunRunAwayRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析雨云规则配置失败: %v", err)
config, err := ParseRunAwayConfig(configJSON)
if err != nil {
return err
}
if config.IK != "我已知晓" {
if config.IK != runAwayConfirmText {
return fmt.Errorf("请输入正确内容")
}
+29 -8
View File
@@ -2,22 +2,30 @@ package rainyun
import (
"context"
"log"
"fmt"
"github.com/ssdomei232/goodBaby/model"
)
// RainyunExecutor 雨云执行器
// RainyunWorkorderExecutor 雨云工单执行器
type RainyunWorkorderExecutor struct{}
func (e *RainyunWorkorderExecutor) GetType() string {
return RuleTypeWorkOrder
}
func (e *RainyunWorkorderExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行雨云工单规则: %s", rule.Name)
func (e *RainyunWorkorderExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
return SendRainyunWorkorderMsg(ctx, rule)
config, err := ParseWorkOrderConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
return SendWorkOrder(ctx, account, config)
}
// RainyunRunAwayExecutor 雨云跑路执行器
@@ -27,7 +35,20 @@ func (e *RainyunRunAwayExecutor) GetType() string {
return RuleTypeRunAway
}
func (e *RainyunRunAwayExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行雨云跑路规则: %s", rule.Name)
return RainyunRunAway(ctx, rule)
func (e *RainyunRunAwayExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
account, err := ParseAccountConfig(task.AccountConfig())
if err != nil {
return err
}
// 跑路规则只要求用户填一句免责声明,执行时重新校验一次
config, err := ParseRunAwayConfig(task.Rule.ConfigJson)
if err != nil {
return err
}
if config.IK != runAwayConfirmText {
return fmt.Errorf("请输入正确内容")
}
return RunAway(ctx, account)
}
+35 -30
View File
@@ -4,31 +4,43 @@ import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/handler/db"
"github.com/ssdomei232/goodBaby/model"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/public"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/rcs"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/rgs"
)
// 从 Rule 中获取 RainyunConfig
func getRainyunConfigFromRule(rule *model.Rule) (*RainyunWorkOrderRule, error) {
var rainyunConfig RainyunWorkOrderRule
if err := json.Unmarshal([]byte(rule.ConfigJson), &rainyunConfig); err != nil {
return nil, fmt.Errorf("解析Rainyun规则配置失败: %w", err)
// ParseAccountConfig 解析雨云账号配置
func ParseAccountConfig(config string) (*RainyunAccount, error) {
if config == "" {
return nil, fmt.Errorf("解析雨云账号配置失败: 规则没有关联账号")
}
return &rainyunConfig, nil
var account RainyunAccount
if err := json.Unmarshal([]byte(config), &account); err != nil {
return nil, fmt.Errorf("解析雨云账号配置失败: %v", err)
}
return &account, nil
}
// 从 Rule 中获取 RainyunAccount
func getRainyunAccountFromRule(rule *model.Rule) (*RainyunAccount, error) {
var rainyunAccount RainyunAccount
if err := db.LoadAccountConfig(rule.AccountID, &rainyunAccount); err != nil {
return nil, err
// ParseWorkOrderConfig 解析雨云工单规则配置
func ParseWorkOrderConfig(configJSON string) (*RainyunWorkOrderRule, error) {
var config RainyunWorkOrderRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析雨云工单规则配置失败: %v", err)
}
return &rainyunAccount, nil
return &config, nil
}
// ParseRunAwayConfig 解析雨云跑路规则配置
func ParseRunAwayConfig(configJSON string) (*RainyunRunAwayRule, error) {
var config RainyunRunAwayRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("解析雨云跑路规则配置失败: %v", err)
}
return &config, nil
}
// reinstallOneRCS 重装一台 RCS
func reinstallOneRCS(rcsID int, rcsClient *rcs.Client) error {
rcsDetail, err := rcsClient.GetRcsDetails(rcsID)
if err != nil {
@@ -42,20 +54,17 @@ func reinstallOneRCS(rcsID int, rcsClient *rcs.Client) error {
for _, os := range rcsOSList.Data {
if os.Region == rcsDetail.Data.Data.Node.Region && os.OsType == "linux" {
rcsReinstallRequest := &rcs.ReinstallRcsRequest{
_, err := rcsClient.ReinstallRcs(rcsID, &rcs.ReinstallRcsRequest{
OsID: os.ID,
ResetOsd: true,
}
_, err := rcsClient.ReinstallRcs(rcsID, rcsReinstallRequest)
if err != nil {
return err
}
break
})
return err
}
}
return nil
return fmt.Errorf("没有找到可用的 linux 镜像")
}
// reinstallOneRGS 重装一台 RGS
func reinstallOneRGS(rgsID int, rgsClient *rgs.Client) error {
rgsDetail, err := rgsClient.GetRgsDetails(rgsID)
if err != nil {
@@ -69,16 +78,12 @@ func reinstallOneRGS(rgsID int, rgsClient *rgs.Client) error {
for _, os := range rgsOSList.Data {
if os.Region == rgsDetail.Data.Data.Node.Region && os.OsType == "linux" {
rgsReinstallRequest := &rcs.ReinstallRcsRequest{
_, err := rgsClient.Reinstallgs(rgsID, &rcs.ReinstallRcsRequest{
OsID: os.ID,
ResetOsd: true,
}
_, err := rgsClient.Reinstallgs(rgsID, rgsReinstallRequest)
if err != nil {
return err
}
break
})
return err
}
}
return nil
return fmt.Errorf("没有找到可用的 linux 镜像")
}