Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfebcee886 | ||
|
|
9b656cebee | ||
|
|
0d89b7fe8f | ||
|
|
360169ee64 | ||
|
|
59cf99ae22 | ||
|
|
74ff904598 | ||
|
|
0a68d6358d | ||
|
|
df5165d4a3 | ||
|
|
868731c95e | ||
|
|
6990166dfc | ||
|
|
fa07a6f0e4 | ||
|
|
d428a7db2e | ||
|
|
8163d0d9d4 | ||
|
|
3831950780 | ||
|
|
9220cf82aa | ||
|
|
4a61527952 | ||
|
|
48bb6b8864 | ||
|
|
58b30fa41a | ||
|
|
2d73157a60 |
@@ -0,0 +1,34 @@
|
|||||||
|
name: Quality check
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "*"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
web-ui:
|
||||||
|
name: Web UI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: web/frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
working-directory: web/frontend
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
working-directory: web/frontend
|
||||||
@@ -33,3 +33,5 @@ node_modules/
|
|||||||
# 前端构建产物不入库,仅保留 .gitkeep 以满足 go:embed
|
# 前端构建产物不入库,仅保留 .gitkeep 以满足 go:embed
|
||||||
web/dist/*
|
web/dist/*
|
||||||
!web/dist/.gitkeep
|
!web/dist/.gitkeep
|
||||||
|
|
||||||
|
art/
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
# goodBaby v2
|
# goodBaby v2
|
||||||
|
|
||||||
摇篮系统 —— 为独居青年准备的「死人开关」(Dead Man's Switch)。
|
  
|
||||||
|
|
||||||
一个人在大城市生活,最怕的是出了事都没人知道。goodBaby 让你定期回来报一声平安;一旦超过设定的期限没有签到,它就会替你把预先托付的留言送出去:给家人的邮件、给朋友的 QQ / 钉钉消息、一条 B 站动态,或是公开你的 GitHub 仓库,把作品留给世界。
|
根据《中国心血管健康与疾病报告2023》,中国 每年 猝死总人数(含心源性和非心源性)或达80万-100万,18-35岁人群占比为38%,也就是说,每天18-35岁人群有近1000人猝死
|
||||||
|
|
||||||
|
伴随着中国人口老龄化加剧,年轻人的牵挂越来越多,面对着可能的猝死,我们或许真的需要做好准备
|
||||||
|
|
||||||
|
所以我写了goodBaby来解决这个问题,他和apple app store里的“死了么”形式上有些接近,不过goodBaby支持了更多好玩的通知方式,比如bilibili动态,github仓库,邮件,OneBot,还可以方便的自托管
|
||||||
|
|
||||||
[](https://app.rainyun.com/apps/rca/store/7125/cat_)
|
[](https://app.rainyun.com/apps/rca/store/7125/cat_)
|
||||||
|
|
||||||
@@ -17,6 +21,8 @@
|
|||||||
* 发送钉钉机器人消息
|
* 发送钉钉机器人消息
|
||||||
* 公开 GitHub 仓库
|
* 公开 GitHub 仓库
|
||||||
* **账号 (Account)**:集中管理第三方凭据,支持连通性测试,敏感字段(密码/Cookie/Token)不会回显
|
* **账号 (Account)**:集中管理第三方凭据,支持连通性测试,敏感字段(密码/Cookie/Token)不会回显
|
||||||
|
* **消息网关 (Gateway)**:为外部系统生成 Webhook 地址,投递进来的消息会触发绑定在该网关上的「网关规则」
|
||||||
|
* **网关规则**:与定时器规则分开存储、分开管理,规则里的消息字段(msg / message / body)会被投递内容替换
|
||||||
* **执行日志**:每次规则执行与提醒都有记录,规则支持手动测试
|
* **执行日志**:每次规则执行与提醒都有记录,规则支持手动测试
|
||||||
* **配置测试**:账号可一键测试连通性;规则可手动触发验证
|
* **配置测试**:账号可一键测试连通性;规则可手动触发验证
|
||||||
|
|
||||||
@@ -39,12 +45,10 @@ docker compose up -d
|
|||||||
docker run -d --name goodbaby -p 8088:8088 -v ./data:/app/data ghcr.io/ssdomei232/goodbaby:latest
|
docker run -d --name goodbaby -p 8088:8088 -v ./data:/app/data ghcr.io/ssdomei232/goodbaby:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
可用标签:`latest`、`v2.2.0`(完整版本)、`2.2`(次版本)、`2`(主版本)。
|
|
||||||
|
|
||||||
### 下载预编译二进制
|
### 下载预编译二进制
|
||||||
|
|
||||||
[Releases](https://github.com/ssdomei232/goodBaby/releases) 提供 Linux / Windows / macOS 的 amd64 与 arm64 产物,
|
[Releases](https://github.com/ssdomei232/goodBaby/releases) 提供 Linux / Windows / macOS 的 amd64 与 arm64 产物,
|
||||||
解压后直接运行即可(前端已内嵌,无需额外部署)。压缩包旁的 `checksums.txt` 可校验完整性:
|
解压后直接运行即可。压缩包旁的 `checksums.txt` 可校验完整性:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sha256sum -c checksums.txt --ignore-missing
|
sha256sum -c checksums.txt --ignore-missing
|
||||||
@@ -55,7 +59,7 @@ sha256sum -c checksums.txt --ignore-missing
|
|||||||
需要 Go 1.25+ 与 Node.js 20+:
|
需要 Go 1.25+ 与 Node.js 20+:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 构建前端(产物输出到 web/dist,会被 go:embed 打进二进制)
|
# 1. 构建前端
|
||||||
cd web/frontend && npm install && npm run build && cd ../..
|
cd web/frontend && npm install && npm run build && cd ../..
|
||||||
|
|
||||||
# 2. 构建后端
|
# 2. 构建后端
|
||||||
@@ -97,16 +101,21 @@ cd web/frontend && npm run dev
|
|||||||
|
|
||||||
### 管理员后台
|
### 管理员后台
|
||||||
|
|
||||||
**第一个注册的用户自动成为管理员**(老版本升级上来时,注册最早的用户会被自动提升)。
|
**第一个注册的用户自动成为管理员**。
|
||||||
|
|
||||||
管理员在「设置」页面会多出一个「系统配置」区块,可以直接修改并立即生效,无需重启:
|
### API Key
|
||||||
|
|
||||||
* 开放注册
|
每个用户在注册时会自动生成 API Key,可在 WebUI 的“设置”页面查看。调用需要认证的 API 时,在请求头中任选其一:
|
||||||
* 检查间隔(保存后会自动重排定时任务)
|
|
||||||
* 规则重试时长
|
|
||||||
* 日志保留条数
|
|
||||||
|
|
||||||
监听地址、数据库、会话密钥这类启动项不在后台开放,只能改配置文件。
|
```http
|
||||||
|
X-API-Key: gb_<your-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer gb_<your-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
## 数据库
|
## 数据库
|
||||||
|
|
||||||
@@ -126,12 +135,26 @@ cd web/frontend && npm run dev
|
|||||||
* [OneBot (QQ)](docs/onebot-config.md)
|
* [OneBot (QQ)](docs/onebot-config.md)
|
||||||
* [钉钉机器人](docs/dingtalk-config.md)
|
* [钉钉机器人](docs/dingtalk-config.md)
|
||||||
* [GitHub](docs/github-config.md)
|
* [GitHub](docs/github-config.md)
|
||||||
|
* [饭碗警告](docs/fwalert-config.md)
|
||||||
|
* [消息网关](docs/gateway-config.md)
|
||||||
|
|
||||||
## 开发:新增一种规则类型
|
## 开发:新增一种规则类型
|
||||||
|
|
||||||
|
driver 只负责把动作做出去:规则本体与关联账号由 `handler/runner` 组装成 `model.RuleTask` 后传进来,
|
||||||
|
driver 内部不访问数据库、不写日志,需要账号凭据时从 `task.Account` 里解析。
|
||||||
|
|
||||||
1. 在 `drivers/<name>/` 下实现:
|
1. 在 `drivers/<name>/` 下实现:
|
||||||
|
* `tool.go`:解析规则 / 账号配置的 `ParseXxx` 函数
|
||||||
* 规则验证器(实现 `ruleConfigChecker.RuleValidator`,`Meta()` 返回表单元数据)
|
* 规则验证器(实现 `ruleConfigChecker.RuleValidator`,`Meta()` 返回表单元数据)
|
||||||
* 执行器(实现 `runner.RuleExecutor`)
|
* 执行器(实现 `runner.RuleExecutor`,方法签名为 `Execute(ctx, *model.RuleTask)`)
|
||||||
|
* 对外动作(底层函数)保持原子:一次调用只做一件事,用 `retry.Do(ctx, ...)` 包裹重试
|
||||||
* 如需第三方凭据,再实现账号验证器(`accountConfigChecker.AccountValidator`,可选实现 `AccountTester` 支持连通性测试)
|
* 如需第三方凭据,再实现账号验证器(`accountConfigChecker.AccountValidator`,可选实现 `AccountTester` 支持连通性测试)
|
||||||
2. 在 `internal/ruleConfigChecker/reg.go`、`internal/accountConfigChecker/reg.go`、`handler/runner/interface.go` 中注册
|
2. 在 `internal/ruleConfigChecker/reg.go`、`internal/accountConfigChecker/reg.go`、`handler/runner/reg.go` 中注册
|
||||||
3. 前端无需改动 —— WebUI 会根据 `Meta()` 返回的字段描述自动渲染配置表单
|
3. 前端无需改动 —— WebUI 会根据 `Meta()` 返回的字段描述自动渲染配置表单
|
||||||
|
|
||||||
|
## 开发:新增一种消息网关
|
||||||
|
|
||||||
|
1. 在 `internal/gateway/` 下实现 `Gateway` 接口(`GetType()` / `Meta()` / `Deliver()`),
|
||||||
|
`Deliver` 拿到的 `Task` 里已经包含网关与待触发的规则,实现里不访问数据库
|
||||||
|
2. 在 `internal/gateway/reg.go` 中注册
|
||||||
|
3. 前端无需改动 —— 新建网关时的类型选项与说明来自 `Meta()`
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ func HandleTestAccount(c *gin.Context) {
|
|||||||
|
|
||||||
// HandleCheckDeleteAccount 检查删除账号请求
|
// HandleCheckDeleteAccount 检查删除账号请求
|
||||||
//
|
//
|
||||||
// 删除账号会同时删除相关规则,先请求该接口获取受影响的规则
|
// 删除账号会同时删除相关规则(含消息网关规则),先请求该接口获取受影响的规则
|
||||||
func HandleCheckDeleteAccount(c *gin.Context) {
|
func HandleCheckDeleteAccount(c *gin.Context) {
|
||||||
userInfo, err := user.GetUserInfoByGinCtx(c)
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,7 +201,16 @@ func HandleCheckDeleteAccount(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.OK(c, rules)
|
gatewayRules, err := getGatewayRulesByAccountID(accountID, userInfo.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取相关网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, gin.H{
|
||||||
|
"rules": rules,
|
||||||
|
"gateway_rules": gatewayRules,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDeleteAccount 删除账号及其关联规则
|
// HandleDeleteAccount 删除账号及其关联规则
|
||||||
@@ -218,6 +227,18 @@ func HandleDeleteAccount(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查账号是否归属请求用户
|
||||||
|
ownerUID, err := getAccountOwnerUID(accountID)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取账号所属用户失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownerUID != userInfo.ID {
|
||||||
|
response.Forbidden(c, "无权限操作该账号")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 删除相关规则
|
// 删除相关规则
|
||||||
rules, err := getRulesByAccountID(accountID, userInfo.ID)
|
rules, err := getRulesByAccountID(accountID, userInfo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -231,6 +252,19 @@ func HandleDeleteAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除相关的消息网关规则
|
||||||
|
gatewayRules, err := getGatewayRulesByAccountID(accountID, userInfo.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取相关网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, oneRule := range gatewayRules {
|
||||||
|
if err := rule.DeleteGatewayRuleByID(oneRule.ID, userInfo.ID); err != nil {
|
||||||
|
response.ServerError(c, "删除相关网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 删除账号
|
// 删除账号
|
||||||
gormDB, err := db.GetGormDB()
|
gormDB, err := db.GetGormDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -45,6 +45,20 @@ func getRulesByAccountID(accountID uint, uid uint) ([]*model.Rule, error) {
|
|||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getGatewayRulesByAccountID 获取账号相关的消息网关规则列表
|
||||||
|
func getGatewayRulesByAccountID(accountID uint, uid uint) ([]*model.GatewayRule, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := []*model.GatewayRule{}
|
||||||
|
if err := gormDB.Where("account_id = ? AND uid = ?", accountID, uid).Find(&rules).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rules, nil
|
||||||
|
}
|
||||||
|
|
||||||
// maskAccount 掩码账号配置中的敏感字段,避免 Cookie/密码原样返回给前端
|
// maskAccount 掩码账号配置中的敏感字段,避免 Cookie/密码原样返回给前端
|
||||||
func maskAccount(account model.Account) model.Account {
|
func maskAccount(account model.Account) model.Account {
|
||||||
fields := accountConfigChecker.InitValidatorRegistry().FieldsOf(account.Type)
|
fields := accountConfigChecker.InitValidatorRegistry().FieldsOf(account.Type)
|
||||||
@@ -59,3 +73,17 @@ func maskAccounts(accounts []model.Account) []model.Account {
|
|||||||
}
|
}
|
||||||
return masked
|
return masked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAccountOwnerUID 获取账号所属用户的 UID
|
||||||
|
func getAccountOwnerUID(accountID uint) (uint, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var account model.Account
|
||||||
|
if err := gormDB.Select("uid").Where("id = ?", accountID).First(&account).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return account.UID, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ func HandleGetOverview(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var ruleCount, accountCount int64
|
var ruleCount, gatewayCount, gatewayRuleCount, accountCount int64
|
||||||
gormDB.Model(&model.Rule{}).Where("uid = ?", userInfo.ID).Count(&ruleCount)
|
gormDB.Model(&model.Rule{}).Where("uid = ?", userInfo.ID).Count(&ruleCount)
|
||||||
|
gormDB.Model(&model.MessageGateway{}).Where("uid = ?", userInfo.ID).Count(&gatewayCount)
|
||||||
|
gormDB.Model(&model.GatewayRule{}).Where("uid = ?", userInfo.ID).Count(&gatewayRuleCount)
|
||||||
gormDB.Model(&model.Account{}).Where("uid = ?", userInfo.ID).Count(&accountCount)
|
gormDB.Model(&model.Account{}).Where("uid = ?", userInfo.ID).Count(&accountCount)
|
||||||
|
|
||||||
recentLogs := []model.ExecutionLog{}
|
recentLogs := []model.ExecutionLog{}
|
||||||
@@ -62,13 +64,15 @@ func HandleGetOverview(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
overview := gin.H{
|
overview := gin.H{
|
||||||
"timer_count": len(timers),
|
"timer_count": len(timers),
|
||||||
"enabled_timers": enabledCount,
|
"enabled_timers": enabledCount,
|
||||||
"triggered_count": triggeredCount,
|
"triggered_count": triggeredCount,
|
||||||
"rule_count": ruleCount,
|
"rule_count": ruleCount,
|
||||||
"account_count": accountCount,
|
"gateway_count": gatewayCount,
|
||||||
"server_time": now,
|
"gateway_rule_count": gatewayRuleCount,
|
||||||
"recent_logs": recentLogs,
|
"account_count": accountCount,
|
||||||
|
"server_time": now,
|
||||||
|
"recent_logs": recentLogs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if urgent != nil {
|
if urgent != nil {
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// Package gateway 提供消息网关的 HTTP 接口。
|
||||||
|
//
|
||||||
|
// 数据库读写都发生在这一层,真正的投递逻辑在 internal/gateway 里,
|
||||||
|
// 这里只负责鉴权、取数、组装投递任务。
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/ssdomei232/goodBaby/api/response"
|
||||||
|
"github.com/ssdomei232/goodBaby/api/user"
|
||||||
|
"github.com/ssdomei232/goodBaby/handler/db"
|
||||||
|
gatewaycore "github.com/ssdomei232/goodBaby/internal/gateway"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// webhookRequest 外部系统投递消息的请求体
|
||||||
|
type webhookRequest struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleList 获取当前用户的所有消息网关
|
||||||
|
func HandleList(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := []model.MessageGateway{}
|
||||||
|
if err := dbConn.Where("uid = ?", userInfo.ID).Order("id DESC").Find(&items).Error; err != nil {
|
||||||
|
response.ServerError(c, "获取网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCreate 创建消息网关
|
||||||
|
func HandleCreate(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req model.MessageGatewayRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "输入参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Name = strings.TrimSpace(req.Name)
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
response.FromError(c, err, "创建网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不填类型时用默认网关,填了就必须是已注册的类型
|
||||||
|
if req.Type == "" {
|
||||||
|
req.Type = model.GatewayTypeWebhook
|
||||||
|
}
|
||||||
|
if _, ok := gatewaycore.InitGatewayRegistry().Resolve(req.Type); !ok {
|
||||||
|
response.BadRequest(c, "不支持的消息网关类型: "+req.Type)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := gatewaycore.NewToken()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "生成网关 Token 失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "创建网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := model.MessageGateway{
|
||||||
|
UID: userInfo.ID,
|
||||||
|
Name: req.Name,
|
||||||
|
Type: req.Type,
|
||||||
|
Token: token,
|
||||||
|
CreateAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&item).Error; err != nil {
|
||||||
|
response.ServerError(c, "创建网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDelete 删除消息网关,绑定在它下面的规则一并删除
|
||||||
|
func HandleDelete(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "未登录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayID, err := parseID(c.Param("gatewayID"))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "网关 ID 格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "删除网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dbConn.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Where("gateway_id = ? AND uid = ?", gatewayID, userInfo.ID).
|
||||||
|
Delete(&model.GatewayRule{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Where("id = ? AND uid = ?", gatewayID, userInfo.ID).
|
||||||
|
Delete(&model.MessageGateway{}).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "删除网关失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, "网关已删除")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleWebhook 接收外部系统投递的消息,触发绑定在该网关上的规则
|
||||||
|
func HandleWebhook(c *gin.Context) {
|
||||||
|
var req webhookRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Message) == "" {
|
||||||
|
response.BadRequest(c, "message 不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "网关不可用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var target model.MessageGateway
|
||||||
|
if err := dbConn.Where("token = ?", c.Param("token")).First(&target).Error; err != nil {
|
||||||
|
response.Fail(c, http.StatusNotFound, "网关不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := []model.GatewayRule{}
|
||||||
|
if err := dbConn.Where("uid = ? AND gateway_id = ? AND enabled = ?", target.UID, target.ID, true).
|
||||||
|
Find(&rules).Error; err != nil {
|
||||||
|
response.ServerError(c, "读取网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 投递需要在 HTTP 请求内返回结果,因此用较短的超时
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := gatewaycore.InitGatewayRegistry().Deliver(ctx, target.Type, &gatewaycore.Task{
|
||||||
|
Gateway: &target,
|
||||||
|
Rules: rules,
|
||||||
|
Message: gatewaycore.Message{
|
||||||
|
Title: strings.TrimSpace(req.Title),
|
||||||
|
Content: strings.TrimSpace(req.Message),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, result)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// parseID 解析 URL 里的数字 ID
|
||||||
|
func parseID(raw string) (uint, error) {
|
||||||
|
id, err := strconv.ParseUint(raw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint(id), nil
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ func HandleGetLogs(c *gin.Context) {
|
|||||||
if ruleID := c.Query("rule_id"); ruleID != "" {
|
if ruleID := c.Query("rule_id"); ruleID != "" {
|
||||||
query = query.Where("rule_id = ?", ruleID)
|
query = query.Where("rule_id = ?", ruleID)
|
||||||
}
|
}
|
||||||
|
if gatewayRuleID := c.Query("gateway_rule_id"); gatewayRuleID != "" {
|
||||||
|
query = query.Where("gateway_rule_id = ?", gatewayRuleID)
|
||||||
|
}
|
||||||
if success := c.Query("success"); success == "true" || success == "false" {
|
if success := c.Query("success"); success == "true" || success == "false" {
|
||||||
query = query.Where("success = ?", success == "true")
|
query = query.Where("success = ?", success == "true")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import (
|
|||||||
"github.com/ssdomei232/goodBaby/api/user"
|
"github.com/ssdomei232/goodBaby/api/user"
|
||||||
"github.com/ssdomei232/goodBaby/configs"
|
"github.com/ssdomei232/goodBaby/configs"
|
||||||
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
|
"github.com/ssdomei232/goodBaby/internal/accountConfigChecker"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/gateway"
|
||||||
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
|
"github.com/ssdomei232/goodBaby/internal/ruleConfigChecker"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleGetProviders 返回所有账号类型与规则类型的元数据
|
// HandleGetProviders 返回所有账号类型、规则类型与消息网关类型的元数据
|
||||||
//
|
//
|
||||||
// 前端据此动态渲染配置表单,新增驱动时前端无需改动。
|
// 前端据此动态渲染配置表单,新增驱动时前端无需改动。
|
||||||
func HandleGetProviders(c *gin.Context) {
|
func HandleGetProviders(c *gin.Context) {
|
||||||
response.OK(c, gin.H{
|
response.OK(c, gin.H{
|
||||||
"accounts": accountConfigChecker.InitValidatorRegistry().Metas(),
|
"accounts": accountConfigChecker.InitValidatorRegistry().Metas(),
|
||||||
"rules": ruleConfigChecker.InitValidatorRegistry().Metas(),
|
"rules": ruleConfigChecker.InitValidatorRegistry().Metas(),
|
||||||
|
"gateways": gateway.InitGatewayRegistry().Metas(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ func NotFound(c *gin.Context, msg string) { Fail(c, http.StatusNotFound, msg) }
|
|||||||
// ServerError 服务端错误
|
// ServerError 服务端错误
|
||||||
func ServerError(c *gin.Context, msg string) { Fail(c, http.StatusInternalServerError, msg) }
|
func ServerError(c *gin.Context, msg string) { Fail(c, http.StatusInternalServerError, msg) }
|
||||||
|
|
||||||
|
// Forbidden 无权限
|
||||||
|
func Forbidden(c *gin.Context, msg string) { Fail(c, http.StatusForbidden, msg) }
|
||||||
|
|
||||||
// FromError 按错误类型选择合适的状态码:
|
// FromError 按错误类型选择合适的状态码:
|
||||||
// 参数校验错误返回 400,其余返回 500
|
// 参数校验错误返回 400,其余返回 500
|
||||||
func FromError(c *gin.Context, err error, fallback string) {
|
func FromError(c *gin.Context, err error, fallback string) {
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/ssdomei232/goodBaby/api/response"
|
||||||
|
"github.com/ssdomei232/goodBaby/api/user"
|
||||||
|
"github.com/ssdomei232/goodBaby/handler/db"
|
||||||
|
"github.com/ssdomei232/goodBaby/handler/runner"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/gateway"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleGetAllGatewayRules 获取用户的消息网关规则,支持按 gateway_id 过滤
|
||||||
|
//
|
||||||
|
// 消息网关规则和定时器规则是两张表,互不影响,因此这里单独一组接口。
|
||||||
|
func HandleGetAllGatewayRules(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "服务器内部错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := gormDB.Where("uid = ?", userInfo.ID)
|
||||||
|
if raw := c.Query("gateway_id"); raw != "" {
|
||||||
|
gatewayID, err := parseID(raw)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "gateway_id 格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query = query.Where("gateway_id = ?", gatewayID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules := []model.GatewayRule{}
|
||||||
|
if err := query.Order("id DESC").Find(&rules).Error; err != nil {
|
||||||
|
response.ServerError(c, "获取网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, maskGatewayRules(rules))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCreateGatewayRule 创建消息网关规则
|
||||||
|
func HandleCreateGatewayRule(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req model.GatewayRuleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "输入参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRule := model.GatewayRule{
|
||||||
|
UID: userInfo.ID,
|
||||||
|
Name: req.Name,
|
||||||
|
GatewayID: req.GatewayID,
|
||||||
|
AccountID: req.AccountID,
|
||||||
|
Type: req.Type,
|
||||||
|
ConfigJson: req.ConfigJson,
|
||||||
|
Enabled: boolOr(req.Enabled, true),
|
||||||
|
CreateAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateGatewayRule(&req, &newRule); err != nil {
|
||||||
|
response.FromError(c, err, "创建网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "服务器内部错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gormDB.Create(&newRule).Error; err != nil {
|
||||||
|
response.ServerError(c, "创建网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, maskGatewayRule(newRule))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEditGatewayRule 编辑消息网关规则
|
||||||
|
func HandleEditGatewayRule(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleID, err := parseID(c.Param("ruleID"))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "规则 ID 格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := findGatewayRule(ruleID, userInfo.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, "网关规则不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req model.GatewayRuleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "输入参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := *existing
|
||||||
|
updated.Name = req.Name
|
||||||
|
updated.Type = req.Type
|
||||||
|
updated.GatewayID = req.GatewayID
|
||||||
|
updated.AccountID = req.AccountID
|
||||||
|
updated.Enabled = boolOr(req.Enabled, existing.Enabled)
|
||||||
|
// 前端提交的敏感字段可能是掩码占位符,用旧配置补回
|
||||||
|
updated.ConfigJson = unmaskRuleConfig(req.Type, req.ConfigJson, existing.ConfigJson)
|
||||||
|
|
||||||
|
req.ConfigJson = updated.ConfigJson
|
||||||
|
if err := validateGatewayRule(&req, &updated); err != nil {
|
||||||
|
response.FromError(c, err, "更新网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "服务器内部错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gormDB.Save(&updated).Error; err != nil {
|
||||||
|
response.ServerError(c, "更新网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, maskGatewayRule(updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDeleteGatewayRule 删除消息网关规则
|
||||||
|
func HandleDeleteGatewayRule(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleID, err := parseID(c.Param("ruleID"))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "规则 ID 格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := findGatewayRule(ruleID, userInfo.ID); err != nil {
|
||||||
|
response.NotFound(c, "网关规则不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DeleteGatewayRuleByID(ruleID, userInfo.ID); err != nil {
|
||||||
|
response.ServerError(c, "删除网关规则失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, "网关规则删除成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleTestGatewayRule 立即执行一次网关规则,用于验证配置
|
||||||
|
//
|
||||||
|
// 与投递不同:这里用规则里保存的配置,不覆盖外部消息。
|
||||||
|
func HandleTestGatewayRule(c *gin.Context) {
|
||||||
|
userInfo, err := user.GetUserInfoByGinCtx(c)
|
||||||
|
if err != nil {
|
||||||
|
response.Unauthorized(c, "获取用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleID, err := parseID(c.Param("ruleID"))
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "规则 ID 格式错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
target, err := findGatewayRule(ruleID, userInfo.ID)
|
||||||
|
if err != nil {
|
||||||
|
response.NotFound(c, "网关规则不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用一条示例消息走一次真实的投递覆盖,让用户看到规则最终会发出什么内容
|
||||||
|
if configJSON, applied, err := gateway.ApplyMessage(target.ConfigJson, gateway.TestMessage); err == nil && applied {
|
||||||
|
target.ConfigJson = configJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), retry.TestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := runner.ExecuteGatewayRuleWithContext(ctx, target, model.TriggerManual); err != nil {
|
||||||
|
response.BadRequest(c, fmt.Sprintf("规则执行失败: %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OK(c, "规则执行成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateGatewayRule 校验网关规则的通用字段、关联对象与类型专属配置
|
||||||
|
func validateGatewayRule(req *model.GatewayRuleRequest, rule *model.GatewayRule) error {
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkGatewayExists(rule.GatewayID, rule.UID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleMeta, err := ruleMetaOf(req.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题/内容由投递请求提供,页面不填,这里补占位值后再校验
|
||||||
|
rule.ConfigJson = meta.FillGatewayMessages(rule.ConfigJson, ruleMeta.Fields)
|
||||||
|
if err := validateRuleConfig(req.Type, rule.ConfigJson); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID, err := checkAccount(rule.AccountID, rule.UID, ruleMeta.AccountType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rule.AccountID = accountID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -162,6 +162,16 @@ func HandleDeleteRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ownerUID, err := getRuleOwnerUID(ruleID)
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取规则所属用户失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ownerUID != userInfo.ID {
|
||||||
|
response.Forbidden(c, "无权限操作该规则")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := DeleteRuleByID(ruleID, userInfo.ID); err != nil {
|
if err := DeleteRuleByID(ruleID, userInfo.ID); err != nil {
|
||||||
response.ServerError(c, "删除规则失败")
|
response.ServerError(c, "删除规则失败")
|
||||||
return
|
return
|
||||||
@@ -209,19 +219,24 @@ func validateRule(req *model.RuleRequest, rule *model.Rule) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(req.Type)
|
// 检查关联的 Timer 是否存在、账号是否可用
|
||||||
if !ok {
|
if err := checkTimerExists(rule.TimerID, rule.UID); err != nil {
|
||||||
return model.ErrValidation(fmt.Sprintf("不支持的规则类型: %s", req.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查关联的 Timer 与账号是否存在且属于当前用户
|
|
||||||
if err := checkRuleConfigAccountAndTimerExist(rule, ruleMeta.AccountType); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ruleConfigChecker.InitValidatorRegistry().Validate(req.Type, rule.ConfigJson); err != nil {
|
ruleMeta, err := ruleMetaOf(req.Type)
|
||||||
return model.ErrValidation(fmt.Sprintf("规则配置验证失败: %s", err.Error()))
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateRuleConfig(req.Type, rule.ConfigJson); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID, err := checkAccount(rule.AccountID, rule.UID, ruleMeta.AccountType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rule.AccountID = accountID
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func boolOr(v *bool, fallback bool) bool {
|
|||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
// findRule 查找属于该用户的规则
|
// findRule 查找属于该用户的定时器规则
|
||||||
func findRule(ruleID, uid uint) (*model.Rule, error) {
|
func findRule(ruleID, uid uint) (*model.Rule, error) {
|
||||||
gormDB, err := db.GetGormDB()
|
gormDB, err := db.GetGormDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,7 +39,21 @@ func findRule(ruleID, uid uint) (*model.Rule, error) {
|
|||||||
return &target, nil
|
return &target, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRuleByID 根据 ID 删除规则
|
// findGatewayRule 查找属于该用户的消息网关规则
|
||||||
|
func findGatewayRule(ruleID, uid uint) (*model.GatewayRule, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var target model.GatewayRule
|
||||||
|
if err := gormDB.Where("id = ? AND uid = ?", ruleID, uid).First(&target).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &target, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRuleByID 根据 ID 删除定时器规则
|
||||||
func DeleteRuleByID(id uint, uid uint) error {
|
func DeleteRuleByID(id uint, uid uint) error {
|
||||||
gormDB, err := db.GetGormDB()
|
gormDB, err := db.GetGormDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,50 +63,99 @@ func DeleteRuleByID(id uint, uid uint) error {
|
|||||||
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.Rule{}).Error
|
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.Rule{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkRuleConfigAccountAndTimerExist 检查关联的 Timer 与账号是否存在且归属当前用户
|
// DeleteGatewayRuleByID 根据 ID 删除消息网关规则
|
||||||
//
|
func DeleteGatewayRuleByID(id uint, uid uint) error {
|
||||||
// requiredAccountType 为空表示该规则类型不需要账号。
|
gormDB, err := db.GetGormDB()
|
||||||
func checkRuleConfigAccountAndTimerExist(rule *model.Rule, requiredAccountType string) error {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gormDB.Where("id = ? AND uid = ?", id, uid).Delete(&model.GatewayRule{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTimerExists 检查关联的 Timer 是否存在且归属当前用户
|
||||||
|
func checkTimerExists(timerID, uid uint) error {
|
||||||
gormDB, err := db.GetGormDB()
|
gormDB, err := db.GetGormDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var count int64
|
var count int64
|
||||||
if err := gormDB.Model(&model.Timer{}).
|
if err := gormDB.Model(&model.Timer{}).Where("id = ? AND uid = ?", timerID, uid).
|
||||||
Where("id = ? AND uid = ?", rule.TimerID, rule.UID).Count(&count).Error; err != nil {
|
Count(&count).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return model.ErrValidation("关联的 Timer 不存在")
|
return model.ErrValidation("关联的 Timer 不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
if requiredAccountType == "" {
|
|
||||||
// 该类型不需要账号,忽略前端可能传来的 account_id
|
|
||||||
rule.AccountID = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.AccountID == 0 {
|
|
||||||
return model.ErrValidation("该规则类型必须关联一个账号")
|
|
||||||
}
|
|
||||||
|
|
||||||
var account model.Account
|
|
||||||
if err := gormDB.Where("id = ? AND uid = ?", rule.AccountID, rule.UID).First(&account).Error; err != nil {
|
|
||||||
return model.ErrValidation("关联的账号不存在")
|
|
||||||
}
|
|
||||||
if account.Type != requiredAccountType {
|
|
||||||
return model.ErrValidation(fmt.Sprintf("规则需要 %s 类型的账号,但关联的是 %s", requiredAccountType, account.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// maskRule 掩码规则配置中的敏感字段
|
// checkGatewayExists 检查关联的消息网关是否存在且归属当前用户
|
||||||
func maskRule(target model.Rule) model.Rule {
|
func checkGatewayExists(gatewayID, uid uint) error {
|
||||||
if ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(target.Type); ok {
|
gormDB, err := db.GetGormDB()
|
||||||
target.ConfigJson = meta.Mask(target.ConfigJson, ruleMeta.Fields)
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := gormDB.Model(&model.MessageGateway{}).Where("id = ? AND uid = ?", gatewayID, uid).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return model.ErrValidation("关联的消息网关不存在")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAccount 校验规则关联的账号存在、归属当前用户且类型匹配。
|
||||||
|
//
|
||||||
|
// requiredType 为空表示该规则类型不需要账号,此时返回 0,调用方据此清掉 account_id。
|
||||||
|
func checkAccount(accountID, uid uint, requiredType string) (uint, error) {
|
||||||
|
if requiredType == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if accountID == 0 {
|
||||||
|
return 0, model.ErrValidation("该规则类型必须关联一个账号")
|
||||||
|
}
|
||||||
|
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var account model.Account
|
||||||
|
if err := gormDB.Where("id = ? AND uid = ?", accountID, uid).First(&account).Error; err != nil {
|
||||||
|
return 0, model.ErrValidation("关联的账号不存在")
|
||||||
|
}
|
||||||
|
if account.Type != requiredType {
|
||||||
|
return 0, model.ErrValidation(
|
||||||
|
fmt.Sprintf("规则需要 %s 类型的账号,但关联的是 %s", requiredType, account.Type))
|
||||||
|
}
|
||||||
|
return accountID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ruleMetaOf 取规则类型的元数据,不支持的类型返回校验错误
|
||||||
|
func ruleMetaOf(ruleType string) (meta.RuleMeta, error) {
|
||||||
|
ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(ruleType)
|
||||||
|
if !ok {
|
||||||
|
return meta.RuleMeta{}, model.ErrValidation(fmt.Sprintf("不支持的规则类型: %s", ruleType))
|
||||||
|
}
|
||||||
|
return ruleMeta, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRuleConfig 校验类型专属的规则配置
|
||||||
|
func validateRuleConfig(ruleType, configJSON string) error {
|
||||||
|
if err := ruleConfigChecker.InitValidatorRegistry().Validate(ruleType, configJSON); err != nil {
|
||||||
|
return model.ErrValidation(fmt.Sprintf("规则配置验证失败: %s", err.Error()))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskRule 掩码定时器规则配置中的敏感字段
|
||||||
|
func maskRule(target model.Rule) model.Rule {
|
||||||
|
target.ConfigJson = maskConfig(target.Type, target.ConfigJson)
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,3 +166,53 @@ func maskRules(rules []model.Rule) []model.Rule {
|
|||||||
}
|
}
|
||||||
return masked
|
return masked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maskGatewayRule 掩码消息网关规则配置中的敏感字段
|
||||||
|
func maskGatewayRule(target model.GatewayRule) model.GatewayRule {
|
||||||
|
target.ConfigJson = maskConfig(target.Type, target.ConfigJson)
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskGatewayRules(rules []model.GatewayRule) []model.GatewayRule {
|
||||||
|
masked := make([]model.GatewayRule, 0, len(rules))
|
||||||
|
for _, r := range rules {
|
||||||
|
masked = append(masked, maskGatewayRule(r))
|
||||||
|
}
|
||||||
|
return masked
|
||||||
|
}
|
||||||
|
|
||||||
|
// maskConfig 掩码规则配置中的敏感字段
|
||||||
|
func maskConfig(ruleType, configJSON string) string {
|
||||||
|
if ruleMeta, ok := ruleConfigChecker.InitValidatorRegistry().MetaOf(ruleType); ok {
|
||||||
|
return meta.Mask(configJSON, ruleMeta.Fields)
|
||||||
|
}
|
||||||
|
return configJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRuleOwnerUID 获取定时器规则所属用户的 UID
|
||||||
|
func getRuleOwnerUID(ruleID uint) (uint, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var target model.Rule
|
||||||
|
if err := gormDB.Select("uid").Where("id = ?", ruleID).First(&target).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return target.UID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGatewayRuleOwnerUID 获取消息网关规则所属用户的 UID
|
||||||
|
func getGatewayRuleOwnerUID(ruleID uint) (uint, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var target model.GatewayRule
|
||||||
|
if err := gormDB.Select("uid").Where("id = ?", ruleID).First(&target).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return target.UID, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,3 +70,17 @@ func signTimer(timer *model.Timer) error {
|
|||||||
timer.Triggered = false
|
timer.Triggered = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTimerOwnerUID 获取 Timer 的所属用户 ID
|
||||||
|
func getTimerOwnerUID(timerID uint) (uint, error) {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var timer model.Timer
|
||||||
|
if err := gormDB.Select("uid").Where("id = ?", timerID).First(&timer).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return timer.UID, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
|
||||||
"github.com/ssdomei232/goodBaby/handler/db"
|
"github.com/ssdomei232/goodBaby/handler/db"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -48,11 +50,44 @@ func createUser(u *model.User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.Password = hashedPassword
|
u.Password = hashedPassword
|
||||||
|
if u.APIKey == "" {
|
||||||
|
u.APIKey, err = generateAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
u.CreateAt = time.Now().Unix()
|
u.CreateAt = time.Now().Unix()
|
||||||
|
|
||||||
return gormDB.Create(u).Error
|
return gormDB.Create(u).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateAPIKey() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "gb_" + hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAPIKey(user *model.User) error {
|
||||||
|
if user.APIKey != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key, err := generateAPIKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gormDB.Model(&model.User{}).Where("id = ? AND (api_key IS NULL OR api_key = '')", user.ID).Update("api_key", key).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.APIKey = key
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsUsernameTaken 用户名是否已被占用
|
// IsUsernameTaken 用户名是否已被占用
|
||||||
func IsUsernameTaken(username string) (bool, error) {
|
func IsUsernameTaken(username string) (bool, error) {
|
||||||
gormDB, err := db.GetGormDB()
|
gormDB, err := db.GetGormDB()
|
||||||
@@ -134,6 +169,9 @@ func GetUserByID(id uint) (*model.User, error) {
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ensureAPIKey(&user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +214,10 @@ func validateNotifyConfig(raw *string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var config struct {
|
// 复用钉钉驱动里的解析逻辑,避免两边对配置格式的理解不一致
|
||||||
AccessToken string `json:"access_token"`
|
config, err := dingtalk.ParseAccountConfig(*raw)
|
||||||
}
|
if err != nil {
|
||||||
if err := json.Unmarshal([]byte(*raw), &config); err != nil {
|
return err
|
||||||
return fmt.Errorf("钉钉配置 JSON 格式错误: %v", err)
|
|
||||||
}
|
}
|
||||||
if config.AccessToken == "" {
|
if config.AccessToken == "" {
|
||||||
return fmt.Errorf("钉钉配置中 access_token 不能为空")
|
return fmt.Errorf("钉钉配置中 access_token 不能为空")
|
||||||
|
|||||||
@@ -210,6 +210,27 @@ func HandleUpdateNotifyConfig(c *gin.Context) {
|
|||||||
// AuthMiddleware 认证中间件,同时把当前用户放进 context 供后续 handler 复用
|
// AuthMiddleware 认证中间件,同时把当前用户放进 context 供后续 handler 复用
|
||||||
func AuthMiddleware() gin.HandlerFunc {
|
func AuthMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
apiKey := c.GetHeader("X-API-Key")
|
||||||
|
if apiKey == "" {
|
||||||
|
auth := c.GetHeader("Authorization")
|
||||||
|
const prefix = "Bearer "
|
||||||
|
if len(auth) > len(prefix) && auth[:len(prefix)] == prefix {
|
||||||
|
apiKey = auth[len(prefix):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if apiKey != "" {
|
||||||
|
gormDB, err := db.GetGormDB()
|
||||||
|
if err == nil {
|
||||||
|
var user model.User
|
||||||
|
if gormDB.Where("api_key = ?", apiKey).First(&user).Error == nil {
|
||||||
|
c.Set(contextKey, &user)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.AbortWith(c, http.StatusUnauthorized, "API Key 无效")
|
||||||
|
return
|
||||||
|
}
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
uid, ok := session.Get("uid").(uint)
|
uid, ok := session.Get("uid").(uint)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -82,8 +82,11 @@ volumes:
|
|||||||
|
|
||||||
两种数据库之间**没有**自动数据迁移。切换驱动后是一个全新的空库,需要重新注册账号并配置规则。
|
两种数据库之间**没有**自动数据迁移。切换驱动后是一个全新的空库,需要重新注册账号并配置规则。
|
||||||
|
|
||||||
如果要搬运已有数据,得自己从 SQLite 导出再导入 PostgreSQL(表结构一致:`users` / `timers` / `rules` / `accounts` / `execution_logs`)。
|
如果要搬运已有数据,得自己从 SQLite 导出再导入 PostgreSQL(表结构一致:`users` / `timers` / `rules` / `gateway_rules` / `message_gateways` / `accounts` / `execution_logs`)。
|
||||||
|
|
||||||
## 建表
|
## 建表
|
||||||
|
|
||||||
首次启动会自动执行 `AutoMigrate` 建表并补齐新增字段,不需要手动执行 SQL。
|
首次启动会自动执行 `AutoMigrate` 建表并补齐新增字段,不需要手动执行 SQL。
|
||||||
|
|
||||||
|
升级到 v2.2.0 时,如果 `rules` 表里还有靠 `gateway_id` 标识的历史消息网关规则,
|
||||||
|
启动阶段会自动把它们搬到 `gateway_rules` 表,不需要手工处理。
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 饭碗警告配置
|
||||||
|
|
||||||
|
规则类型: `fwalert`。
|
||||||
|
|
||||||
|
定时器到期后,请求饭碗警告的 Webhook 地址
|
||||||
|
|
||||||
|
饭碗警告: 比较简单的(不用实名认证)支持电话和短信通知的第三方平台: `https://fwalert.com/`,价格不算最低,但比较方便
|
||||||
|
|
||||||
|
## 规则配置 (Rule Config)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"webhook_url": "https://fwalert.com/xxx-xxx-xxx-xxx-xxx",
|
||||||
|
"msg": "消息内容"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 饭碗警告平台配置
|
||||||
|
|
||||||
|
goodbaby 在请求饭碗警告时将`msg`放在请求体中,参照下面和官方教程设置:
|
||||||
|
|
||||||
|
| 字段 | 值 |
|
||||||
|
| --- | --- |
|
||||||
|
| `变量名` | `message` |
|
||||||
|
| `变量来源` | `请求体(body)` |
|
||||||
|
| `提取规则` | `序列化数据` |
|
||||||
|
| `键` | `message` |
|
||||||
|
| `通知简述` | 随便填 |
|
||||||
|
| `通知正文` | `{{message}}` |
|
||||||
|
| `联系方式类型` | 参照官方文档添加并选择 |
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 消息网关配置
|
||||||
|
|
||||||
|
消息网关让外部系统来触发 goodBaby 的动作:为网关生成一个 Webhook 地址,
|
||||||
|
外部系统往这个地址 POST 一条消息,绑定在该网关上的「网关规则」就会被执行。
|
||||||
|
|
||||||
|
## 创建网关
|
||||||
|
|
||||||
|
在 WebUI 的「消息网关」页面点击「新建网关」:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 网关类型 | 目前只有 `webhook`,新类型由 `internal/gateway` 注册后自动出现在这里 |
|
||||||
|
| 网关名称 | 便于识别的名字,例如「生产环境告警」 |
|
||||||
|
|
||||||
|
创建后会得到一个地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://<你的域名>/api/v1/gateways/gw_xxxxxxxx/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
Token 由服务端随机生成,无法在页面上修改;删除网关会连同它的网关规则一起删除。
|
||||||
|
|
||||||
|
## 投递消息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://<你的域名>/api/v1/gateways/gw_xxxxxxxx/webhook \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"title": "数据库告警", "message": "主库连接数超过 90%"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `message` | 是 | 消息正文,会覆盖网关规则配置里的消息字段 |
|
||||||
|
| `title` | 否 | 消息标题,规则配置里存在 `title` 字段时会被替换 |
|
||||||
|
|
||||||
|
响应里会给出本次触发的规则总数与失败明细:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": 200, "data": { "total": 2, "failed": ["发送邮件: 连接 SMTP 服务器失败"] } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 网关规则
|
||||||
|
|
||||||
|
网关规则在「网关规则」页面维护,和定时器规则是两张表、两个页面,互不影响。
|
||||||
|
只需配置「发给谁」这类固定信息:名称 / 类型 / 关联账号 / 类型专属配置 / 启用。
|
||||||
|
|
||||||
|
**消息标题与内容不用在规则里预先写**:邮件标题、邮件正文、QQ 消息内容、钉钉标题与内容、
|
||||||
|
B 站动态与私信内容、饭碗警告内容这些字段在「网关规则」页面上不会让用户填写
|
||||||
|
(页面上只显示一行提示),投递时统一由请求里的 `title` / `message` 填充。
|
||||||
|
|
||||||
|
没有消息字段的规则(例如「公开 GitHub 仓库」「删除阿里云 DNS 记录」)也能挂到网关上,
|
||||||
|
这类规则固定按自己的配置执行,投递的消息只起到触发器的作用。
|
||||||
|
|
||||||
|
规则编辑页的「测试」按钮会用一条示例消息走一次真实的投递覆盖,
|
||||||
|
方便你在执行日志里确认最终发出的内容。
|
||||||
@@ -37,7 +37,7 @@ git push origin v2.2.0
|
|||||||
|
|
||||||
编译时通过 ldflags 注入,启动日志里能看到:
|
编译时通过 ldflags 注入,启动日志里能看到:
|
||||||
|
|
||||||
```
|
```log
|
||||||
goodBaby v2.2.0 (a1b2c3d, 2026-07-27T07:00:00Z) 启动中...
|
goodBaby v2.2.0 (a1b2c3d, 2026-07-27T07:00:00Z) 启动中...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package alidns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alibabacloud-go/alidns-20150109/v5/client"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AliDNSAccountConfigValidator struct{}
|
||||||
|
|
||||||
|
func (v *AliDNSAccountConfigValidator) GetType() string { return AccountType }
|
||||||
|
|
||||||
|
func (v *AliDNSAccountConfigValidator) Validate(config string) error {
|
||||||
|
cfg, err := ParseAccountConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cfg.AK == "" || cfg.SK == "" {
|
||||||
|
return fmt.Errorf("阿里云账号配置不完整")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AliDNSAccountConfigValidator) Test(config string) error {
|
||||||
|
cfg, err := ParseAccountConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
aliDNSClient, err := newClient(cfg.AK, cfg.SK)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = aliDNSClient.DescribeDnsProductInstances(&client.DescribeDnsProductInstancesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AliDNSAccountConfigValidator) Meta() meta.AccountMeta {
|
||||||
|
return meta.AccountMeta{
|
||||||
|
Type: AccountType,
|
||||||
|
Label: "阿里云",
|
||||||
|
Description: "阿里云国内站",
|
||||||
|
Fields: []meta.Field{
|
||||||
|
{Key: "ak", Label: "Access Key", Type: meta.FieldPassword, Required: true, Secret: true, Placeholder: "xxxxxxxxx"},
|
||||||
|
{Key: "sk", Label: "Secret Key", Type: meta.FieldPassword, Required: true, Secret: true, Placeholder: "xxxxxxxxx"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package alidns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
alidns "github.com/alibabacloud-go/alidns-20150109/v5/client"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 := client.DeleteDomainRecord(&alidns.DeleteDomainRecordRequest{
|
||||||
|
RecordId: &config.RecordID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("删除阿里云DNS记录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package alidns
|
||||||
|
|
||||||
|
const (
|
||||||
|
AccountType = "alidns"
|
||||||
|
RuleTypeDeleteRecord = "alidns-delete-record"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AliDNSAccount struct {
|
||||||
|
AK string `json:"ak"`
|
||||||
|
SK string `json:"sk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteRecordConfig struct {
|
||||||
|
RecordID string `json:"record_id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package alidns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 阿里云删除记录规则验证器
|
||||||
|
type AliDNSDeleteRecordRuleValidator struct{}
|
||||||
|
|
||||||
|
func (v *AliDNSDeleteRecordRuleValidator) GetType() string {
|
||||||
|
return RuleTypeDeleteRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AliDNSDeleteRecordRuleValidator) Validate(configJSON string) error {
|
||||||
|
config, err := ParseDeleteRecordConfig(configJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.RecordID == "" {
|
||||||
|
return fmt.Errorf("阿里云删除记录规则配置中 record_id 不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AliDNSDeleteRecordRuleValidator) Meta() meta.RuleMeta {
|
||||||
|
return meta.RuleMeta{
|
||||||
|
Type: RuleTypeDeleteRecord,
|
||||||
|
Label: "删除阿里云DNS解析记录",
|
||||||
|
Description: "触发时删除阿里云DNS解析记录",
|
||||||
|
AccountType: AccountType,
|
||||||
|
Fields: []meta.Field{
|
||||||
|
{Key: "record_id", Label: "记录ID", Type: meta.FieldString, Required: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package alidns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteAliDNSRecordExecutor 删除阿里云DNS记录执行器
|
||||||
|
type DeleteAliDNSRecordExecutor struct{}
|
||||||
|
|
||||||
|
func (e *DeleteAliDNSRecordExecutor) GetType() string {
|
||||||
|
return RuleTypeDeleteRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DeleteAliDNSRecordExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
|
account, err := ParseAccountConfig(task.AccountConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := ParseDeleteRecordConfig(task.Rule.ConfigJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeleteRecord(ctx, account, config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseAccountConfig 解析阿里云账号配置
|
||||||
|
func ParseAccountConfig(config string) (*AliDNSAccount, error) {
|
||||||
|
if config == "" {
|
||||||
|
return nil, fmt.Errorf("解析阿里云账号配置失败: 规则没有关联账号")
|
||||||
|
}
|
||||||
|
|
||||||
|
var account AliDNSAccount
|
||||||
|
if err := json.Unmarshal([]byte(config), &account); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析阿里云账号配置失败: %v", err)
|
||||||
|
}
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, fmt.Errorf("创建阿里云凭据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return alidns.NewClient(&openapi.Config{
|
||||||
|
Credential: akCredential,
|
||||||
|
Endpoint: tea.String("alidns.aliyuncs.com"),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package bilibili
|
package bilibili
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
@@ -15,7 +14,7 @@ func (v *BilibiliAccountConfigValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *BilibiliAccountConfigValidator) Validate(config string) error {
|
func (v *BilibiliAccountConfigValidator) Validate(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -29,7 +28,7 @@ func (v *BilibiliAccountConfigValidator) Validate(config string) error {
|
|||||||
|
|
||||||
// Test 用 cookie 拉一次账号信息,验证 cookie 是否还有效
|
// Test 用 cookie 拉一次账号信息,验证 cookie 是否还有效
|
||||||
func (v *BilibiliAccountConfigValidator) Test(config string) error {
|
func (v *BilibiliAccountConfigValidator) Test(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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,32 +6,22 @@ import (
|
|||||||
|
|
||||||
"github.com/CuteReimu/bilibili/v2"
|
"github.com/CuteReimu/bilibili/v2"
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendBiliDynamicMsg 在 Bilibili 动态发送消息
|
// SendDynamic 用给定账号发送一条 B 站动态
|
||||||
//
|
//
|
||||||
// 暂时没有处理 429 和 403 的区别对待
|
// 暂时没有处理 429 和 403 的区别对待
|
||||||
func SendBiliDynamicMsg(ctx context.Context, rule *model.Rule) error {
|
func SendDynamic(ctx context.Context, account *BiliAccount, config *BiliDynamicConfig) error {
|
||||||
biliClient, err := getBiliClient(rule)
|
client := newClient(account.RawCookies)
|
||||||
if err != nil {
|
params := bilibili.CreateDynamicParam{
|
||||||
return fmt.Errorf("获取B站客户端失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
biliDynamicConfig, err := getBiliDynamicConfig(rule)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取B站动态配置失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicParams := bilibili.CreateDynamicParam{
|
|
||||||
DynamicId: 0,
|
DynamicId: 0,
|
||||||
Type: 4,
|
Type: 4,
|
||||||
Rid: 0,
|
Rid: 0,
|
||||||
Content: biliDynamicConfig.Msg,
|
Content: config.Msg,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := retry.Do(ctx, func() error {
|
if err := retry.Do(ctx, func() error {
|
||||||
_, err := biliClient.CreateDynamic(dynamicParams)
|
_, err := client.CreateDynamic(params)
|
||||||
return err
|
return err
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("发送B站动态失败: %w", err)
|
return fmt.Errorf("发送B站动态失败: %w", err)
|
||||||
|
|||||||
@@ -2,54 +2,34 @@ package bilibili
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/CuteReimu/bilibili/v2"
|
"github.com/CuteReimu/bilibili/v2"
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 私信消息类型:1 为文字
|
// privateMsgTypeText 私信消息类型:1 为文字
|
||||||
const privateMsgTypeText = 1
|
const privateMsgTypeText = 1
|
||||||
|
|
||||||
// SendBiliPrivateMessage 向配置中的每个 UID 发送一条 B 站私信
|
// SendPrivateMessage 用给定账号向配置中的每个 UID 发送一条 B 站私信
|
||||||
func SendBiliPrivateMessage(ctx context.Context, rule *model.Rule) error {
|
//
|
||||||
client, err := getBiliClient(rule)
|
// 每个接收者单独重试、互不阻塞,最后汇总失败信息交给上层记录。
|
||||||
if err != nil {
|
func SendPrivateMessage(ctx context.Context, account *BiliAccount, config *BiliPrivateMessageConfig) error {
|
||||||
return fmt.Errorf("获取B站客户端失败: %w", err)
|
client := newClient(account.RawCookies)
|
||||||
}
|
|
||||||
|
|
||||||
config, err := getPrivateMessageConfig(rule)
|
// 发私信需要带上自己的 UID,从账号信息里取
|
||||||
|
selfUID, err := fetchSelfUID(ctx, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
var fails []string
|
||||||
for _, uid := range config.ReceiverUids {
|
for _, uid := range config.ReceiverUids {
|
||||||
err := retry.Do(ctx, func() error {
|
if err := retry.Do(ctx, func() error {
|
||||||
_, err := client.SendPrivateMessage(bilibili.SendPrivateMessageParam{
|
return sendPrivateMessage(client, selfUID, uid, config.Msg)
|
||||||
SenderUid: account.Mid,
|
}); err != nil {
|
||||||
ReceiverId: int(uid),
|
|
||||||
ReceiverType: 1,
|
|
||||||
MsgType: privateMsgTypeText,
|
|
||||||
Timestamp: int(time.Now().Unix()),
|
|
||||||
Content: buildTextContent(config.Msg),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fails = append(fails, fmt.Sprintf("UID %d: %v", uid, err))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTextContent 文字私信的 content 是一个 JSON 字符串 {"content":"..."}
|
// fetchSelfUID 取当前账号的 UID,私信接口需要它
|
||||||
func buildTextContent(msg string) string {
|
func fetchSelfUID(ctx context.Context, client *bilibili.Client) (int, error) {
|
||||||
payload, err := json.Marshal(map[string]string{"content": msg})
|
var selfUID int
|
||||||
if err != nil {
|
if err := retry.Do(ctx, func() error {
|
||||||
// msg 是普通字符串,序列化不会失败;兜底也返回合法 JSON
|
info, err := client.GetAccountInformation()
|
||||||
return `{"content":""}`
|
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) {
|
// sendPrivateMessage 发送一条私信
|
||||||
var config BiliPrivateMessageConfig
|
func sendPrivateMessage(client *bilibili.Client, senderUID int, receiverUID int64, msg string) error {
|
||||||
if err := json.Unmarshal([]byte(rule.ConfigJson), &config); err != nil {
|
_, err := client.SendPrivateMessage(bilibili.SendPrivateMessageParam{
|
||||||
return nil, fmt.Errorf("解析B站私信规则配置失败: %w", err)
|
SenderUid: senderUID,
|
||||||
}
|
ReceiverId: int(receiverUID),
|
||||||
return &config, nil
|
ReceiverType: 1,
|
||||||
|
MsgType: privateMsgTypeText,
|
||||||
|
Timestamp: int(time.Now().Unix()),
|
||||||
|
Content: buildTextContent(msg),
|
||||||
|
})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package bilibili
|
package bilibili
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
@@ -15,9 +14,9 @@ func (v *BilibiliDynamicRuleValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *BilibiliDynamicRuleValidator) Validate(configJSON string) error {
|
func (v *BilibiliDynamicRuleValidator) Validate(configJSON string) error {
|
||||||
var config BiliDynamicConfig
|
config, err := ParseDynamicConfig(configJSON)
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析B站动态规则配置失败: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Msg == "" {
|
if config.Msg == "" {
|
||||||
@@ -39,9 +38,9 @@ func (v *BilibiliPrivateMessageRuleValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *BilibiliPrivateMessageRuleValidator) Validate(configJSON string) error {
|
func (v *BilibiliPrivateMessageRuleValidator) Validate(configJSON string) error {
|
||||||
var config BiliPrivateMessageConfig
|
config, err := ParsePrivateMessageConfig(configJSON)
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析B站私信规则配置失败: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Msg == "" {
|
if config.Msg == "" {
|
||||||
@@ -77,7 +76,7 @@ func (v *BilibiliPrivateMessageRuleValidator) Meta() meta.RuleMeta {
|
|||||||
Placeholder: "2",
|
Placeholder: "2",
|
||||||
Help: "对方空间地址 space.bilibili.com/ 后面的那串数字",
|
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,
|
AccountType: AccountType,
|
||||||
Fields: []meta.Field{
|
Fields: []meta.Field{
|
||||||
{
|
{
|
||||||
Key: "msg",
|
Key: "msg",
|
||||||
Label: "动态内容",
|
Label: "动态内容",
|
||||||
Type: meta.FieldTextarea,
|
Type: meta.FieldTextarea,
|
||||||
Required: true,
|
Required: true,
|
||||||
Placeholder: "要发送的动态正文",
|
GatewayMessage: true,
|
||||||
|
Placeholder: "要发送的动态正文",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package bilibili
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
@@ -14,10 +13,18 @@ func (e *BilibiliDynamicExecutor) GetType() string {
|
|||||||
return RuleTypeDynamic
|
return RuleTypeDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *BilibiliDynamicExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *BilibiliDynamicExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行B站动态规则: %s (ID: %d)", rule.Name, rule.ID)
|
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站私信执行器
|
// BilibiliPrivateMessageExecutor B站私信执行器
|
||||||
@@ -27,8 +34,16 @@ func (e *BilibiliPrivateMessageExecutor) GetType() string {
|
|||||||
return RuleTypePrivateMessage
|
return RuleTypePrivateMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *BilibiliPrivateMessageExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *BilibiliPrivateMessageExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行B站私信规则: %s (ID: %d)", rule.Name, rule.ID)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,55 @@ package bilibili
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/CuteReimu/bilibili/v2"
|
"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 {
|
func newClient(rawCookies string) *bilibili.Client {
|
||||||
client := bilibili.New()
|
client := bilibili.New()
|
||||||
client.SetRawCookies(rawCookies)
|
client.SetRawCookies(rawCookies)
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 Bilibili Client
|
// buildTextContent 文字私信的 content 是一个 JSON 字符串 {"content":"..."}
|
||||||
func getBiliClient(rule *model.Rule) (*bilibili.Client, error) {
|
func buildTextContent(msg string) string {
|
||||||
var accountConfig BiliAccount
|
payload, err := json.Marshal(map[string]string{"content": msg})
|
||||||
if err := db.LoadAccountConfig(rule.AccountID, &accountConfig); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// msg 是普通字符串,序列化不会失败;兜底也返回合法 JSON
|
||||||
|
return `{"content":""}`
|
||||||
}
|
}
|
||||||
|
return string(payload)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,51 @@
|
|||||||
package dingtalk
|
package dingtalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/CatchZeng/dingtalk/pkg/dingtalk"
|
"github.com/CatchZeng/dingtalk/pkg/dingtalk"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrNoUserConfig 表示用户没有配置钉钉提醒
|
||||||
|
var ErrNoUserConfig = fmt.Errorf("用户未配置钉钉机器人")
|
||||||
|
|
||||||
// SendDingTalkMsg 使用用户配置的钉钉机器人发送提醒
|
// SendDingTalkMsg 使用用户配置的钉钉机器人发送提醒
|
||||||
//
|
//
|
||||||
// 用户没有配置钉钉机器人时返回 ErrNoUserConfig
|
// 用户没有配置钉钉机器人时返回 ErrNoUserConfig
|
||||||
func SendDingTalkMsg(user *model.User, title string, msg string) error {
|
func SendDingTalkMsg(user *model.User, title string, msg string) error {
|
||||||
config := getDingTalkConfigFromUser(user)
|
if user.DingTalkConfig == nil {
|
||||||
if config == nil {
|
|
||||||
return ErrNoUserConfig
|
return ErrNoUserConfig
|
||||||
}
|
}
|
||||||
return send(config, title, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrNoUserConfig 表示用户没有配置钉钉提醒
|
config, err := ParseAccountConfig(*user.DingTalkConfig)
|
||||||
var ErrNoUserConfig = fmt.Errorf("用户未配置钉钉机器人")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func send(config *DingTalkConfig, title, msg string) error {
|
if err := send(config, title, msg); err != nil {
|
||||||
client := dingtalk.NewClient(config.AccessToken, config.Secret)
|
|
||||||
message := dingtalk.NewMarkdownMessage().SetMarkdown(title, msg)
|
|
||||||
|
|
||||||
if _, _, err := client.Send(message); err != nil {
|
|
||||||
return fmt.Errorf("发送钉钉消息失败: %w", err)
|
return fmt.Errorf("发送钉钉消息失败: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dingtalk
|
package dingtalk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
@@ -42,17 +41,8 @@ func (v *DingTalkRuleValidator) Meta() meta.RuleMeta {
|
|||||||
Fields: []meta.Field{
|
Fields: []meta.Field{
|
||||||
{Key: "access_token", Label: "Access Token", Type: meta.FieldPassword, Required: true, Secret: true, Help: "机器人 Webhook 中 access_token 参数的值"},
|
{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: "secret", Label: "加签 Secret", Type: meta.FieldPassword, Secret: true, Help: "机器人安全设置选择“加签”时填写"},
|
||||||
{Key: "title", Label: "消息标题", Type: meta.FieldString, Required: true},
|
{Key: "title", Label: "消息标题", Type: meta.FieldString, Required: true, GatewayMessage: true},
|
||||||
{Key: "msg", Label: "消息内容", Type: meta.FieldTextarea, Required: true, Help: "支持 Markdown"},
|
{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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,30 +2,23 @@ package dingtalk
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DingTalkExecutor 钉钉机器人执行器
|
// DingTalkExecutor 钉钉机器人执行器
|
||||||
//
|
|
||||||
// 之前只注册了校验器却没有执行器,导致钉钉规则可以创建但永远不会被执行。
|
|
||||||
type DingTalkExecutor struct{}
|
type DingTalkExecutor struct{}
|
||||||
|
|
||||||
func (e *DingTalkExecutor) GetType() string {
|
func (e *DingTalkExecutor) GetType() string {
|
||||||
return RuleType
|
return RuleType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DingTalkExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *DingTalkExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行钉钉规则: %s (ID: %d)", rule.Name, rule.ID)
|
// 钉钉机器人凭据直接写在规则配置里,不需要关联账号
|
||||||
|
config, err := ParseRuleConfig(task.Rule.ConfigJson)
|
||||||
config, err := ParseRuleConfig(rule.ConfigJson)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return retry.Do(ctx, func() error {
|
return SendMessage(ctx, config)
|
||||||
return send(&config.DingTalkConfig, config.Title, config.Msg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,27 @@ package dingtalk
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getDingTalkConfigFromUser(user *model.User) *DingTalkConfig {
|
// ParseAccountConfig 解析钉钉机器人凭据配置
|
||||||
if user.DingTalkConfig == nil {
|
func ParseAccountConfig(config string) (*DingTalkConfig, error) {
|
||||||
return nil
|
if config == "" {
|
||||||
|
return nil, fmt.Errorf("解析钉钉机器人配置失败: 配置为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
var config DingTalkConfig
|
var account DingTalkConfig
|
||||||
err := json.Unmarshal([]byte(*user.DingTalkConfig), &config)
|
if err := json.Unmarshal([]byte(config), &account); err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("解析钉钉机器人配置失败: %v", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
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,7 +2,6 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ func (v *EmailAccountConfigValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *EmailAccountConfigValidator) Validate(config string) error {
|
func (v *EmailAccountConfigValidator) Validate(config string) error {
|
||||||
cfg, err := parseAccountConfig(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -56,7 +55,7 @@ func (v *EmailAccountConfigValidator) Validate(config string) error {
|
|||||||
|
|
||||||
// Test 连接 SMTP 服务器并完成认证;填写了测试收件地址时会真的发一封测试邮件
|
// Test 连接 SMTP 服务器并完成认证;填写了测试收件地址时会真的发一封测试邮件
|
||||||
func (v *EmailAccountConfigValidator) Test(config string) error {
|
func (v *EmailAccountConfigValidator) Test(config string) error {
|
||||||
cfg, err := parseAccountConfig(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,38 +3,31 @@ package email
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendMail 向规则配置里的所有地址发送邮件。
|
// SendMail 用给定 SMTP 账号向规则配置里的所有地址发送邮件。
|
||||||
//
|
//
|
||||||
// 每个地址单独重试,互不阻塞;全部失败/部分失败都会汇总成错误返回,
|
// 每个地址单独重试、互不阻塞;全部失败/部分失败都会汇总成错误返回,
|
||||||
// 由 runner 记录到执行日志中。
|
// 由 runner 记录到执行日志中。
|
||||||
func SendMail(ctx context.Context, rule *model.Rule) error {
|
func SendMail(ctx context.Context, account *EmailAccountConfig, config *EmailRule) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
fails []string
|
fails []string
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, destination := range emailRule.Destinations {
|
for _, destination := range config.Destinations {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(address string) {
|
go func(address string) {
|
||||||
defer wg.Done()
|
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()
|
mu.Lock()
|
||||||
fails = append(fails, fmt.Sprintf("%s: %v", address, err))
|
fails = append(fails, fmt.Sprintf("%s: %v", address, err))
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
@@ -44,8 +37,10 @@ func SendMail(ctx context.Context, rule *model.Rule) error {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if len(fails) > 0 {
|
if len(fails) > 0 {
|
||||||
|
// 并发发送的顺序不确定,排序后错误信息才是稳定的
|
||||||
|
sort.Strings(fails)
|
||||||
return fmt.Errorf("%d/%d 封邮件发送失败: %s",
|
return fmt.Errorf("%d/%d 封邮件发送失败: %s",
|
||||||
len(fails), len(emailRule.Destinations), strings.Join(fails, "; "))
|
len(fails), len(config.Destinations), strings.Join(fails, "; "))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
|
||||||
@@ -16,9 +15,9 @@ func (v *EmailRuleValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *EmailRuleValidator) Validate(configJSON string) error {
|
func (v *EmailRuleValidator) Validate(configJSON string) error {
|
||||||
var config EmailRule
|
config, err := ParseRuleConfig(configJSON)
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析邮件规则配置失败: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Title == "" {
|
if config.Title == "" {
|
||||||
@@ -51,8 +50,8 @@ func (v *EmailRuleValidator) Meta() meta.RuleMeta {
|
|||||||
Docs: "docs/email-config.md",
|
Docs: "docs/email-config.md",
|
||||||
AccountType: AccountType,
|
AccountType: AccountType,
|
||||||
Fields: []meta.Field{
|
Fields: []meta.Field{
|
||||||
{Key: "title", Label: "邮件标题", Type: meta.FieldString, Required: true},
|
{Key: "title", Label: "邮件标题", Type: meta.FieldString, Required: true, GatewayMessage: true},
|
||||||
{Key: "msg", Label: "邮件正文", Type: meta.FieldTextarea, Required: true},
|
{Key: "msg", Label: "邮件正文", Type: meta.FieldTextarea, Required: true, GatewayMessage: true},
|
||||||
{Key: "destinations", Label: "收件人", Type: meta.FieldStringList, Required: true, Placeholder: "someone@example.com"},
|
{Key: "destinations", Label: "收件人", Type: meta.FieldStringList, Required: true, Placeholder: "someone@example.com"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
@@ -14,8 +13,16 @@ func (e *EmailExecutor) GetType() string {
|
|||||||
return RuleType
|
return RuleType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmailExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *EmailExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行邮件规则: %s (ID: %d)", rule.Name, rule.ID)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,29 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/handler/db"
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
"github.com/wneessen/go-mail"
|
"github.com/wneessen/go-mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetEmailAccountFromRule 从 Rule 中获取 EmailAccount 配置
|
// ParseAccountConfig 解析邮箱账号配置
|
||||||
func GetEmailAccountFromRule(rule *model.Rule) (*EmailAccountConfig, error) {
|
func ParseAccountConfig(config string) (*EmailAccountConfig, error) {
|
||||||
var emailAccountConfig EmailAccountConfig
|
if config == "" {
|
||||||
if err := db.LoadAccountConfig(rule.AccountID, &emailAccountConfig); err != nil {
|
return nil, fmt.Errorf("解析邮箱账号配置失败: 规则没有关联账号")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
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 配置
|
// ParseRuleConfig 解析邮件规则配置
|
||||||
func GetEmailRuleFromRule(rule *model.Rule) (*EmailRule, error) {
|
func ParseRuleConfig(configJSON string) (*EmailRule, error) {
|
||||||
var emailRule EmailRule
|
var config EmailRule
|
||||||
if err := json.Unmarshal([]byte(rule.ConfigJson), &emailRule); err != nil {
|
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("解析邮件规则配置失败: %v", err)
|
||||||
}
|
}
|
||||||
return &emailRule, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSMTPClient 按账号配置里的加密方式创建 SMTP 客户端
|
// newSMTPClient 按账号配置里的加密方式创建 SMTP 客户端
|
||||||
@@ -54,6 +55,7 @@ func newSMTPClient(cfg *EmailAccountConfig) (*mail.Client, error) {
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildMessage 组装一封邮件
|
||||||
func buildMessage(cfg *EmailAccountConfig, address, title, body string) (*mail.Msg, error) {
|
func buildMessage(cfg *EmailAccountConfig, address, title, body string) (*mail.Msg, error) {
|
||||||
message := mail.NewMsg()
|
message := mail.NewMsg()
|
||||||
if err := message.From(cfg.FromOrDefault()); err != nil {
|
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
|
return message, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMailMsgWithRetry(ctx context.Context, cfg *EmailAccountConfig, rule *EmailRule, address string) error {
|
// sendMail 给一个地址发送一封邮件
|
||||||
return retry.Do(ctx, func() error {
|
func sendMail(ctx context.Context, cfg *EmailAccountConfig, config *EmailRule, address string) error {
|
||||||
return sendMailMsg(ctx, cfg, rule, address)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendMailMsg(ctx context.Context, cfg *EmailAccountConfig, rule *EmailRule, address string) error {
|
|
||||||
client, err := newSMTPClient(cfg)
|
client, err := newSMTPClient(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
message, err := buildMessage(cfg, address, rule.Title, rule.Msg)
|
message, err := buildMessage(cfg, address, config.Title, config.Msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package fwalert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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("Webhook 返回状态码 %s", resp.Status())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package fwalert
|
||||||
|
|
||||||
|
// RuleType 饭碗警告规则类型标识
|
||||||
|
const RuleType = "fwalert"
|
||||||
|
|
||||||
|
type FwAlertRuleConfig struct {
|
||||||
|
WebhookURL string `json:"webhook_url"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FwAlertRequest struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package fwalert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FwalertRuleValidator 饭碗警告规则验证器
|
||||||
|
type FwalertRuleValidator struct{}
|
||||||
|
|
||||||
|
func (v *FwalertRuleValidator) GetType() string {
|
||||||
|
return RuleType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *FwalertRuleValidator) Validate(configJSON string) error {
|
||||||
|
config, err := ParseRuleConfig(configJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := url.Parse(config.WebhookURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("饭碗警告规则配置中 webhook_url 格式不正确: %v", err)
|
||||||
|
}
|
||||||
|
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||||
|
return fmt.Errorf("饭碗警告规则配置中 webhook_url 必须是 HTTP 或 HTTPS URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Msg == "" {
|
||||||
|
return fmt.Errorf("饭碗警告规则配置中 msg 不能为空")
|
||||||
|
}
|
||||||
|
if len([]rune(config.Msg)) > 1000 {
|
||||||
|
return fmt.Errorf("饭碗警告内容过长")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *FwalertRuleValidator) Meta() meta.RuleMeta {
|
||||||
|
return meta.RuleMeta{
|
||||||
|
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, GatewayMessage: true, Help: "最大长度 1000 个字符"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package fwalert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FwalertExecutor 饭碗警告执行器
|
||||||
|
type FwalertExecutor struct{}
|
||||||
|
|
||||||
|
func (e *FwalertExecutor) GetType() string {
|
||||||
|
return RuleType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FwalertExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
|
// 饭碗警告的凭据就是 Webhook 地址,直接写在规则配置里
|
||||||
|
config, err := ParseRuleConfig(task.Rule.ConfigJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Send(ctx, config)
|
||||||
|
}
|
||||||
@@ -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,7 +2,6 @@ package github
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/go-github/v84/github"
|
"github.com/google/go-github/v84/github"
|
||||||
@@ -18,7 +17,7 @@ func (v *GitHubAccountConfigValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *GitHubAccountConfigValidator) Validate(config string) error {
|
func (v *GitHubAccountConfigValidator) Validate(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -35,7 +34,7 @@ func (v *GitHubAccountConfigValidator) Validate(config string) error {
|
|||||||
|
|
||||||
// Test 用 token 拉一次当前用户信息,验证 token 是否有效
|
// Test 用 token 拉一次当前用户信息,验证 token 是否有效
|
||||||
func (v *GitHubAccountConfigValidator) Test(config string) error {
|
func (v *GitHubAccountConfigValidator) Test(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,29 +6,24 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MakeRepositoryPublic 将规则中配置的仓库设置为 public
|
// MakeRepositoryPublic 把配置里的仓库逐个设置为 public
|
||||||
func MakeRepositoryPublic(ctx context.Context, rule *model.Rule) error {
|
//
|
||||||
reposConfig, account, err := GetGithubReposAndAccountFromRule(rule)
|
// 每个仓库单独重试、互不阻塞,最后汇总失败信息交给上层记录。
|
||||||
if err != nil {
|
func MakeRepositoryPublic(ctx context.Context, account *GithubAccount, config *GithubReposConfig) error {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var fails []string
|
var fails []string
|
||||||
for _, repo := range reposConfig.Repos {
|
for _, repo := range config.Repos {
|
||||||
err := retry.Do(ctx, func() error {
|
if err := retry.Do(ctx, func() error {
|
||||||
return SetRepositoryPublic(ctx, account.Token, account.Owner, repo)
|
return SetRepositoryPublic(ctx, account.Token, account.Owner, repo)
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
fails = append(fails, err.Error())
|
||||||
fails = append(fails, fmt.Sprintf("%s: %v", repo, err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fails) > 0 {
|
if len(fails) > 0 {
|
||||||
return fmt.Errorf("%d/%d 个仓库设置为 public 失败: %s",
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package github
|
package github
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -15,9 +14,9 @@ func (v *GithubMakeRepositoryPublicRuleValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *GithubMakeRepositoryPublicRuleValidator) Validate(configJSON string) error {
|
func (v *GithubMakeRepositoryPublicRuleValidator) Validate(configJSON string) error {
|
||||||
var config GithubReposConfig
|
config, err := ParseReposConfig(configJSON)
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析 GitHub 规则配置失败: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Repos) == 0 {
|
if len(config.Repos) == 0 {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package github
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
@@ -14,8 +13,16 @@ func (e *GithubMakeRepoPublicExecutor) GetType() string {
|
|||||||
return RuleTypeMakeRepoPublic
|
return RuleTypeMakeRepoPublic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GithubMakeRepoPublicExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *GithubMakeRepoPublicExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行GitHub仓库公开规则: %s (ID: %d)", rule.Name, rule.ID)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,33 +6,37 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/go-github/v84/github"
|
"github.com/google/go-github/v84/github"
|
||||||
"github.com/ssdomei232/goodBaby/handler/db"
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetGithubReposAndAccountFromRule 从 rule 中获取 GithubReposConfig 和 GithubAccount
|
// ParseAccountConfig 解析 GitHub 账号配置
|
||||||
func GetGithubReposAndAccountFromRule(rule *model.Rule) (*GithubReposConfig, *GithubAccount, error) {
|
func ParseAccountConfig(config string) (*GithubAccount, error) {
|
||||||
var githubReposConfig GithubReposConfig
|
if config == "" {
|
||||||
if err := json.Unmarshal([]byte(rule.ConfigJson), &githubReposConfig); err != nil {
|
return nil, fmt.Errorf("解析GitHub账号配置失败: 规则没有关联账号")
|
||||||
return nil, nil, fmt.Errorf("解析 GitHub 规则配置失败: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var githubAccount GithubAccount
|
var account GithubAccount
|
||||||
if err := db.LoadAccountConfig(rule.AccountID, &githubAccount); err != nil {
|
if err := json.Unmarshal([]byte(config), &account); err != nil {
|
||||||
return nil, nil, err
|
return nil, fmt.Errorf("解析GitHub账号配置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
return &account, nil
|
||||||
return &githubReposConfig, &githubAccount, 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 {
|
func SetRepositoryPublic(ctx context.Context, token, owner, repo string) error {
|
||||||
client := github.NewClient(nil).WithAuthToken(token)
|
client := github.NewClient(nil).WithAuthToken(token)
|
||||||
|
if _, _, err := client.Repositories.Edit(ctx, owner, repo, &github.Repository{
|
||||||
opts := &github.Repository{
|
|
||||||
Visibility: github.Ptr("public"),
|
Visibility: github.Ptr("public"),
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("设置仓库 %s 为 public 失败: %w", repo, err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
_, _, err := client.Repositories.Edit(ctx, owner, repo, opts)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package onebot
|
package onebot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,7 +16,7 @@ func (v *OneBotAccountConfigValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *OneBotAccountConfigValidator) Validate(config string) error {
|
func (v *OneBotAccountConfigValidator) Validate(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ func (v *OneBotAccountConfigValidator) Validate(config string) error {
|
|||||||
|
|
||||||
// Test 调用 get_login_info 验证地址与 Token
|
// Test 调用 get_login_info 验证地址与 Token
|
||||||
func (v *OneBotAccountConfigValidator) Test(config string) error {
|
func (v *OneBotAccountConfigValidator) Test(config string) error {
|
||||||
cfg, err := parseAccount(config)
|
cfg, err := ParseAccountConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,40 +6,29 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/retry"
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendOneBotMsg 向规则中配置的群和好友发送消息
|
// SendMessage 向规则中配置的群和好友发送消息
|
||||||
func SendOneBotMsg(ctx context.Context, rule *model.Rule) error {
|
//
|
||||||
oneBotConfig, err := getOneBotConfigFromRule(rule)
|
// 每个目标单独重试、互不阻塞,最后汇总失败信息交给上层记录。
|
||||||
if err != nil {
|
func SendMessage(ctx context.Context, account *OneBotAccount, config *OneBotConfig) error {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oneBotAccount, err := getOneBotAccountFromRule(rule)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取OneBot账户失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fails []string
|
var fails []string
|
||||||
total := 0
|
total := 0
|
||||||
|
|
||||||
for _, groupID := range oneBotConfig.SendGroups {
|
for _, groupID := range config.SendGroups {
|
||||||
total++
|
total++
|
||||||
err := retry.Do(ctx, func() error {
|
if err := retry.Do(ctx, func() error {
|
||||||
return sendGroupMsg(oneBotAccount, groupID, oneBotConfig.Msg)
|
return sendGroupMsg(account, groupID, config.Msg)
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
fails = append(fails, fmt.Sprintf("群 %d: %v", groupID, err))
|
fails = append(fails, fmt.Sprintf("群 %d: %v", groupID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, userID := range oneBotConfig.SendUsers {
|
for _, userID := range config.SendUsers {
|
||||||
total++
|
total++
|
||||||
err := retry.Do(ctx, func() error {
|
if err := retry.Do(ctx, func() error {
|
||||||
return sendPrivateMsg(oneBotAccount, userID, oneBotConfig.Msg)
|
return sendPrivateMsg(account, userID, config.Msg)
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
fails = append(fails, fmt.Sprintf("好友 %d: %v", userID, err))
|
fails = append(fails, fmt.Sprintf("好友 %d: %v", userID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package onebot
|
package onebot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/internal/meta"
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
@@ -15,9 +14,9 @@ func (v *OneBotRuleValidator) GetType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *OneBotRuleValidator) Validate(configJSON string) error {
|
func (v *OneBotRuleValidator) Validate(configJSON string) error {
|
||||||
var config OneBotConfig
|
config, err := ParseRuleConfig(configJSON)
|
||||||
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析OneBot规则配置失败: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Msg == "" {
|
if config.Msg == "" {
|
||||||
@@ -44,7 +43,7 @@ func (v *OneBotRuleValidator) Meta() meta.RuleMeta {
|
|||||||
Description: "触发时通过 OneBot 向指定的群或好友发送消息。",
|
Description: "触发时通过 OneBot 向指定的群或好友发送消息。",
|
||||||
AccountType: AccountType,
|
AccountType: AccountType,
|
||||||
Fields: []meta.Field{
|
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_groups", Label: "群号", Type: meta.FieldNumberList, Placeholder: "123456789"},
|
||||||
{Key: "send_users", Label: "好友 QQ 号", Type: meta.FieldNumberList, Placeholder: "123456789"},
|
{Key: "send_users", Label: "好友 QQ 号", Type: meta.FieldNumberList, Placeholder: "123456789"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package onebot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
@@ -14,8 +13,16 @@ func (e *OneBotExecutor) GetType() string {
|
|||||||
return RuleType
|
return RuleType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *OneBotExecutor) Execute(ctx context.Context, rule *model.Rule) error {
|
func (e *OneBotExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
log.Printf("执行OneBot规则: %s (ID: %d)", rule.Name, rule.ID)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,37 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/handler/db"
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
|
||||||
napcat "github.com/ssdomei232/napcat-http-go-sdk"
|
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 {
|
func newClient(account *OneBotAccount) *napcat.Client {
|
||||||
return napcat.NewClient(account.Token, account.URL)
|
return napcat.NewClient(account.Token, account.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Rule 中获取 OneBotConfig
|
// sendGroupMsg 发送一条群消息
|
||||||
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 错误、群号不存在这类失败会被当成发送成功。
|
|
||||||
func sendGroupMsg(account *OneBotAccount, groupID int64, msg string) error {
|
func sendGroupMsg(account *OneBotAccount, groupID int64, msg string) error {
|
||||||
return call(account, "/send_group_msg", map[string]any{
|
return call(account, "/send_group_msg", map[string]any{
|
||||||
"group_id": groupID,
|
"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 {
|
func sendPrivateMsg(account *OneBotAccount, userID int64, msg string) error {
|
||||||
return call(account, "/send_private_msg", map[string]any{
|
return call(account, "/send_private_msg", map[string]any{
|
||||||
"user_id": userID,
|
"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 {
|
func call(account *OneBotAccount, endpoint string, payload map[string]any) error {
|
||||||
var resp apiResponse
|
var resp apiResponse
|
||||||
if err := newClient(account).DoRequest("POST", endpoint, payload, &resp); err != nil {
|
if err := newClient(account).DoRequest("POST", endpoint, payload, &resp); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 := ParseAccountConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.APIKey == "" {
|
||||||
|
return fmt.Errorf("雨云账号配置中APIKey不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RainyunAccountConfigValidator) Test(config string) error {
|
||||||
|
cfg, err := ParseAccountConfig(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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AccountType Rainyun 账号类型标识
|
||||||
|
AccountType = "rainyun"
|
||||||
|
// RuleTypeWorkOrder 发送 Rainyun 工单消息的规则类型标识
|
||||||
|
RuleTypeWorkOrder = "rainyun-workorder"
|
||||||
|
RuleTypeRunAway = "rainyun-runaway"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pageSize 拉取云服务器列表时每页的条数
|
||||||
|
const pageSize = 20
|
||||||
|
|
||||||
|
// runAwayConfirmText 跑路规则要求用户输入的免责声明
|
||||||
|
const runAwayConfirmText = "我已知晓"
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/retry"
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createWorkOrder 调用一次工单接口
|
||||||
|
func createWorkOrder(account *RainyunAccount, config *RainyunWorkOrderRule) error {
|
||||||
|
workorderClient := workorder.Client{Client: rain.NewClient(account.APIKey)}
|
||||||
|
|
||||||
|
_, err := workorderClient.CreateWorkOrder(&workorder.CreateWorkerorderRequest{
|
||||||
|
Content: config.Msg,
|
||||||
|
IsAuthed: false,
|
||||||
|
IsUrgent: 1,
|
||||||
|
Title: config.Title,
|
||||||
|
Type: "reward",
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
rgsFails, rgsTotal, err := reinstallAllRgs(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rainyun规则验证器
|
||||||
|
type RainyunWorkorderRuleValidator struct{}
|
||||||
|
|
||||||
|
func (v *RainyunWorkorderRuleValidator) GetType() string {
|
||||||
|
return RuleTypeWorkOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RainyunWorkorderRuleValidator) Validate(configJSON string) error {
|
||||||
|
config, err := ParseWorkOrderConfig(configJSON)
|
||||||
|
if err != nil {
|
||||||
|
return 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: RuleTypeWorkOrder,
|
||||||
|
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 RuleTypeRunAway
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RainyunRunAwayRuleValidator) Validate(configJSON string) error {
|
||||||
|
config, err := ParseRunAwayConfig(configJSON)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.IK != runAwayConfirmText {
|
||||||
|
return fmt.Errorf("请输入正确内容")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RainyunRunAwayRuleValidator) Meta() meta.RuleMeta {
|
||||||
|
return meta.RuleMeta{
|
||||||
|
Type: RuleTypeRunAway,
|
||||||
|
Label: "重置雨云账号中所有云服务器(一键跑路)",
|
||||||
|
Description: "触发时会重装雨云账号下所有云服务器来实现跑路",
|
||||||
|
AccountType: AccountType,
|
||||||
|
Fields: []meta.Field{
|
||||||
|
{Key: "ik", Label: "免责声明", Type: meta.FieldTextarea, Required: true, Placeholder: "该操作引发的损失及法律责任归属账号所有人,本软件一律不承担,请输入\"我已知晓\""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RainyunWorkorderExecutor 雨云工单执行器
|
||||||
|
type RainyunWorkorderExecutor struct{}
|
||||||
|
|
||||||
|
func (e *RainyunWorkorderExecutor) GetType() string {
|
||||||
|
return RuleTypeWorkOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RainyunWorkorderExecutor) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
|
account, err := ParseAccountConfig(task.AccountConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := ParseWorkOrderConfig(task.Rule.ConfigJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendWorkOrder(ctx, account, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RainyunRunAwayExecutor 雨云跑路执行器
|
||||||
|
type RainyunRunAwayExecutor struct{}
|
||||||
|
|
||||||
|
func (e *RainyunRunAwayExecutor) GetType() string {
|
||||||
|
return RuleTypeRunAway
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package rainyun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseAccountConfig 解析雨云账号配置
|
||||||
|
func ParseAccountConfig(config string) (*RainyunAccount, error) {
|
||||||
|
if config == "" {
|
||||||
|
return nil, fmt.Errorf("解析雨云账号配置失败: 规则没有关联账号")
|
||||||
|
}
|
||||||
|
|
||||||
|
var account RainyunAccount
|
||||||
|
if err := json.Unmarshal([]byte(config), &account); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析雨云账号配置失败: %v", err)
|
||||||
|
}
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 &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 {
|
||||||
|
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" {
|
||||||
|
_, err := rcsClient.ReinstallRcs(rcsID, &rcs.ReinstallRcsRequest{
|
||||||
|
OsID: os.ID,
|
||||||
|
ResetOsd: true,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("没有找到可用的 linux 镜像")
|
||||||
|
}
|
||||||
|
|
||||||
|
// reinstallOneRGS 重装一台 RGS
|
||||||
|
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" {
|
||||||
|
_, err := rgsClient.Reinstallgs(rgsID, &rcs.ReinstallRcsRequest{
|
||||||
|
OsID: os.ID,
|
||||||
|
ResetOsd: true,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("没有找到可用的 linux 镜像")
|
||||||
|
}
|
||||||
@@ -5,22 +5,37 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/CatchZeng/dingtalk v1.5.0
|
github.com/CatchZeng/dingtalk v1.5.0
|
||||||
github.com/CuteReimu/bilibili/v2 v2.3.5
|
github.com/CuteReimu/bilibili/v2 v2.3.5
|
||||||
|
github.com/alibabacloud-go/alidns-20150109/v5 v5.6.0
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi v0.2.1
|
||||||
|
github.com/alibabacloud-go/tea v1.5.2
|
||||||
|
github.com/aliyun/credentials-go v1.4.5
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3
|
github.com/cenkalti/backoff/v5 v5.0.3
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/glebarez/sqlite 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/google/go-github/v84 v84.0.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/ssdomei232/napcat-http-go-sdk v1.0.0
|
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
|
github.com/wneessen/go-mail v0.7.2
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.25.12
|
gorm.io/gorm v1.25.12
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f // indirect
|
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f // indirect
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 // indirect
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.4.3 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
|
||||||
|
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
@@ -29,7 +44,6 @@ require (
|
|||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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-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-json v0.10.5 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/google/go-querystring v1.2.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
@@ -37,6 +51,7 @@ require (
|
|||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
github.com/gorilla/sessions v1.4.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||||
@@ -57,6 +72,7 @@ require (
|
|||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
github.com/spf13/cast v1.9.2 // indirect
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
@@ -68,7 +84,7 @@ require (
|
|||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gorm.io/driver/postgres v1.6.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
modernc.org/memory v1.5.0 // indirect
|
modernc.org/memory v1.5.0 // indirect
|
||||||
|
|||||||
@@ -1,24 +1,89 @@
|
|||||||
bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI=
|
bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI=
|
||||||
bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA=
|
bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA=
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
|
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
|
||||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
|
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/CatchZeng/dingtalk v1.5.0 h1:3YNpGIdeZ4pTfjjVyC2G1CCzWocMmYniBY8CXiOY9Ss=
|
github.com/CatchZeng/dingtalk v1.5.0 h1:3YNpGIdeZ4pTfjjVyC2G1CCzWocMmYniBY8CXiOY9Ss=
|
||||||
github.com/CatchZeng/dingtalk v1.5.0/go.mod h1:/CT0Eskr26XfjAybLBLV+DjvPHaznwiFXRBndQp7zZ8=
|
github.com/CatchZeng/dingtalk v1.5.0/go.mod h1:/CT0Eskr26XfjAybLBLV+DjvPHaznwiFXRBndQp7zZ8=
|
||||||
github.com/CuteReimu/bilibili/v2 v2.3.5 h1:0MpBOJL6Q7kP3lFItC7NQ3MLG/I1uIzZBjemXIE0CRM=
|
github.com/CuteReimu/bilibili/v2 v2.3.5 h1:0MpBOJL6Q7kP3lFItC7NQ3MLG/I1uIzZBjemXIE0CRM=
|
||||||
github.com/CuteReimu/bilibili/v2 v2.3.5/go.mod h1:FHKH1cc04M/qpQhF+DaFIBFleLtvgMwYfZCNhHK/kBQ=
|
github.com/CuteReimu/bilibili/v2 v2.3.5/go.mod h1:FHKH1cc04M/qpQhF+DaFIBFleLtvgMwYfZCNhHK/kBQ=
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||||
|
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||||
|
github.com/alibabacloud-go/alidns-20150109/v5 v5.6.0 h1:dfHe5OpLz/vGINeU8ZOB1Y0dANk4+Td023zMaf+8rew=
|
||||||
|
github.com/alibabacloud-go/alidns-20150109/v5 v5.6.0/go.mod h1:1tXuCJoXdeAY9uBxgJ6ZLbv+SOe/4SJhUMSNCz9pOng=
|
||||||
|
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||||
|
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||||
|
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||||
|
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||||
|
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||||
|
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi v0.2.1 h1:WyzxxKvhdVDlwpAMOHgAiCJ+NXa6g5ZWPFEzaK/ewwY=
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi v0.2.1/go.mod h1:zXOqLbpIqq543oioL9IuuZYOQgHQ5B8/n5OPrnko8aY=
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4 h1:o6veen0IZ/Fe1JawwhwQMZcbw67CVDY1pQwXcNWPyQo=
|
||||||
|
github.com/alibabacloud-go/darabonba-openapi/v2 v2.2.4/go.mod h1:eHjVxrT9g8uVYN/nyAwOFQEfkVA154ChiqRc2XnNKYU=
|
||||||
|
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||||
|
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||||
|
github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||||
|
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||||
|
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||||
|
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||||
|
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||||
|
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||||
|
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||||
|
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.0.11/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||||
|
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.19/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
|
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||||
|
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||||
|
github.com/alibabacloud-go/tea v1.5.2 h1:DUzV/cRTbJC++rmADfqVu//GuWbaD64ZJ3yW3v2ukNU=
|
||||||
|
github.com/alibabacloud-go/tea v1.5.2/go.mod h1:hgSs82CkOiehSQMoiFN79dL6zsGX7pVGvnn9SIEs8/0=
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.4.3 h1:8SzwmmRrOnQ09Hf5a9GyfJc0d7Sjv6fmsZoF4UDbFjo=
|
||||||
|
github.com/alibabacloud-go/tea-utils v1.4.3/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA=
|
||||||
|
github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||||
|
github.com/alibabacloud-go/tea-xml v1.1.2 h1:oLxa7JUXm2EDFzMg+7oRsYc+kutgCVwm+bZlhhmvW5M=
|
||||||
|
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||||
|
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||||
|
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||||
|
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||||
|
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
|
||||||
|
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||||
|
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
@@ -47,8 +112,23 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
|||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
@@ -63,12 +143,16 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
|||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -81,12 +165,16 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
@@ -98,14 +186,18 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
@@ -119,50 +211,195 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
|
|||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
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 h1:BY3iqqlZw5letiCwTmfMrhyqsx0a96eL5LpDuinnOSE=
|
||||||
github.com/ssdomei232/napcat-http-go-sdk v1.0.0/go.mod h1:yPsSaMs3OyKMVPXwIs5ZB5G532vRi1GvL/WiWUEwblU=
|
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
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=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||||
|
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||||
|
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -170,6 +407,8 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
|||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
|||||||
@@ -1,33 +1,28 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadAccountConfig 读取账号并把 Config 字段反序列化到 out。
|
// GetAccount 读取一个账号,返回数据库里的原始记录。
|
||||||
//
|
//
|
||||||
// 各个 driver 之前都各自实现了一遍这段逻辑,其中 github driver 甚至
|
// 规则执行需要的账号凭据由上层(handler/runner)在这里取好再传给 driver,
|
||||||
// 直接把驱动内的结构体当成 gorm model 查询,这里统一收敛。
|
// driver 自己不碰数据库。accountID 为 0 表示该规则不需要账号,返回 (nil, nil)。
|
||||||
func LoadAccountConfig(accountID uint, out any) error {
|
func GetAccount(accountID uint) (*model.Account, error) {
|
||||||
if accountID == 0 {
|
if accountID == 0 {
|
||||||
return fmt.Errorf("该规则没有关联账号")
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gormDB, err := GetGormDB()
|
gormDB, err := GetGormDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var account model.Account
|
var account model.Account
|
||||||
if err := gormDB.First(&account, accountID).Error; err != nil {
|
if err := gormDB.First(&account, accountID).Error; err != nil {
|
||||||
return fmt.Errorf("获取账号(ID: %d)失败: %w", accountID, err)
|
return nil, fmt.Errorf("获取账号(ID: %d)失败: %w", accountID, err)
|
||||||
}
|
}
|
||||||
|
return &account, nil
|
||||||
if err := json.Unmarshal([]byte(account.Config), out); err != nil {
|
|
||||||
return fmt.Errorf("解析账号(ID: %d)配置失败: %w", accountID, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ func MustInit() *gorm.DB {
|
|||||||
if err := ensureAdmin(gormDB); err != nil {
|
if err := ensureAdmin(gormDB); err != nil {
|
||||||
log.Fatalf("初始化管理员失败: %v", err)
|
log.Fatalf("初始化管理员失败: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := migrateGatewayRules(gormDB); err != nil {
|
||||||
|
log.Fatalf("迁移消息网关规则失败: %v", err)
|
||||||
|
}
|
||||||
return gormDB
|
return gormDB
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +139,9 @@ func AutoMigrate(gormDB *gorm.DB) error {
|
|||||||
&model.User{},
|
&model.User{},
|
||||||
&model.Timer{},
|
&model.Timer{},
|
||||||
&model.Rule{},
|
&model.Rule{},
|
||||||
|
&model.GatewayRule{},
|
||||||
&model.Account{},
|
&model.Account{},
|
||||||
&model.ExecutionLog{},
|
&model.ExecutionLog{},
|
||||||
|
&model.MessageGateway{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// legacyRule 是迁移前的 rules 表结构,只用来读出已经废弃的 gateway_id
|
||||||
|
type legacyRule struct {
|
||||||
|
ID uint
|
||||||
|
UID uint
|
||||||
|
GatewayID uint
|
||||||
|
Name string
|
||||||
|
AccountID uint
|
||||||
|
Type string
|
||||||
|
ConfigJson string
|
||||||
|
Enabled bool
|
||||||
|
CreateAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrateGatewayRules 把历史数据里混在 rules 表中的消息网关规则搬到 gateway_rules 表。
|
||||||
|
//
|
||||||
|
// v2.1 之前消息网关规则和定时器规则共用 rules 表,靠 gateway_id 区分;
|
||||||
|
// 现在两者分表存储,这里做一次性搬迁,避免老用户升级后规则凭空消失。
|
||||||
|
func migrateGatewayRules(gormDB *gorm.DB) error {
|
||||||
|
if !gormDB.Migrator().HasColumn("rules", "gateway_id") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacy []legacyRule
|
||||||
|
if err := gormDB.Table("rules").Where("gateway_id <> 0").Find(&legacy).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(legacy) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return gormDB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, rule := range legacy {
|
||||||
|
moved := model.GatewayRule{
|
||||||
|
UID: rule.UID,
|
||||||
|
GatewayID: rule.GatewayID,
|
||||||
|
Name: rule.Name,
|
||||||
|
AccountID: rule.AccountID,
|
||||||
|
Type: rule.Type,
|
||||||
|
ConfigJson: rule.ConfigJson,
|
||||||
|
Enabled: rule.Enabled,
|
||||||
|
CreateAt: rule.CreateAt,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&moved).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Exec("DELETE FROM rules WHERE id = ?", rule.ID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("已迁移 %d 条消息网关规则到 gateway_rules 表", len(legacy))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,18 +6,16 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
|
||||||
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
|
|
||||||
"github.com/ssdomei232/goodBaby/drivers/email"
|
|
||||||
"github.com/ssdomei232/goodBaby/drivers/github"
|
|
||||||
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
|
||||||
"github.com/ssdomei232/goodBaby/model"
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RuleExecutor 规则执行器接口
|
// RuleExecutor 规则执行器接口。
|
||||||
|
//
|
||||||
|
// 执行一条规则需要的一切(规则本体、账号凭据)都由上层组装成 model.RuleTask,
|
||||||
|
// driver 只负责把动作做出去:不查数据库、不写日志,因此每个实现都能单独测试。
|
||||||
type RuleExecutor interface {
|
type RuleExecutor interface {
|
||||||
// Execute 执行规则,ctx 决定了重试的最长时间
|
// Execute 执行规则,ctx 决定了重试的最长时间
|
||||||
Execute(ctx context.Context, rule *model.Rule) error
|
Execute(ctx context.Context, task *model.RuleTask) error
|
||||||
// GetType 获取执行器支持的规则类型
|
// GetType 获取执行器支持的规则类型
|
||||||
GetType() string
|
GetType() string
|
||||||
}
|
}
|
||||||
@@ -40,13 +38,13 @@ func (er *ExecutorRegistry) Register(executor RuleExecutor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute 根据规则类型执行规则
|
// Execute 根据规则类型执行规则
|
||||||
func (er *ExecutorRegistry) Execute(ctx context.Context, rule *model.Rule) error {
|
func (er *ExecutorRegistry) Execute(ctx context.Context, task *model.RuleTask) error {
|
||||||
executor, exists := er.executors[rule.Type]
|
executor, exists := er.executors[task.Rule.Type]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("不支持的规则类型: %s", rule.Type)
|
return fmt.Errorf("不支持的规则类型: %s", task.Rule.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
return executor.Execute(ctx, rule)
|
return executor.Execute(ctx, task)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSupportedTypes 获取所有支持的规则类型
|
// GetSupportedTypes 获取所有支持的规则类型
|
||||||
@@ -64,25 +62,6 @@ var (
|
|||||||
globalExecutorRegistry *ExecutorRegistry
|
globalExecutorRegistry *ExecutorRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitExecutorRegistry 初始化执行器注册表并注册所有执行器
|
|
||||||
func InitExecutorRegistry() *ExecutorRegistry {
|
|
||||||
registryOnce.Do(func() {
|
|
||||||
registry := NewExecutorRegistry()
|
|
||||||
|
|
||||||
// 注册所有规则执行器
|
|
||||||
registry.Register(&bilibili.BilibiliDynamicExecutor{})
|
|
||||||
registry.Register(&bilibili.BilibiliPrivateMessageExecutor{})
|
|
||||||
registry.Register(&email.EmailExecutor{})
|
|
||||||
registry.Register(&github.GithubMakeRepoPublicExecutor{})
|
|
||||||
registry.Register(&onebot.OneBotExecutor{})
|
|
||||||
registry.Register(&dingtalk.DingTalkExecutor{})
|
|
||||||
// 未来添加新规则类型时,在这里注册即可
|
|
||||||
|
|
||||||
globalExecutorRegistry = registry
|
|
||||||
})
|
|
||||||
return globalExecutorRegistry
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalExecutorRegistry 获取全局执行器注册表
|
// GetGlobalExecutorRegistry 获取全局执行器注册表
|
||||||
func GetGlobalExecutorRegistry() *ExecutorRegistry {
|
func GetGlobalExecutorRegistry() *ExecutorRegistry {
|
||||||
if globalExecutorRegistry == nil {
|
if globalExecutorRegistry == nil {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/alidns"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/email"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/fwalert"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/github"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/rainyun"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitExecutorRegistry 初始化执行器注册表并注册所有执行器
|
||||||
|
func InitExecutorRegistry() *ExecutorRegistry {
|
||||||
|
registryOnce.Do(func() {
|
||||||
|
registry := NewExecutorRegistry()
|
||||||
|
|
||||||
|
// 注册所有规则执行器
|
||||||
|
registry.Register(&bilibili.BilibiliDynamicExecutor{})
|
||||||
|
registry.Register(&bilibili.BilibiliPrivateMessageExecutor{})
|
||||||
|
registry.Register(&email.EmailExecutor{})
|
||||||
|
registry.Register(&github.GithubMakeRepoPublicExecutor{})
|
||||||
|
registry.Register(&onebot.OneBotExecutor{})
|
||||||
|
registry.Register(&dingtalk.DingTalkExecutor{})
|
||||||
|
registry.Register(&fwalert.FwalertExecutor{})
|
||||||
|
registry.Register(&rainyun.RainyunWorkorderExecutor{})
|
||||||
|
registry.Register(&rainyun.RainyunRunAwayExecutor{})
|
||||||
|
registry.Register(&alidns.DeleteAliDNSRecordExecutor{})
|
||||||
|
// 未来添加新规则类型时,在这里注册即可
|
||||||
|
|
||||||
|
globalExecutorRegistry = registry
|
||||||
|
})
|
||||||
|
return globalExecutorRegistry
|
||||||
|
}
|
||||||
@@ -53,7 +53,18 @@ func Runner(timer *model.Timer) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteRule 执行单个规则并记录执行日志
|
// NewTask 组装执行一条规则所需的输入:规则本体 + 从数据库取出的关联账号。
|
||||||
|
//
|
||||||
|
// 数据库访问只发生在这一层,driver 拿到的 task 已经是自包含的。
|
||||||
|
func NewTask(rule *model.Rule) (*model.RuleTask, error) {
|
||||||
|
account, err := db.GetAccount(rule.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.RuleTask{Rule: rule, Account: account}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteRule 执行单条规则并记录执行日志
|
||||||
func ExecuteRule(rule *model.Rule, trigger string) error {
|
func ExecuteRule(rule *model.Rule, trigger string) error {
|
||||||
ctx, cancel := retry.ExecutionContext()
|
ctx, cancel := retry.ExecutionContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -62,24 +73,54 @@ func ExecuteRule(rule *model.Rule, trigger string) error {
|
|||||||
|
|
||||||
// ExecuteRuleWithContext 使用给定 context 执行规则,手动测试时可传入较短的超时
|
// ExecuteRuleWithContext 使用给定 context 执行规则,手动测试时可传入较短的超时
|
||||||
func ExecuteRuleWithContext(ctx context.Context, rule *model.Rule, trigger string) error {
|
func ExecuteRuleWithContext(ctx context.Context, rule *model.Rule, trigger string) error {
|
||||||
err := GetGlobalExecutorRegistry().Execute(ctx, rule)
|
task, err := NewTask(rule)
|
||||||
|
return execTask(ctx, task, err, &model.ExecutionLog{
|
||||||
message := "执行成功"
|
|
||||||
if err != nil {
|
|
||||||
message = err.Error()
|
|
||||||
log.Printf("执行规则失败 [ID: %d, Type: %s]: %v", rule.ID, rule.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logstore.Record(&model.ExecutionLog{
|
|
||||||
UID: rule.UID,
|
UID: rule.UID,
|
||||||
RuleID: rule.ID,
|
RuleID: rule.ID,
|
||||||
RuleName: rule.Name,
|
RuleName: rule.Name,
|
||||||
RuleType: rule.Type,
|
RuleType: rule.Type,
|
||||||
TimerID: rule.TimerID,
|
TimerID: rule.TimerID,
|
||||||
Trigger: trigger,
|
Trigger: trigger,
|
||||||
Success: err == nil,
|
|
||||||
Message: message,
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteGatewayRuleWithContext 执行一条消息网关规则,日志里带上网关信息。
|
||||||
|
//
|
||||||
|
// 与定时器规则走同一条执行链路,区别只有触发来源与日志字段。
|
||||||
|
func ExecuteGatewayRuleWithContext(ctx context.Context, rule *model.GatewayRule, trigger string) error {
|
||||||
|
task, err := NewTask(rule.AsRule())
|
||||||
|
return execTask(ctx, task, err, &model.ExecutionLog{
|
||||||
|
UID: rule.UID,
|
||||||
|
RuleName: rule.Name,
|
||||||
|
RuleType: rule.Type,
|
||||||
|
GatewayID: rule.GatewayID,
|
||||||
|
GatewayRuleID: rule.ID,
|
||||||
|
Trigger: trigger,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// execTask 执行规则并写入执行日志。
|
||||||
|
//
|
||||||
|
// taskErr 是组装输入时的错误(例如账号已被删除):这时不会走到 driver,
|
||||||
|
// 但同样要留下一条失败日志,方便用户在 WebUI 上看到原因。
|
||||||
|
func execTask(ctx context.Context, task *model.RuleTask, taskErr error, entry *model.ExecutionLog) error {
|
||||||
|
if taskErr != nil {
|
||||||
|
return record(entry, taskErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("执行规则 [%s] %s (触发来源: %s)", entry.RuleType, entry.RuleName, entry.Trigger)
|
||||||
|
return record(entry, GetGlobalExecutorRegistry().Execute(ctx, task))
|
||||||
|
}
|
||||||
|
|
||||||
|
// record 写入执行日志并返回原始错误
|
||||||
|
func record(entry *model.ExecutionLog, err error) error {
|
||||||
|
entry.Success = err == nil
|
||||||
|
entry.Message = "执行成功"
|
||||||
|
if err != nil {
|
||||||
|
entry.Message = err.Error()
|
||||||
|
log.Printf("执行规则失败 [%s] %s: %v", entry.RuleType, entry.RuleName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logstore.Record(entry)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package accountConfigChecker
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/alidns"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/email"
|
"github.com/ssdomei232/goodBaby/drivers/email"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/github"
|
"github.com/ssdomei232/goodBaby/drivers/github"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/rainyun"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -24,6 +26,8 @@ func InitValidatorRegistry() *ValidatorRegistry {
|
|||||||
r.Register(&email.EmailAccountConfigValidator{})
|
r.Register(&email.EmailAccountConfigValidator{})
|
||||||
r.Register(&github.GitHubAccountConfigValidator{})
|
r.Register(&github.GitHubAccountConfigValidator{})
|
||||||
r.Register(&onebot.OneBotAccountConfigValidator{})
|
r.Register(&onebot.OneBotAccountConfigValidator{})
|
||||||
|
r.Register(&rainyun.RainyunAccountConfigValidator{})
|
||||||
|
r.Register(&alidns.AliDNSAccountConfigValidator{})
|
||||||
|
|
||||||
registry = r
|
registry = r
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Package gateway 定义消息网关的扩展接口与注册表。
|
||||||
|
//
|
||||||
|
// 与 drivers 的分工:driver 负责把一条规则执行出去(发邮件、发消息……),
|
||||||
|
// gateway 负责把外部系统投递进来的消息派发给绑定在网关上的规则。
|
||||||
|
// 新增一种网关只要实现 Gateway 并在 reg.go 里注册。
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message 是外部系统投递进来的一条消息
|
||||||
|
type Message struct {
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result 是一次投递的汇总结果
|
||||||
|
type Result struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Failed []string `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task 是一次投递所需的全部输入:网关与待触发的规则都由上层从数据库取出
|
||||||
|
type Task struct {
|
||||||
|
Gateway *model.MessageGateway
|
||||||
|
Rules []model.GatewayRule
|
||||||
|
Message Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gateway 一种消息网关
|
||||||
|
type Gateway interface {
|
||||||
|
// GetType 获取网关类型标识
|
||||||
|
GetType() string
|
||||||
|
// Meta 返回给 WebUI 展示用的元数据
|
||||||
|
Meta() meta.GatewayMeta
|
||||||
|
// Deliver 把投递进来的消息派发给 task 里的规则
|
||||||
|
Deliver(ctx context.Context, task *Task) (*Result, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry 网关注册表
|
||||||
|
type Registry struct {
|
||||||
|
gateways map[string]Gateway
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegistry 创建新的网关注册表
|
||||||
|
func NewRegistry() *Registry {
|
||||||
|
return &Registry{
|
||||||
|
gateways: make(map[string]Gateway),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 注册消息网关
|
||||||
|
func (r *Registry) Register(g Gateway) {
|
||||||
|
r.gateways[g.GetType()] = g
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 按类型取网关实现,类型为空时回退到默认类型
|
||||||
|
func (r *Registry) Resolve(gatewayType string) (Gateway, bool) {
|
||||||
|
if gatewayType == "" {
|
||||||
|
gatewayType = model.GatewayTypeWebhook
|
||||||
|
}
|
||||||
|
|
||||||
|
g, exists := r.gateways[gatewayType]
|
||||||
|
return g, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver 按网关类型派发一次投递
|
||||||
|
func (r *Registry) Deliver(ctx context.Context, gatewayType string, task *Task) (*Result, error) {
|
||||||
|
g, exists := r.Resolve(gatewayType)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("不支持的消息网关类型: %s", gatewayType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.Deliver(ctx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedTypes 获取所有支持的网关类型
|
||||||
|
func (r *Registry) GetSupportedTypes() []string {
|
||||||
|
types := make([]string, 0, len(r.gateways))
|
||||||
|
for t := range r.gateways {
|
||||||
|
types = append(types, t)
|
||||||
|
}
|
||||||
|
sort.Strings(types)
|
||||||
|
return types
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metas 返回所有网关类型的元数据,按类型名排序
|
||||||
|
func (r *Registry) Metas() []meta.GatewayMeta {
|
||||||
|
metas := make([]meta.GatewayMeta, 0, len(r.gateways))
|
||||||
|
for _, g := range r.gateways {
|
||||||
|
metas = append(metas, g.Meta())
|
||||||
|
}
|
||||||
|
sort.Slice(metas, func(i, j int) bool { return metas[i].Type < metas[j].Type })
|
||||||
|
return metas
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
registry *Registry
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitGatewayRegistry 返回全局唯一的消息网关注册表
|
||||||
|
func InitGatewayRegistry() *Registry {
|
||||||
|
once.Do(func() {
|
||||||
|
r := NewRegistry()
|
||||||
|
|
||||||
|
// 注册所有消息网关
|
||||||
|
r.Register(&WebhookGateway{})
|
||||||
|
// 未来添加新网关类型时,在这里注册即可
|
||||||
|
|
||||||
|
registry = r
|
||||||
|
})
|
||||||
|
return registry
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewToken 生成一个消息网关的访问 Token
|
||||||
|
func NewToken() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "gw_" + hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/handler/runner"
|
||||||
|
"github.com/ssdomei232/goodBaby/internal/meta"
|
||||||
|
"github.com/ssdomei232/goodBaby/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// messageFields 规则配置里可以被投递消息覆盖的字段名
|
||||||
|
var messageFields = []string{"msg", "message", "body"}
|
||||||
|
|
||||||
|
// titleField 规则配置里可以被投递标题覆盖的字段名
|
||||||
|
const titleField = "title"
|
||||||
|
|
||||||
|
// TestMessage 是「测试」网关规则时使用的示例投递内容,
|
||||||
|
// 让用户在页面上就能看到消息被替换后的效果。
|
||||||
|
var TestMessage = Message{
|
||||||
|
Title: "goodBaby 测试标题",
|
||||||
|
Content: "这是一条来自消息网关的测试消息",
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebhookGateway 默认网关:外部系统 POST 一条消息来触发规则
|
||||||
|
type WebhookGateway struct{}
|
||||||
|
|
||||||
|
func (g *WebhookGateway) GetType() string {
|
||||||
|
return model.GatewayTypeWebhook
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *WebhookGateway) Meta() meta.GatewayMeta {
|
||||||
|
return meta.GatewayMeta{
|
||||||
|
Type: model.GatewayTypeWebhook,
|
||||||
|
Label: "Webhook",
|
||||||
|
Description: "为外部系统生成一个 Webhook 地址,POST 一条消息即可触发绑定在该网关上的规则。",
|
||||||
|
Docs: "docs/gateway-config.md",
|
||||||
|
PayloadHint: `{"title": "监控告警", "message": "服务已恢复"}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver 逐条执行绑定在网关上的规则
|
||||||
|
//
|
||||||
|
// 单条规则失败不影响其它规则,失败原因汇总在 Result 里返回。
|
||||||
|
func (g *WebhookGateway) Deliver(ctx context.Context, task *Task) (*Result, error) {
|
||||||
|
// 初始化为空切片:nil 切片会被序列化成 JSON null,前端读 failed.length 时会报错
|
||||||
|
failed := []string{}
|
||||||
|
|
||||||
|
for i := range task.Rules {
|
||||||
|
rule := task.Rules[i]
|
||||||
|
|
||||||
|
executed := rule
|
||||||
|
configJSON, applied, err := ApplyMessage(rule.ConfigJson, task.Message)
|
||||||
|
if err != nil {
|
||||||
|
failed = append(failed, rule.Name+": "+err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if applied {
|
||||||
|
// 用投递进来的消息执行,但不改动数据库里的规则配置
|
||||||
|
executed.ConfigJson = configJSON
|
||||||
|
}
|
||||||
|
if err := runner.ExecuteGatewayRuleWithContext(ctx, &executed, model.TriggerGateway); err != nil {
|
||||||
|
failed = append(failed, rule.Name+": "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Result{Total: len(task.Rules), Failed: failed}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyMessage 把投递进来的消息覆盖到规则配置的消息字段上。
|
||||||
|
//
|
||||||
|
// 返回覆盖后的配置;applied 为 false 表示配置里没有可覆盖的消息字段
|
||||||
|
// (例如「公开 GitHub 仓库」「删除 DNS 记录」),此时按规则里保存的配置执行。
|
||||||
|
func ApplyMessage(configJSON string, msg Message) (string, bool, error) {
|
||||||
|
var config map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
|
||||||
|
return configJSON, false, fmt.Errorf("解析规则配置失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matched := false
|
||||||
|
for _, key := range messageFields {
|
||||||
|
if _, ok := config[key]; ok {
|
||||||
|
if msg.Content != "" {
|
||||||
|
config[key] = msg.Content
|
||||||
|
}
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Title != "" {
|
||||||
|
if _, ok := config[titleField]; ok {
|
||||||
|
config[titleField] = msg.Title
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
return configJSON, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return configJSON, false, fmt.Errorf("生成规则配置失败: %v", err)
|
||||||
|
}
|
||||||
|
return string(out), true, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// GatewayMessagePlaceholder 是「由投递内容提供」的字段占位值。
|
||||||
|
//
|
||||||
|
// 这些字段在「网关规则」页面不由用户填写,真实内容来自 webhook 请求;
|
||||||
|
// 但规则配置里必须保留该字段,投递时才知道要覆盖哪一个。
|
||||||
|
const GatewayMessagePlaceholder = "由消息网关投递内容替换"
|
||||||
|
|
||||||
|
// GatewayMessageFields 返回所有由投递内容提供的字段
|
||||||
|
func GatewayMessageFields(fields []Field) []Field {
|
||||||
|
matched := make([]Field, 0, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.GatewayMessage {
|
||||||
|
matched = append(matched, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillGatewayMessages 给配置里「由投递内容提供」的字段补上占位值。
|
||||||
|
//
|
||||||
|
// 网关规则提交的配置里通常没有 title / msg 这类字段(前端不让填),
|
||||||
|
// 校验前先补齐,规则才会既通过校验、又能在投递时被覆盖。
|
||||||
|
func FillGatewayMessages(configJSON string, fields []Field) string {
|
||||||
|
messageFields := GatewayMessageFields(fields)
|
||||||
|
if len(messageFields) == 0 || configJSON == "" {
|
||||||
|
return configJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(configJSON), &raw); err != nil {
|
||||||
|
return configJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := false
|
||||||
|
for _, f := range messageFields {
|
||||||
|
if value, ok := raw[f.Key].(string); ok && value != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
raw[f.Key] = GatewayMessagePlaceholder
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return configJSON
|
||||||
|
}
|
||||||
|
|
||||||
|
filled, err := json.Marshal(raw)
|
||||||
|
if err != nil {
|
||||||
|
return configJSON
|
||||||
|
}
|
||||||
|
return string(filled)
|
||||||
|
}
|
||||||
@@ -26,8 +26,11 @@ type Field struct {
|
|||||||
Placeholder string `json:"placeholder,omitempty"`
|
Placeholder string `json:"placeholder,omitempty"`
|
||||||
Help string `json:"help,omitempty"`
|
Help string `json:"help,omitempty"`
|
||||||
// Secret 为 true 的字段在读取接口中会被掩码
|
// Secret 为 true 的字段在读取接口中会被掩码
|
||||||
Secret bool `json:"secret,omitempty"`
|
Secret bool `json:"secret,omitempty"`
|
||||||
Default any `json:"default,omitempty"`
|
// GatewayMessage 为 true 的字段由消息网关投递的内容提供(msg / title 之类):
|
||||||
|
// 在「网关规则」页面不需要用户填写,投递时会被请求里的 message / title 替换
|
||||||
|
GatewayMessage bool `json:"gateway_message,omitempty"`
|
||||||
|
Default any `json:"default,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountMeta 账号类型的元数据
|
// AccountMeta 账号类型的元数据
|
||||||
@@ -51,6 +54,16 @@ type RuleMeta struct {
|
|||||||
Fields []Field `json:"fields"`
|
Fields []Field `json:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GatewayMeta 消息网关类型的元数据
|
||||||
|
type GatewayMeta struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Docs string `json:"docs,omitempty"`
|
||||||
|
// PayloadHint WebUI 上展示的请求体示例
|
||||||
|
PayloadHint string `json:"payload_hint,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// SecretKeys 返回需要掩码的字段名
|
// SecretKeys 返回需要掩码的字段名
|
||||||
func SecretKeys(fields []Field) []string {
|
func SecretKeys(fields []Field) []string {
|
||||||
keys := make([]string, 0, len(fields))
|
keys := make([]string, 0, len(fields))
|
||||||
|
|||||||
@@ -31,6 +31,16 @@ func Do(ctx context.Context, op func() error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Permanent 标记一个重试没有意义的错误(例如凭据无效、配置错误)
|
||||||
|
//
|
||||||
|
// driver 里凡是重试也救不回来的失败都包一层,避免白白退避几个小时。
|
||||||
|
func Permanent(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backoff.Permanent(err)
|
||||||
|
}
|
||||||
|
|
||||||
// ExecutionTimeout 返回配置中的规则执行超时时间
|
// ExecutionTimeout 返回配置中的规则执行超时时间
|
||||||
func ExecutionTimeout() time.Duration {
|
func ExecutionTimeout() time.Duration {
|
||||||
config, err := configs.GetConfig()
|
config, err := configs.GetConfig()
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package ruleConfigChecker
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/alidns"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
"github.com/ssdomei232/goodBaby/drivers/bilibili"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
|
"github.com/ssdomei232/goodBaby/drivers/dingtalk"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/email"
|
"github.com/ssdomei232/goodBaby/drivers/email"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/fwalert"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/github"
|
"github.com/ssdomei232/goodBaby/drivers/github"
|
||||||
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
"github.com/ssdomei232/goodBaby/drivers/onebot"
|
||||||
|
"github.com/ssdomei232/goodBaby/drivers/rainyun"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -27,6 +30,10 @@ func InitValidatorRegistry() *ValidatorRegistry {
|
|||||||
r.Register(&onebot.OneBotRuleValidator{})
|
r.Register(&onebot.OneBotRuleValidator{})
|
||||||
r.Register(&dingtalk.DingTalkRuleValidator{})
|
r.Register(&dingtalk.DingTalkRuleValidator{})
|
||||||
r.Register(&github.GithubMakeRepositoryPublicRuleValidator{})
|
r.Register(&github.GithubMakeRepositoryPublicRuleValidator{})
|
||||||
|
r.Register(&fwalert.FwalertRuleValidator{})
|
||||||
|
r.Register(&rainyun.RainyunWorkorderRuleValidator{})
|
||||||
|
r.Register(&rainyun.RainyunRunAwayRuleValidator{})
|
||||||
|
r.Register(&alidns.AliDNSDeleteRecordRuleValidator{})
|
||||||
|
|
||||||
registry = r
|
registry = r
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/ssdomei232/goodBaby/api/account"
|
"github.com/ssdomei232/goodBaby/api/account"
|
||||||
"github.com/ssdomei232/goodBaby/api/admin"
|
"github.com/ssdomei232/goodBaby/api/admin"
|
||||||
"github.com/ssdomei232/goodBaby/api/dashboard"
|
"github.com/ssdomei232/goodBaby/api/dashboard"
|
||||||
|
"github.com/ssdomei232/goodBaby/api/gateway"
|
||||||
apilog "github.com/ssdomei232/goodBaby/api/log"
|
apilog "github.com/ssdomei232/goodBaby/api/log"
|
||||||
apimeta "github.com/ssdomei232/goodBaby/api/meta"
|
apimeta "github.com/ssdomei232/goodBaby/api/meta"
|
||||||
"github.com/ssdomei232/goodBaby/api/rule"
|
"github.com/ssdomei232/goodBaby/api/rule"
|
||||||
@@ -65,6 +66,7 @@ func main() {
|
|||||||
v1.GET("/site", apimeta.HandleGetSiteInfo)
|
v1.GET("/site", apimeta.HandleGetSiteInfo)
|
||||||
v1.POST("/user/registry", user.HandleRegistry)
|
v1.POST("/user/registry", user.HandleRegistry)
|
||||||
v1.POST("/user/login", user.HandleLogin)
|
v1.POST("/user/login", user.HandleLogin)
|
||||||
|
v1.POST("/gateways/:token/webhook", gateway.HandleWebhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 需要认证的路由组
|
// 需要认证的路由组
|
||||||
@@ -104,6 +106,15 @@ func main() {
|
|||||||
rules.DELETE("/:ruleID", rule.HandleDeleteRule)
|
rules.DELETE("/:ruleID", rule.HandleDeleteRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gatewayRules := authorized.Group("/gateway-rules")
|
||||||
|
{
|
||||||
|
gatewayRules.GET("/", rule.HandleGetAllGatewayRules)
|
||||||
|
gatewayRules.POST("/", rule.HandleCreateGatewayRule)
|
||||||
|
gatewayRules.PUT("/:ruleID", rule.HandleEditGatewayRule)
|
||||||
|
gatewayRules.POST("/:ruleID/test", rule.HandleTestGatewayRule)
|
||||||
|
gatewayRules.DELETE("/:ruleID", rule.HandleDeleteGatewayRule)
|
||||||
|
}
|
||||||
|
|
||||||
accounts := authorized.Group("/accounts")
|
accounts := authorized.Group("/accounts")
|
||||||
{
|
{
|
||||||
accounts.GET("/", account.HandleGetAllAccounts)
|
accounts.GET("/", account.HandleGetAllAccounts)
|
||||||
@@ -114,6 +125,13 @@ func main() {
|
|||||||
accounts.DELETE("/:accountID", account.HandleDeleteAccount)
|
accounts.DELETE("/:accountID", account.HandleDeleteAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gateways := authorized.Group("/gateways")
|
||||||
|
{
|
||||||
|
gateways.GET("/", gateway.HandleList)
|
||||||
|
gateways.POST("/", gateway.HandleCreate)
|
||||||
|
gateways.DELETE("/:gatewayID", gateway.HandleDelete)
|
||||||
|
}
|
||||||
|
|
||||||
logs := authorized.Group("/logs")
|
logs := authorized.Group("/logs")
|
||||||
{
|
{
|
||||||
logs.GET("/", apilog.HandleGetLogs)
|
logs.GET("/", apilog.HandleGetLogs)
|
||||||
@@ -151,7 +169,7 @@ func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
|
|||||||
c.Header("Access-Control-Allow-Origin", origin)
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
c.Header("Access-Control-Allow-Credentials", "true")
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
c.Header("Access-Control-Allow-Headers", "Content-Type")
|
c.Header("Access-Control-Allow-Headers", "Content-Type, X-API-Key, Authorization")
|
||||||
c.Header("Vary", "Origin")
|
c.Header("Vary", "Origin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// GatewayRule 是挂在消息网关上的通知规则。
|
||||||
|
//
|
||||||
|
// 与 Rule 的区别只有触发来源:Rule 由定时器到期触发,
|
||||||
|
// GatewayRule 由外部系统投递到网关的消息触发,两者互不干扰。
|
||||||
|
type GatewayRule struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UID uint `gorm:"index" json:"uid"`
|
||||||
|
GatewayID uint `gorm:"index" json:"gateway_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AccountID uint `json:"account_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ConfigJson string `json:"config_json"`
|
||||||
|
Enabled bool `gorm:"default:true" json:"enabled"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsRule 把网关规则换成统一的执行结构。
|
||||||
|
//
|
||||||
|
// 执行层只认识 Rule:投递消息前把外部消息覆盖到 ConfigJson 上,
|
||||||
|
// 再交给同一个 runner 执行即可。
|
||||||
|
func (r *GatewayRule) AsRule() *Rule {
|
||||||
|
return &Rule{
|
||||||
|
UID: r.UID,
|
||||||
|
Name: r.Name,
|
||||||
|
AccountID: r.AccountID,
|
||||||
|
Type: r.Type,
|
||||||
|
ConfigJson: r.ConfigJson,
|
||||||
|
Enabled: r.Enabled,
|
||||||
|
CreateAt: r.CreateAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GatewayRuleRequest 创建/编辑消息网关规则的请求体
|
||||||
|
type GatewayRuleRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
GatewayID uint `json:"gateway_id"`
|
||||||
|
AccountID uint `json:"account_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ConfigJson string `json:"config_json"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 校验网关规则请求中与类型无关的通用部分
|
||||||
|
func (r *GatewayRuleRequest) Validate() error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return ErrValidation("规则名称不能为空")
|
||||||
|
}
|
||||||
|
if len(r.Name) > 64 {
|
||||||
|
return ErrValidation("规则名称过长")
|
||||||
|
}
|
||||||
|
if r.Type == "" {
|
||||||
|
return ErrValidation("规则类型不能为空")
|
||||||
|
}
|
||||||
|
if r.GatewayID == 0 {
|
||||||
|
return ErrValidation("规则必须关联一个消息网关")
|
||||||
|
}
|
||||||
|
if r.ConfigJson == "" {
|
||||||
|
return ErrValidation("规则配置不能为空")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package model
|
|||||||
|
|
||||||
// 执行日志的触发来源
|
// 执行日志的触发来源
|
||||||
const (
|
const (
|
||||||
TriggerTimer = "timer" // Timer 到期自动触发
|
TriggerTimer = "timer" // Timer 到期自动触发
|
||||||
TriggerManual = "manual" // 用户在 WebUI 手动测试
|
TriggerManual = "manual" // 用户在 WebUI 手动测试
|
||||||
TriggerRemind = "remind" // 提醒通知
|
TriggerRemind = "remind" // 提醒通知
|
||||||
|
TriggerGateway = "gateway" // 外部系统投递到消息网关触发
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecutionLog 记录每一次规则执行/提醒的结果,供 WebUI 展示
|
// ExecutionLog 记录每一次规则执行/提醒的结果,供 WebUI 展示
|
||||||
@@ -15,8 +16,11 @@ type ExecutionLog struct {
|
|||||||
RuleName string `json:"rule_name"`
|
RuleName string `json:"rule_name"`
|
||||||
RuleType string `json:"rule_type"`
|
RuleType string `json:"rule_type"`
|
||||||
TimerID uint `json:"timer_id"`
|
TimerID uint `json:"timer_id"`
|
||||||
Trigger string `json:"trigger"`
|
// GatewayID / GatewayRuleID 只有消息网关投递触发时才有值
|
||||||
Success bool `json:"success"`
|
GatewayID uint `json:"gateway_id"`
|
||||||
Message string `json:"message"`
|
GatewayRuleID uint `json:"gateway_rule_id"`
|
||||||
CreateAt int64 `gorm:"index" json:"create_at"`
|
Trigger string `json:"trigger"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
CreateAt int64 `gorm:"index" json:"create_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// GatewayTypeWebhook 默认的网关类型:外部系统 POST 一条消息来触发规则
|
||||||
|
const GatewayTypeWebhook = "webhook"
|
||||||
|
|
||||||
|
type MessageGateway struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
UID uint `gorm:"index" json:"uid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Type 网关类型,见 internal/gateway 注册表
|
||||||
|
Type string `gorm:"default:webhook" json:"type"`
|
||||||
|
Token string `gorm:"uniqueIndex;size:67" json:"token"`
|
||||||
|
CreateAt int64 `json:"create_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageGatewayRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate 校验网关请求中与类型无关的通用部分
|
||||||
|
func (r *MessageGatewayRequest) Validate() error {
|
||||||
|
if r.Name == "" {
|
||||||
|
return ErrValidation("网关名称不能为空")
|
||||||
|
}
|
||||||
|
if len(r.Name) > 64 {
|
||||||
|
return ErrValidation("网关名称过长")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ func (r *RuleRequest) Validate() error {
|
|||||||
return ErrValidation("规则类型不能为空")
|
return ErrValidation("规则类型不能为空")
|
||||||
}
|
}
|
||||||
if r.TimerID == 0 {
|
if r.TimerID == 0 {
|
||||||
return ErrValidation("必须关联一个 Timer")
|
return ErrValidation("规则必须关联一个定时器")
|
||||||
}
|
}
|
||||||
if r.ConfigJson == "" {
|
if r.ConfigJson == "" {
|
||||||
return ErrValidation("规则配置不能为空")
|
return ErrValidation("规则配置不能为空")
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// RuleTask 是一次规则执行所需的全部输入。
|
||||||
|
//
|
||||||
|
// 规则本体与关联账号都由上层(handler/runner、消息网关)从数据库取出后传进来,
|
||||||
|
// driver 只负责把动作做出去,所以底层实现里不会再出现任何数据库操作。
|
||||||
|
type RuleTask struct {
|
||||||
|
// Rule 规则本体,永远不会为空
|
||||||
|
Rule *Rule
|
||||||
|
// Account 规则关联的账号;该规则类型不需要账号时为 nil
|
||||||
|
Account *Account
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountConfig 返回关联账号的配置 JSON,没有关联账号时返回空字符串
|
||||||
|
func (t *RuleTask) AccountConfig() string {
|
||||||
|
if t == nil || t.Account == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Account.Config
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ type User struct {
|
|||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
Username string `gorm:"uniqueIndex" json:"username"`
|
Username string `gorm:"uniqueIndex" json:"username"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
|
APIKey string `gorm:"index;size:67" json:"api_key"`
|
||||||
// 管理员可以修改系统配置。第一个注册的用户自动成为管理员。
|
// 管理员可以修改系统配置。第一个注册的用户自动成为管理员。
|
||||||
IsAdmin bool `gorm:"default:false" json:"is_admin"`
|
IsAdmin bool `gorm:"default:false" json:"is_admin"`
|
||||||
// 钉钉机器人配置(JSON 字符串),用于接收提醒
|
// 钉钉机器人配置(JSON 字符串),用于接收提醒
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
"element-plus": "^2.14.3",
|
"element-plus": "^2.14.5",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.39",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
@@ -286,9 +286,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -306,9 +303,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -326,9 +320,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -346,9 +337,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -366,9 +354,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -386,9 +371,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -513,7 +495,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/web-bluetooth": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "0.0.21",
|
"version": "0.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -727,14 +709,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/core": {
|
"node_modules/@vueuse/core": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.4.0.tgz",
|
||||||
"integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
|
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
"@vueuse/metadata": "14.3.0",
|
"@vueuse/metadata": "14.4.0",
|
||||||
"@vueuse/shared": "14.3.0"
|
"@vueuse/shared": "14.4.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
@@ -744,18 +726,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/metadata": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.4.0.tgz",
|
||||||
"integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
|
"integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vueuse/shared": {
|
"node_modules/@vueuse/shared": {
|
||||||
"version": "14.3.0",
|
"version": "14.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.4.0.tgz",
|
||||||
"integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
|
"integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
@@ -905,18 +887,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/element-plus": {
|
"node_modules/element-plus": {
|
||||||
"version": "2.14.3",
|
"version": "2.14.5",
|
||||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.3.tgz",
|
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.5.tgz",
|
||||||
"integrity": "sha512-pJcvxcpZjYruNzuJhAeVwnbYjfNgzBKnWHwSVEhwzM2/kcLI3brzmtIBxtPqd4hQWJfD1PRnjoc1WipLw2eBGg==",
|
"integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^4.2.0",
|
"@ctrl/tinycolor": "^4.2.0",
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@floating-ui/dom": "^1.7.6",
|
"@floating-ui/dom": "^1.8.0",
|
||||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
|
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
|
||||||
"@types/lodash": "^4.17.24",
|
"@types/lodash": "^4.17.24",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@vueuse/core": "14.3.0",
|
"@vueuse/core": "14.4.0",
|
||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
@@ -924,7 +906,7 @@
|
|||||||
"lodash-unified": "^1.0.3",
|
"lodash-unified": "^1.0.3",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-wheel-es": "^1.2.0",
|
"normalize-wheel-es": "^1.2.0",
|
||||||
"vue-component-type-helpers": "^3.3.5"
|
"vue-component-type-helpers": "^3.3.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.3.7"
|
"vue": "^3.3.7"
|
||||||
@@ -1322,9 +1304,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1346,9 +1325,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1370,9 +1346,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1394,9 +1367,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1854,9 +1824,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-component-type-helpers": {
|
"node_modules/vue-component-type-helpers": {
|
||||||
"version": "3.3.8",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz",
|
"resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz",
|
||||||
"integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==",
|
"integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
"element-plus": "^2.14.3",
|
"element-plus": "^2.14.5",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.39",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import type {
|
|||||||
AdminConfigResponse,
|
AdminConfigResponse,
|
||||||
DashboardOverview,
|
DashboardOverview,
|
||||||
ExecutionLog,
|
ExecutionLog,
|
||||||
|
GatewayRule,
|
||||||
|
GatewayRuleRequest,
|
||||||
LogPage,
|
LogPage,
|
||||||
|
MessageGateway,
|
||||||
Providers,
|
Providers,
|
||||||
Rule,
|
Rule,
|
||||||
RuleRequest,
|
RuleRequest,
|
||||||
|
AccountDeleteImpact,
|
||||||
SiteInfo,
|
SiteInfo,
|
||||||
Timer,
|
Timer,
|
||||||
TimerRequest,
|
TimerRequest,
|
||||||
@@ -65,13 +69,22 @@ export const ruleApi = {
|
|||||||
remove: (id: number) => api.delete<string>(`/rules/${id}`),
|
remove: (id: number) => api.delete<string>(`/rules/${id}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const gatewayRuleApi = {
|
||||||
|
list: (gatewayId?: number) =>
|
||||||
|
api.get<GatewayRule[]>('/gateway-rules/', gatewayId ? { gateway_id: gatewayId } : undefined),
|
||||||
|
create: (body: GatewayRuleRequest) => api.post<GatewayRule>('/gateway-rules/', body),
|
||||||
|
update: (id: number, body: GatewayRuleRequest) => api.put<GatewayRule>(`/gateway-rules/${id}`, body),
|
||||||
|
test: (id: number) => api.post<string>(`/gateway-rules/${id}/test`),
|
||||||
|
remove: (id: number) => api.delete<string>(`/gateway-rules/${id}`),
|
||||||
|
}
|
||||||
|
|
||||||
export const accountApi = {
|
export const accountApi = {
|
||||||
list: (type?: string) =>
|
list: (type?: string) =>
|
||||||
api.get<Account[]>('/accounts/', type ? { type } : undefined),
|
api.get<Account[]>('/accounts/', type ? { type } : undefined),
|
||||||
create: (body: AccountRequest) => api.post<Account>('/accounts/', body),
|
create: (body: AccountRequest) => api.post<Account>('/accounts/', body),
|
||||||
update: (id: number, body: AccountRequest) => api.put<Account>(`/accounts/${id}`, body),
|
update: (id: number, body: AccountRequest) => api.put<Account>(`/accounts/${id}`, body),
|
||||||
test: (id: number) => api.post<string>(`/accounts/${id}/test`),
|
test: (id: number) => api.post<string>(`/accounts/${id}/test`),
|
||||||
checkDelete: (id: number) => api.get<Rule[]>(`/accounts/${id}/check`),
|
checkDelete: (id: number) => api.get<AccountDeleteImpact>(`/accounts/${id}/check`),
|
||||||
remove: (id: number) => api.delete<string>(`/accounts/${id}`),
|
remove: (id: number) => api.delete<string>(`/accounts/${id}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +94,12 @@ export const logApi = {
|
|||||||
clear: () => api.delete<string>('/logs/'),
|
clear: () => api.delete<string>('/logs/'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const gatewayApi = {
|
||||||
|
list: () => api.get<MessageGateway[]>('/gateways/'),
|
||||||
|
create: (name: string, type: string) => api.post<MessageGateway>('/gateways/', { name, type }),
|
||||||
|
remove: (id: number) => api.delete<string>(`/gateways/${id}`),
|
||||||
|
}
|
||||||
|
|
||||||
export const adminApi = {
|
export const adminApi = {
|
||||||
getConfig: () => api.get<AdminConfigResponse>('/admin/config'),
|
getConfig: () => api.get<AdminConfigResponse>('/admin/config'),
|
||||||
updateConfig: (body: AdminConfig) => api.put<AdminConfig>('/admin/config', body),
|
updateConfig: (body: AdminConfig) => api.put<AdminConfig>('/admin/config', body),
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export interface UserInfo {
|
|||||||
create_at: number
|
create_at: number
|
||||||
username: string
|
username: string
|
||||||
is_admin: boolean
|
is_admin: boolean
|
||||||
dingtalk_config: string | null
|
dingtalk_config: string | null
|
||||||
|
api_key: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Timer {
|
export interface Timer {
|
||||||
@@ -48,6 +49,28 @@ export interface Rule {
|
|||||||
create_at: number
|
create_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 消息网关规则:由外部系统投递到网关的消息触发,与定时器规则分开存储 */
|
||||||
|
export interface GatewayRule {
|
||||||
|
id: number
|
||||||
|
uid: number
|
||||||
|
gateway_id: number
|
||||||
|
name: string
|
||||||
|
account_id: number
|
||||||
|
type: string
|
||||||
|
config_json: string
|
||||||
|
enabled: boolean
|
||||||
|
create_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageGateway {
|
||||||
|
id: number
|
||||||
|
uid: number
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
token: string
|
||||||
|
create_at: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface RuleRequest {
|
export interface RuleRequest {
|
||||||
name: string
|
name: string
|
||||||
timer_id: number
|
timer_id: number
|
||||||
@@ -57,6 +80,15 @@ export interface RuleRequest {
|
|||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GatewayRuleRequest {
|
||||||
|
name: string
|
||||||
|
gateway_id: number
|
||||||
|
account_id: number
|
||||||
|
type: string
|
||||||
|
config_json: string
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
id: number
|
id: number
|
||||||
uid: number
|
uid: number
|
||||||
@@ -89,6 +121,8 @@ export interface MetaField {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
help?: string
|
help?: string
|
||||||
secret?: boolean
|
secret?: boolean
|
||||||
|
/** 由消息网关投递的请求内容提供,网关规则页面不需要用户填写 */
|
||||||
|
gateway_message?: boolean
|
||||||
default?: unknown
|
default?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +144,18 @@ export interface RuleMeta {
|
|||||||
fields: MetaField[]
|
fields: MetaField[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GatewayMeta {
|
||||||
|
type: string
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
docs?: string
|
||||||
|
payload_hint?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Providers {
|
export interface Providers {
|
||||||
accounts: AccountMeta[]
|
accounts: AccountMeta[]
|
||||||
rules: RuleMeta[]
|
rules: RuleMeta[]
|
||||||
|
gateways: GatewayMeta[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionLog {
|
export interface ExecutionLog {
|
||||||
@@ -122,7 +165,9 @@ export interface ExecutionLog {
|
|||||||
rule_name: string
|
rule_name: string
|
||||||
rule_type: string
|
rule_type: string
|
||||||
timer_id: number
|
timer_id: number
|
||||||
trigger: 'timer' | 'manual' | 'remind'
|
gateway_id: number
|
||||||
|
gateway_rule_id: number
|
||||||
|
trigger: 'timer' | 'manual' | 'remind' | 'gateway'
|
||||||
success: boolean
|
success: boolean
|
||||||
message: string
|
message: string
|
||||||
create_at: number
|
create_at: number
|
||||||
@@ -140,6 +185,8 @@ export interface DashboardOverview {
|
|||||||
enabled_timers: number
|
enabled_timers: number
|
||||||
triggered_count: number
|
triggered_count: number
|
||||||
rule_count: number
|
rule_count: number
|
||||||
|
gateway_count: number
|
||||||
|
gateway_rule_count: number
|
||||||
account_count: number
|
account_count: number
|
||||||
server_time: number
|
server_time: number
|
||||||
recent_logs: ExecutionLog[]
|
recent_logs: ExecutionLog[]
|
||||||
@@ -147,6 +194,12 @@ export interface DashboardOverview {
|
|||||||
urgent_seconds_left?: number
|
urgent_seconds_left?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除账号时会被一并删除的规则 */
|
||||||
|
export interface AccountDeleteImpact {
|
||||||
|
rules: Rule[]
|
||||||
|
gateway_rules: GatewayRule[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface SiteInfo {
|
export interface SiteInfo {
|
||||||
name: string
|
name: string
|
||||||
enable_registry: boolean
|
enable_registry: boolean
|
||||||
|
|||||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 91 KiB |