6 Commits
Author SHA1 Message Date
mei df5165d4a3 fix 2026-08-13 14:56:02 +08:00
mei 868731c95e fix(drivers): rainyun run away 2026-08-13 14:50:08 +08:00
mei 6990166dfc feat(drivers): 新增雨云一键跑路 2026-08-13 14:46:43 +08:00
mei fa07a6f0e4 fix 2026-08-13 09:18:26 +08:00
mei d428a7db2e feat(drivers): 新增雨云工单 2026-08-13 09:13:10 +08:00
mei 8163d0d9d4 fix(README.md) 2026-08-02 20:59:30 +08:00
13 changed files with 424 additions and 7 deletions
+2 -3
View File
@@ -1,14 +1,13 @@
# goodBaby v2
![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/ssdomei232/goodBaby)
![GitHub](https://img.shields.io/github/license/ssdomei232/goodBaby)
![GitHub tag (with filter)](https://img.shields.io/github/v/tag/ssdomei232/goodBaby)
![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/ssdomei232/goodBaby) ![GitHub](https://img.shields.io/github/license/ssdomei232/goodBaby) ![GitHub tag (with filter)](https://img.shields.io/github/v/tag/ssdomei232/goodBaby)
根据《中国心血管健康与疾病报告2023》,中国 每年 猝死总人数(含心源性和非心源性)或达80万-100万,18-35岁人群占比为38%,也就是说,每天18-35岁人群有近1000人猝死
伴随着中国人口老龄化加剧,年轻人的牵挂越来越多,面对着可能的猝死,我们或许真的需要做好准备
所以我写了goodBaby来解决这个问题,他和apple app store里的“死了么”形式上有些接近,不过goodBaby支持了更多好玩的通知方式,比如bilibili动态,github仓库,邮件,OneBot,还可以方便的自托管
[![通过雨云一键部署](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-cn.svg)](https://app.rainyun.com/apps/rca/store/7125/cat_)
## 功能
+1 -1
View File
@@ -37,7 +37,7 @@ git push origin v2.2.0
编译时通过 ldflags 注入,启动日志里能看到:
```
```log
goodBaby v2.2.0 (a1b2c3d, 2026-07-27T07:00:00Z) 启动中...
```
+67
View File
@@ -0,0 +1,67 @@
package rainyun
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
rain "github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/common"
"github.com/ssdomei232/rainyun-go-sdk/v2/rainyun/user"
)
// RainyunAccountConfigValidator Rainyun账号配置验证器
type RainyunAccountConfigValidator struct{}
func (v *RainyunAccountConfigValidator) GetType() string {
return AccountType
}
func (v *RainyunAccountConfigValidator) Validate(config string) error {
// 解析配置
cfg, err := parseAccount(config)
if err != nil {
return err
}
if cfg.APIKey == "" {
return fmt.Errorf("雨云账号配置中APIKey不能为空")
}
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)
if err != nil {
return err
}
client := rain.NewClient(cfg.APIKey)
userClient := user.Client{Client: client}
_, err = userClient.GetUserInfo()
if err != nil {
return fmt.Errorf("测试雨云账号配置失败: %v", err)
}
return nil
}
func (v *RainyunAccountConfigValidator) Meta() meta.AccountMeta {
return meta.AccountMeta{
Type: AccountType,
Label: "雨云",
Description: "对接雨云,方便各位代理/运维删库跑路",
Fields: []meta.Field{
{Key: "api_key", Label: "API Key", Type: meta.FieldPassword, Required: true, Secret: true, Placeholder: "xxxxxxxxx"},
},
}
}
+21
View File
@@ -0,0 +1,21 @@
package rainyun
const (
// AccountType Rainyun 账号类型标识
AccountType = "rainyun"
// RuleType 发送 Rainyun 消息的规则类型标识
RuleType = "rainyun-workorder"
)
type RainyunWorkOrderRule struct {
Title string `json:"title"`
Msg string `json:"msg"`
}
type RainyunAccount struct {
APIKey string `json:"api_key"`
}
type RainyunRunAwayRule struct {
IK string `json:"ik"`
}
+126
View File
@@ -0,0 +1,126 @@
package rainyun
import (
"context"
"fmt"
"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
}
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}
_, err := workorderClient.CreateWorkOrder(&workorder.CreateWorkerorderRequest{
Content: rainyunRuleConfig.Msg,
IsAuthed: false,
IsUrgent: 1,
Title: rainyunRuleConfig.Title,
Type: "reward",
})
if err != nil {
return err
}
return nil
}
func RainyunRunAway(ctx context.Context, rule *model.Rule) error {
rainyunAccount, err := getRainyunAccountFromRule(rule)
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)
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))
}
}
if len(fails) > 0 {
return fmt.Errorf("%d/%d 台云服务器重装失败: %s", len(fails), total, strings.Join(fails, "; "))
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
package rainyun
import (
"encoding/json"
"fmt"
"github.com/ssdomei232/goodBaby/internal/meta"
)
// Rainyun规则验证器
type RainyunWorkorderRuleValidator struct{}
func (v *RainyunWorkorderRuleValidator) GetType() string {
return RuleType
}
func (v *RainyunWorkorderRuleValidator) Validate(configJSON string) error {
var config RainyunWorkOrderRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析雨云规则配置失败: %v", err)
}
if config.Title == "" {
return fmt.Errorf("雨云规则配置中 title 不能为空")
}
if config.Msg == "" {
return fmt.Errorf("雨云规则配置中 msg 不能为空")
}
return nil
}
func (v *RainyunWorkorderRuleValidator) Meta() meta.RuleMeta {
return meta.RuleMeta{
Type: RuleType,
Label: "发送雨云工单",
Description: "触发时发送雨云工单",
AccountType: AccountType,
Fields: []meta.Field{
{Key: "title", Label: "工单标题", Type: meta.FieldTextarea, Required: true},
{Key: "msg", Label: "工单内容", Type: meta.FieldTextarea, Required: true},
},
}
}
type RainyunRunAwayRuleValidator struct{}
func (v *RainyunRunAwayRuleValidator) GetType() string {
return "rainyun-runaway"
}
func (v *RainyunRunAwayRuleValidator) Validate(configJSON string) error {
var config RainyunRunAwayRule
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return fmt.Errorf("解析雨云规则配置失败: %v", err)
}
if config.IK != "我已知晓" {
return fmt.Errorf("请输入正确内容")
}
return nil
}
func (v *RainyunRunAwayRuleValidator) Meta() meta.RuleMeta {
return meta.RuleMeta{
Type: "rainyun-runaway",
Label: "重置雨云账号中所有云服务器(一键跑路)",
Description: "触发时会重装雨云账号下所有云服务器来实现跑路",
AccountType: AccountType,
Fields: []meta.Field{
{Key: "ik", Label: "免责声明", Type: meta.FieldTextarea, Required: true, Placeholder: "该操作引发的损失及法律责任归属账号所有人,本软件一律不承担,请输入\"我已知晓\""},
},
}
}
+33
View File
@@ -0,0 +1,33 @@
package rainyun
import (
"context"
"log"
"github.com/ssdomei232/goodBaby/model"
)
// RainyunExecutor 雨云执行器
type RainyunWorkorderExecutor struct{}
func (e *RainyunWorkorderExecutor) GetType() string {
return RuleType
}
func (e *RainyunWorkorderExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行雨云工单规则: %s", rule.Name)
return SendRainyunWorkorderMsg(ctx, rule)
}
// RainyunRunAwayExecutor 雨云跑路执行器
type RainyunRunAwayExecutor struct{}
func (e *RainyunRunAwayExecutor) GetType() string {
return "rainyun-runaway"
}
func (e *RainyunRunAwayExecutor) Execute(ctx context.Context, rule *model.Rule) error {
log.Printf("执行雨云跑路规则: %s", rule.Name)
return RainyunRunAway(ctx, rule)
}
+84
View File
@@ -0,0 +1,84 @@
package rainyun
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)
}
return &rainyunConfig, 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
}
return &rainyunAccount, nil
}
func reinstallOneRCS(rcsID int, rcsClient *rcs.Client) error {
rcsDetail, err := rcsClient.GetRcsDetails(rcsID)
if err != nil {
return err
}
rcsOSList, err := public.GetRcsOSList()
if err != nil {
return err
}
for _, os := range rcsOSList.Data {
if os.Region == rcsDetail.Data.Data.Node.Region && os.OsType == "linux" {
rcsReinstallRequest := &rcs.ReinstallRcsRequest{
OsID: os.ID,
ResetOsd: true,
}
_, err := rcsClient.ReinstallRcs(rcsID, rcsReinstallRequest)
if err != nil {
return err
}
break
}
}
return nil
}
func reinstallOneRGS(rgsID int, rgsClient *rgs.Client) error {
rgsDetail, err := rgsClient.GetRgsDetails(rgsID)
if err != nil {
return err
}
rgsOSList, err := public.GetRgsOSList()
if err != nil {
return err
}
for _, os := range rgsOSList.Data {
if os.Region == rgsDetail.Data.Data.Node.Region && os.OsType == "linux" {
rgsReinstallRequest := &rcs.ReinstallRcsRequest{
OsID: os.ID,
ResetOsd: true,
}
_, err := rgsClient.Reinstallgs(rgsID, rgsReinstallRequest)
if err != nil {
return err
}
break
}
}
return nil
}
+3 -2
View File
@@ -9,11 +9,14 @@ require (
github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.17.2
github.com/google/go-github/v84 v84.0.0
github.com/robfig/cron/v3 v3.0.1
github.com/ssdomei232/napcat-http-go-sdk v1.0.0
github.com/ssdomei232/rainyun-go-sdk/v2 v2.0.6
github.com/wneessen/go-mail v0.7.2
golang.org/x/crypto v0.48.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.12
)
@@ -29,7 +32,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-resty/resty/v2 v2.17.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
@@ -68,7 +70,6 @@ require (
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
+2
View File
@@ -123,6 +123,8 @@ github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/ssdomei232/napcat-http-go-sdk v1.0.0 h1:BY3iqqlZw5letiCwTmfMrhyqsx0a96eL5LpDuinnOSE=
github.com/ssdomei232/napcat-http-go-sdk v1.0.0/go.mod h1:yPsSaMs3OyKMVPXwIs5ZB5G532vRi1GvL/WiWUEwblU=
github.com/ssdomei232/rainyun-go-sdk/v2 v2.0.6 h1:tjratVKpPk3lc2bw+oVnIzVZG79mFlvUBez+FbX86pQ=
github.com/ssdomei232/rainyun-go-sdk/v2 v2.0.6/go.mod h1:Wf9X0qEpKQ1/uD4iUGGoPLHydiCxULqCVk+JCoVqdIg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+3
View File
@@ -12,6 +12,7 @@ import (
"github.com/ssdomei232/goodBaby/drivers/fwalert"
"github.com/ssdomei232/goodBaby/drivers/github"
"github.com/ssdomei232/goodBaby/drivers/onebot"
"github.com/ssdomei232/goodBaby/drivers/rainyun"
"github.com/ssdomei232/goodBaby/model"
)
@@ -78,6 +79,8 @@ func InitExecutorRegistry() *ExecutorRegistry {
registry.Register(&onebot.OneBotExecutor{})
registry.Register(&dingtalk.DingTalkExecutor{})
registry.Register(&fwalert.FwalertExecutor{})
registry.Register(&rainyun.RainyunWorkorderExecutor{})
registry.Register(&rainyun.RainyunRunAwayExecutor{})
// 未来添加新规则类型时,在这里注册即可
globalExecutorRegistry = registry
+2
View File
@@ -7,6 +7,7 @@ import (
"github.com/ssdomei232/goodBaby/drivers/email"
"github.com/ssdomei232/goodBaby/drivers/github"
"github.com/ssdomei232/goodBaby/drivers/onebot"
"github.com/ssdomei232/goodBaby/drivers/rainyun"
)
var (
@@ -24,6 +25,7 @@ func InitValidatorRegistry() *ValidatorRegistry {
r.Register(&email.EmailAccountConfigValidator{})
r.Register(&github.GitHubAccountConfigValidator{})
r.Register(&onebot.OneBotAccountConfigValidator{})
r.Register(&rainyun.RainyunAccountConfigValidator{})
registry = r
})
+3
View File
@@ -9,6 +9,7 @@ import (
"github.com/ssdomei232/goodBaby/drivers/fwalert"
"github.com/ssdomei232/goodBaby/drivers/github"
"github.com/ssdomei232/goodBaby/drivers/onebot"
"github.com/ssdomei232/goodBaby/drivers/rainyun"
)
var (
@@ -29,6 +30,8 @@ func InitValidatorRegistry() *ValidatorRegistry {
r.Register(&dingtalk.DingTalkRuleValidator{})
r.Register(&github.GithubMakeRepositoryPublicRuleValidator{})
r.Register(&fwalert.FwalertRuleValidator{})
r.Register(&rainyun.RainyunWorkorderRuleValidator{})
r.Register(&rainyun.RainyunRunAwayRuleValidator{})
registry = r
})