- core/module: 命令模板、门控(feature flag/confirm/dry-run)、ecode 错误标签、ResultFormatter - runbook executor: http.get/http.post/assert.json(expect_success 严格断言)、目标主机管理、任务落库、最终 stdout 直接回传 - cf 模块: /cf dns list 支持域名自动解析 zone_id;dns list/zones/workers list 格式化回传;API success:false 判真失败 - 全部 CF/CPA runbook 接入严格断言,杜绝假阳性 success - 新增 cpa/cfdav/mail/sub 模块与 runbooks;QQ 订阅到期提醒调度 - Web 后台: 模块设置(CF/CPA/cfdav/AI)、OPS 任务、渠道管理、审计页
254 lines
7.3 KiB
Go
254 lines
7.3 KiB
Go
package cfdav
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"ops-assistant/internal/core/command"
|
||
"ops-assistant/internal/core/ecode"
|
||
"ops-assistant/models"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type Module struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
type settings struct {
|
||
APIBase string
|
||
AdminUser string
|
||
AdminPass string
|
||
}
|
||
|
||
type userItem struct {
|
||
ID string `json:"id"`
|
||
Email string `json:"email"`
|
||
IsAdmin int `json:"is_admin"`
|
||
CreatedAt string `json:"created_at"`
|
||
}
|
||
|
||
type listResp struct {
|
||
Success bool `json:"success"`
|
||
Data []userItem `json:"data"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
type simpleResp struct {
|
||
Success bool `json:"success"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
func New(db *gorm.DB) *Module {
|
||
return &Module{db: db}
|
||
}
|
||
|
||
func (m *Module) Handle(userID int64, cmd *command.ParsedCommand) (string, error) {
|
||
text := strings.TrimSpace(cmd.Raw)
|
||
if text == "/cfdav" || strings.HasPrefix(text, "/cfdav help") {
|
||
return "cfdav 模块\n- /cfdav users\n- /cfdav add <email> <password> [admin]\n- /cfdav del <user_id>", nil
|
||
}
|
||
|
||
parts := strings.Fields(text)
|
||
if len(parts) == 0 {
|
||
return ecode.Tag(ecode.ErrStepFailed, "空命令"), nil
|
||
}
|
||
// 支持别名:/cfdav list
|
||
if len(parts) >= 2 && (parts[1] == "users" || parts[1] == "list") {
|
||
return m.listUsers()
|
||
}
|
||
if len(parts) >= 2 && (parts[1] == "add" || (len(parts) >= 3 && parts[1] == "user" && parts[2] == "add")) {
|
||
return m.addUser(parts)
|
||
}
|
||
if len(parts) >= 2 && (parts[1] == "del" || parts[1] == "delete" || (len(parts) >= 3 && parts[1] == "user" && (parts[2] == "del" || parts[2] == "delete"))) {
|
||
return m.deleteUser(parts)
|
||
}
|
||
return ecode.Tag(ecode.ErrStepFailed, "cfdav 模块已接入,当前支持:/cfdav users, /cfdav add, /cfdav del"), nil
|
||
}
|
||
|
||
func (m *Module) listUsers() (string, error) {
|
||
cfg, err := m.loadSettings()
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users"
|
||
resp, status, err := m.doRequest(http.MethodGet, url, nil, cfg)
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
if status >= 400 {
|
||
return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil
|
||
}
|
||
var out listResp
|
||
if err := json.Unmarshal([]byte(resp), &out); err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil
|
||
}
|
||
if !out.Success {
|
||
return ecode.Tag(ecode.ErrStepFailed, out.Error), nil
|
||
}
|
||
if len(out.Data) == 0 {
|
||
return "📭 cfdav 用户列表为空", nil
|
||
}
|
||
limit := len(out.Data)
|
||
if limit > 50 {
|
||
limit = 50
|
||
}
|
||
lines := make([]string, 0, limit+2)
|
||
lines = append(lines, fmt.Sprintf("✅ cfdav 用户列表(%d)", len(out.Data)))
|
||
for i := 0; i < limit; i++ {
|
||
u := out.Data[i]
|
||
admin := "user"
|
||
if u.IsAdmin == 1 {
|
||
admin = "admin"
|
||
}
|
||
lines = append(lines, fmt.Sprintf("- %s | %s | %s", u.ID, u.Email, admin))
|
||
}
|
||
if len(out.Data) > limit {
|
||
lines = append(lines, fmt.Sprintf("... 共 %d 个,已展示前 %d 个", len(out.Data), limit))
|
||
}
|
||
return strings.Join(lines, "\n"), nil
|
||
}
|
||
|
||
func (m *Module) addUser(parts []string) (string, error) {
|
||
cfg, err := m.loadSettings()
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
// 支持: /cfdav add <email> <pass> [admin]
|
||
// 或: /cfdav user add <email> <pass> [admin]
|
||
idx := 2
|
||
if len(parts) >= 3 && parts[1] == "user" {
|
||
idx = 3
|
||
}
|
||
if len(parts) < idx+2 {
|
||
return ecode.Tag(ecode.ErrStepFailed, "缺少参数,示例:/cfdav add <email> <password> [admin]"), nil
|
||
}
|
||
email := strings.TrimSpace(parts[idx])
|
||
pass := strings.TrimSpace(parts[idx+1])
|
||
if email == "" || pass == "" {
|
||
return ecode.Tag(ecode.ErrStepFailed, "email/password 不能为空"), nil
|
||
}
|
||
isAdmin := false
|
||
if len(parts) >= idx+3 {
|
||
flag := strings.ToLower(strings.TrimSpace(parts[idx+2]))
|
||
isAdmin = flag == "admin" || flag == "1" || flag == "true" || flag == "yes"
|
||
}
|
||
payload := map[string]any{"email": email, "password": pass, "isAdmin": isAdmin}
|
||
body, _ := json.Marshal(payload)
|
||
url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users"
|
||
resp, status, err := m.doRequest(http.MethodPost, url, body, cfg)
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
if status >= 400 {
|
||
return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil
|
||
}
|
||
var out simpleResp
|
||
if err := json.Unmarshal([]byte(resp), &out); err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil
|
||
}
|
||
if !out.Success {
|
||
return ecode.Tag(ecode.ErrStepFailed, out.Error), nil
|
||
}
|
||
return fmt.Sprintf("✅ cfdav 用户已创建:%s", email), nil
|
||
}
|
||
|
||
func (m *Module) deleteUser(parts []string) (string, error) {
|
||
cfg, err := m.loadSettings()
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
// 支持: /cfdav del <id> 或 /cfdav user del <id>
|
||
idx := 2
|
||
if len(parts) >= 3 && parts[1] == "user" {
|
||
idx = 3
|
||
}
|
||
if len(parts) < idx+1 {
|
||
return ecode.Tag(ecode.ErrStepFailed, "缺少参数,示例:/cfdav del <user_id>"), nil
|
||
}
|
||
id := strings.TrimSpace(parts[idx])
|
||
if id == "" {
|
||
return ecode.Tag(ecode.ErrStepFailed, "user_id 不能为空"), nil
|
||
}
|
||
url := strings.TrimRight(cfg.APIBase, "/") + "/api/admin/users/" + id
|
||
resp, status, err := m.doRequest(http.MethodDelete, url, nil, cfg)
|
||
if err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, err.Error()), nil
|
||
}
|
||
if status >= 400 {
|
||
return ecode.Tag(ecode.ErrStepFailed, fmt.Sprintf("upstream http %d: %s", status, resp)), nil
|
||
}
|
||
var out simpleResp
|
||
if err := json.Unmarshal([]byte(resp), &out); err != nil {
|
||
return ecode.Tag(ecode.ErrStepFailed, "响应解析失败"), nil
|
||
}
|
||
if !out.Success {
|
||
return ecode.Tag(ecode.ErrStepFailed, out.Error), nil
|
||
}
|
||
return fmt.Sprintf("✅ cfdav 用户已删除:%s", id), nil
|
||
}
|
||
|
||
func (m *Module) loadSettings() (*settings, error) {
|
||
if m.db == nil {
|
||
return nil, errors.New("db 未初始化")
|
||
}
|
||
keys := []string{"cfdav_api_base", "cfdav_admin_user", "cfdav_admin_pass"}
|
||
cfg := &settings{}
|
||
for _, k := range keys {
|
||
var sset models.AppSetting
|
||
val := ""
|
||
if err := m.db.Where("key = ?", k).First(&sset).Error; err == nil {
|
||
val = sset.Value
|
||
}
|
||
switch k {
|
||
case "cfdav_api_base":
|
||
cfg.APIBase = strings.TrimSpace(val)
|
||
case "cfdav_admin_user":
|
||
cfg.AdminUser = strings.TrimSpace(val)
|
||
case "cfdav_admin_pass":
|
||
cfg.AdminPass = val
|
||
}
|
||
}
|
||
if cfg.APIBase == "" {
|
||
return nil, errors.New("cfdav api_base 未配置")
|
||
}
|
||
if cfg.AdminUser == "" || cfg.AdminPass == "" {
|
||
return nil, errors.New("cfdav admin 账号未配置")
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
func (m *Module) doRequest(method, url string, body []byte, cfg *settings) (string, int, error) {
|
||
var reader io.Reader
|
||
if body != nil {
|
||
reader = strings.NewReader(string(body))
|
||
}
|
||
req, err := http.NewRequest(method, url, reader)
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
auth := cfg.AdminUser + ":" + cfg.AdminPass
|
||
b64 := base64.StdEncoding.EncodeToString([]byte(auth))
|
||
req.Header.Set("Authorization", "Basic "+b64)
|
||
if body != nil {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
}
|
||
client := &http.Client{Timeout: 15 * time.Second}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
defer resp.Body.Close()
|
||
data, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", resp.StatusCode, err
|
||
}
|
||
return string(data), resp.StatusCode, nil
|
||
}
|