init: ops-assistant codebase
This commit is contained in:
26
internal/core/ops/bootstrap.go
Normal file
26
internal/core/ops/bootstrap.go
Normal 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)
|
||||
}
|
||||
60
internal/core/ops/retry.go
Normal file
60
internal/core/ops/retry.go
Normal 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)
|
||||
}
|
||||
20
internal/core/ops/run_once.go
Normal file
20
internal/core/ops/run_once.go
Normal 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())
|
||||
}
|
||||
100
internal/core/ops/service.go
Normal file
100
internal/core/ops/service.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user