Files
ops-assistant/internal/core/module/template.go
T
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

109 lines
2.7 KiB
Go
Raw 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 module
import (
"fmt"
"strings"
"time"
"ops-assistant/internal/core/policy"
"ops-assistant/internal/core/runbook"
)
type CommandTemplate struct {
RunbookName string
Gate Gate
InputsFn func(text string, parts []string) (map[string]string, error)
MetaFn func(userID int64, confirmToken string, inputs map[string]string) runbook.RunMeta
DryRunMsg string
SuccessMsg func(jobID uint) string
// ResultFormatter 可选:查询类命令用最终 stdout 格式化回传内容。
// 返回 (消息, true) 表示使用格式化结果;返回 (anything, false) 回退到回执式消息。
ResultFormatter func(out string) (string, bool)
}
type CommandSpec struct {
Prefixes []string
Template CommandTemplate
ErrPrefix string
ErrHint string
}
func ExecTemplate(runner *Runner, userID int64, raw string, tpl CommandTemplate) (uint, string, error) {
dryRun, confirmToken := policy.ParseCommonFlags(raw)
parts := strings.Fields(strings.TrimSpace(raw))
inputs := map[string]string{}
if tpl.InputsFn != nil {
out, err := tpl.InputsFn(raw, parts)
if err != nil {
return 0, "", err
}
inputs = out
}
meta := runbook.NewMeta()
if tpl.MetaFn != nil {
meta = tpl.MetaFn(userID, confirmToken, inputs)
}
if meta.RequestID == "" {
meta.RequestID = fmt.Sprintf("ops-u%d-%d", userID, time.Now().Unix())
}
req := Request{
RunbookName: tpl.RunbookName,
Inputs: inputs,
Meta: meta,
Gate: tpl.Gate,
DryRun: dryRun,
ConfirmToken: confirmToken,
}
jobID, out, err := runner.Run(raw, userID, req)
if err != nil {
return jobID, out, err
}
if tpl.ResultFormatter != nil && out != "" && out != "dry-run" {
if msg, ok := tpl.ResultFormatter(out); ok {
return jobID, msg, nil
}
}
return jobID, out, err
}
func FormatDryRunMessage(tpl CommandTemplate) string {
if tpl.DryRunMsg != "" {
return tpl.DryRunMsg
}
return fmt.Sprintf("🧪 dry-run: 将执行 %s(未实际执行)", tpl.RunbookName)
}
func FormatSuccessMessage(tpl CommandTemplate, jobID uint) string {
if tpl.SuccessMsg != nil {
return tpl.SuccessMsg(jobID)
}
return fmt.Sprintf("✅ %s 已执行,job=%d", tpl.RunbookName, jobID)
}
func MatchAnyPrefix(text string, prefixes []string) bool {
text = strings.TrimSpace(text)
for _, p := range prefixes {
if strings.HasPrefix(text, p) {
return true
}
}
return false
}
func MatchCommand(text string, specs []CommandSpec) (CommandSpec, bool) {
for _, sp := range specs {
if MatchAnyPrefix(text, sp.Prefixes) {
return sp, true
}
}
return CommandSpec{}, false
}
func FormatExecError(sp CommandSpec, err error) string {
msg := sp.ErrPrefix + err.Error()
if sp.ErrHint != "" {
msg += "(示例:" + sp.ErrHint + ")"
}
return msg
}