init: ops-assistant codebase

This commit is contained in:
OpenClaw Agent
2026-03-19 21:23:28 +08:00
commit 81deba4766
94 changed files with 10767 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
package ops
import (
"path/filepath"
"ops-assistant/internal/core/registry"
"ops-assistant/internal/core/runbook"
"ops-assistant/internal/module/cf"
"ops-assistant/internal/module/cpa"
"ops-assistant/internal/module/mail"
"gorm.io/gorm"
)
func BuildDefault(db *gorm.DB, dbPath, baseDir string) *Service {
r := registry.New()
exec := runbook.NewExecutor(db, filepath.Join(baseDir, "runbooks"))
cpaModule := cpa.New(db, exec)
cfModule := cf.New(db, exec)
mailModule := mail.New(db, exec)
r.RegisterModule("cpa", cpaModule.Handle)
r.RegisterModule("cf", cfModule.Handle)
r.RegisterModule("mail", mailModule.Handle)
return NewService(dbPath, baseDir, r)
}

View File

@@ -0,0 +1,60 @@
package ops
import (
"encoding/json"
"errors"
"path/filepath"
"strings"
"ops-assistant/internal/core/runbook"
"ops-assistant/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func decodeInputJSON(raw string, out *map[string]string) error {
if strings.TrimSpace(raw) == "" {
return nil
}
return json.Unmarshal([]byte(raw), out)
}
func RetryJobWithDB(db *gorm.DB, baseDir string, jobID uint) (uint, error) {
if db == nil {
return 0, errors.New("db is nil")
}
var old models.OpsJob
if err := db.First(&old, jobID).Error; err != nil {
return 0, err
}
if strings.TrimSpace(old.Status) != "failed" {
return 0, errors.New("only failed jobs can retry")
}
inputs := map[string]string{}
if strings.TrimSpace(old.InputJSON) != "" {
_ = decodeInputJSON(old.InputJSON, &inputs)
}
meta := runbook.NewMeta()
meta.Target = old.Target
meta.RiskLevel = old.RiskLevel
meta.RequestID = old.RequestID + "-retry"
meta.ConfirmHash = old.ConfirmHash
exec := runbook.NewExecutor(db, filepath.Join(baseDir, "runbooks"))
newID, _, err := exec.RunWithInputsAndMeta(old.Command, old.Runbook, old.Operator, inputs, meta)
if err != nil {
return newID, err
}
return newID, nil
}
func RetryJob(dbPath, baseDir string, jobID uint) (uint, error) {
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return 0, err
}
return RetryJobWithDB(db, baseDir, jobID)
}

View File

@@ -0,0 +1,20 @@
package ops
import (
"path/filepath"
"ops-assistant/internal/core/runbook"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// RunOnce executes a runbook directly without bot/channel.
func RunOnce(dbPath, baseDir, commandText, runbookName string, operator int64, inputs map[string]string) (uint, string, error) {
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return 0, "", err
}
exec := runbook.NewExecutor(db, filepath.Join(baseDir, "runbooks"))
return exec.RunWithInputsAndMeta(commandText, runbookName, operator, inputs, runbook.NewMeta())
}

View File

@@ -0,0 +1,100 @@
package ops
import (
"fmt"
"strings"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"ops-assistant/internal/core/command"
coremodule "ops-assistant/internal/core/module"
"ops-assistant/internal/core/registry"
)
type Service struct {
dbPath string
baseDir string
registry *registry.Registry
db *gorm.DB
}
func NewService(dbPath, baseDir string, reg *registry.Registry) *Service {
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
return &Service{dbPath: dbPath, baseDir: baseDir, registry: reg, db: db}
}
func (s *Service) Handle(userID int64, text string) (bool, string) {
if !strings.HasPrefix(strings.TrimSpace(text), "/") {
return false, ""
}
cmd, _, err := command.ParseWithInputs(text)
if err != nil {
return false, ""
}
// 通用帮助
if cmd.Module == "help" || cmd.Name == "/help" || cmd.Name == "/start" {
return true, s.helpText()
}
if cmd.Module == "ops" && (len(cmd.Args) == 0 || cmd.Args[0] == "help") {
return true, s.helpText()
}
if cmd.Module == "ops" && len(cmd.Args) > 0 && cmd.Args[0] == "modules" {
return true, s.modulesStatusText()
}
if cmd.Module != "" && cmd.Module != "ops" && s.db != nil {
if !coremodule.IsEnabled(s.db, cmd.Module) {
return true, fmt.Sprintf("[ERR_FEATURE_DISABLED] 模块未启用: %s开关: enable_module_%s", cmd.Module, cmd.Module)
}
}
out, handled, err := s.registry.Handle(userID, cmd)
if !handled {
return false, ""
}
if err != nil {
return true, "❌ OPS 执行失败: " + err.Error()
}
return true, out
}
func (s *Service) helpText() string {
lines := []string{
"🛠️ OPS 交互命令:",
"- /ops modules (查看模块启用状态)",
"- /cpa help",
"- /cpa status",
"- /cpa usage backup",
"- /cpa usage restore <backup_id> [--confirm YES_RESTORE] [--dry-run]",
"- /cf status (需要 enable_module_cf",
"- /cf zones (需要 enable_module_cf",
"- /cf dns list <zone_id> (需要 enable_module_cf",
"- /cf dns update <zone_id> <record_id> <type> <name> <content> [ttl] [proxied:true|false] (需要 enable_module_cf",
"- /cf dnsadd <name> <content> [on|off] [type] (需要 enable_module_cf",
"- /cf dnsset <record_id> <content> [true] (需要 enable_module_cf",
"- /cf dnsdel <record_id> YES (需要 enable_module_cf",
"- /cf dnsproxy <record_id|name> on|off (需要 enable_module_cf",
"- /cf workers list (需要 enable_module_cf",
"- /mail status (需要 enable_module_mail",
}
return strings.Join(lines, "\n")
}
func (s *Service) modulesStatusText() string {
mods := s.registry.ListModules()
if len(mods) == 0 {
return "暂无已注册模块"
}
lines := []string{"🧩 模块状态:"}
for _, m := range mods {
enabled := false
if s.db != nil {
enabled = coremodule.IsEnabled(s.db, m)
}
state := "disabled"
if enabled {
state = "enabled"
}
lines = append(lines, fmt.Sprintf("- %s: %s", m, state))
}
lines = append(lines, "\n可用命令/ops modules")
return strings.Join(lines, "\n")
}