Files
openclaw 81939825d1 feat: 模块化命令框架 + runbook 严格断言与查询结果回传
- 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 任务、渠道管理、审计页
2026-09-13 07:42:17 +08:00

128 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sub
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
type reportSummaryResp struct {
Success bool `json:"success"`
Data struct {
Total int `json:"total"`
Currency []struct {
Currency string `json:"currency"`
Cnt int `json:"cnt"`
Total float64 `json:"total"`
} `json:"currency"`
Due struct {
Overdue int `json:"overdue"`
Due7 int `json:"due7"`
Due30 int `json:"due30"`
} `json:"due"`
} `json:"data"`
Error string `json:"error"`
}
type reportListResp struct {
Success bool `json:"success"`
Data []map[string]any `json:"data"`
Error string `json:"error"`
}
func (m *Module) report(userID int64, args []string) (string, error) {
cfg, err := m.loadSettings()
if err != nil {
return "❌ " + err.Error(), nil
}
typeName := "summary"
days := ""
if len(args) >= 1 {
typeName = strings.ToLower(strings.TrimSpace(args[0]))
}
if len(args) >= 2 {
days = strings.TrimSpace(args[1])
}
base := strings.TrimRight(cfg.APIBase, "/") + "/api/report?user_id=1&channel=qq&type=" + typeName
if days != "" {
base += "&days=" + days
}
body, status, err := m.doRequest(http.MethodGet, base, nil, cfg)
if err != nil {
return "❌ 报表请求失败: " + err.Error(), nil
}
if status >= 400 {
return fmt.Sprintf("❌ 报表失败: http %d %s", status, body), nil
}
switch typeName {
case "summary":
var out reportSummaryResp
if err := json.Unmarshal([]byte(body), &out); err != nil {
return "❌ 报表解析失败", nil
}
if !out.Success {
return "❌ " + out.Error, nil
}
lines := []string{fmt.Sprintf("📊 订阅总览:%d 项", out.Data.Total)}
if len(out.Data.Currency) > 0 {
lines = append(lines, "💰 按币种汇总:")
for _, c := range out.Data.Currency {
lines = append(lines, fmt.Sprintf("- %s: %.2f(%d项)", c.Currency, c.Total, c.Cnt))
}
}
lines = append(lines, fmt.Sprintf("⏰ 到期:7天内=%d,30天内=%d,已逾期=%d", out.Data.Due.Due7, out.Data.Due.Due30, out.Data.Due.Overdue))
return strings.Join(lines, "\n"), nil
case "category", "cycle", "due":
var out reportListResp
if err := json.Unmarshal([]byte(body), &out); err != nil {
return "❌ 报表解析失败", nil
}
if !out.Success {
return "❌ " + out.Error, nil
}
if len(out.Data) == 0 {
return "📭 无数据", nil
}
lines := []string{fmt.Sprintf("📊 报表(%s):%d 条", typeName, len(out.Data))}
max := len(out.Data)
if max > 30 {
max = 30
}
for i := 0; i < max; i++ {
r := out.Data[i]
switch typeName {
case "category":
lines = append(lines, fmt.Sprintf("- %v | %v | 数量:%v | 金额:%.2f", r["category"], r["currency"], r["cnt"], toF64(r["total"])))
case "cycle":
lines = append(lines, fmt.Sprintf("- %v x%v | 数量:%v", r["cycle_unit"], r["cycle_value"], r["cnt"]))
case "due":
lines = append(lines, fmt.Sprintf("- %v | %v | 分类:%v | 到期:%v | 周期:%v x%v | %.2f %v", r["id"], r["name"], r["category"], r["due_date"], r["cycle_unit"], r["cycle_value"], toF64(r["amount"]), r["currency"]))
}
}
if len(out.Data) > max {
lines = append(lines, fmt.Sprintf("... 共%d条,仅显示前%d条", len(out.Data), max))
}
return strings.Join(lines, "\n"), nil
default:
return "❌ 用法:/sub report | /sub report category | /sub report cycle | /sub report due <7|30|overdue>", nil
}
}
func toF64(v any) float64 {
switch t := v.(type) {
case float64:
return t
case string:
f, _ := strconv.ParseFloat(t, 64)
return f
case int:
return float64(t)
default:
return 0
}
}