Files
dobotai-platform/internal/adminweb/control.go
T
2026-08-14 12:12:33 +08:00

206 lines
7.7 KiB
Go

package adminweb
import (
"context"
"errors"
"strings"
"time"
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/dingtalkconfig"
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/secretstore"
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/settings"
)
type ConfigRepository interface {
ActiveDingTalkConfig(context.Context, string) (string, string, bool, error)
CandidateDingTalkConfig(context.Context, string) (string, string, error)
SaveDingTalkConfig(context.Context, string, string, string, bool) error
MarkDingTalkCandidateFailed(context.Context, string, string, string) error
AdministratorCount(context.Context, string) (int, error)
ActiveBindings(context.Context, string) (map[string]settings.ResourceBinding, error)
CreateBootstrapChallenge(context.Context, string, string, time.Time) error
ConfirmBootstrap(context.Context, string) error
}
type AdministratorRepository interface {
ListAdministrators(context.Context, string) ([]settings.Administrator, error)
DisableAdministrator(context.Context, string, string) error
}
type CandidateActivator interface{ ActivateCandidate(context.Context) error }
type OperationalStatusProvider interface {
Components(context.Context) ([]Component, error)
}
type CredentialVerifier interface {
Verify(context.Context, string, string) dingtalkconfig.Result
}
type ControlService struct {
tenant string
repo ConfigRepository
secrets *secretstore.Store
verifier CredentialVerifier
activator CandidateActivator
status OperationalStatusProvider
}
func NewControlService(tenant string, repo ConfigRepository, secrets *secretstore.Store, verifier CredentialVerifier) (*ControlService, error) {
if strings.TrimSpace(tenant) == "" || repo == nil || secrets == nil || verifier == nil {
return nil, errors.New("admin control dependencies are required")
}
return &ControlService{tenant: strings.TrimSpace(tenant), repo: repo, secrets: secrets, verifier: verifier}, nil
}
func (s *ControlService) Snapshot(ctx context.Context) (Snapshot, error) {
id, ref, verified, err := s.repo.ActiveDingTalkConfig(ctx, s.tenant)
if err != nil {
return Snapshot{}, err
}
admins, err := s.repo.AdministratorCount(ctx, s.tenant)
if err != nil {
return Snapshot{}, err
}
bindings, err := s.repo.ActiveBindings(ctx, s.tenant)
if err != nil {
return Snapshot{}, err
}
administratorViews := []AdministratorView{}
if administratorRepo, ok := s.repo.(AdministratorRepository); ok {
administrators, listErr := administratorRepo.ListAdministrators(ctx, s.tenant)
if listErr != nil {
return Snapshot{}, listErr
}
activeCount := 0
for _, administrator := range administrators {
if administrator.Status == "active" {
activeCount++
}
}
for _, administrator := range administrators {
administratorViews = append(administratorViews, AdministratorView{
StaffID: administrator.StaffID, StaffIDMasked: maskID(administrator.StaffID),
DisplayName: administrator.DisplayName, Department: administrator.Department,
Role: administrator.Role, Status: administrator.Status,
CanDisable: administrator.Status == "active" && activeCount > 1,
})
}
}
state := settings.DetermineState(settings.Snapshot{ClientID: id, SecretConfigured: s.secrets.Configured(ref), CredentialsVerified: verified, AdministratorCount: admins})
now := time.Now()
components := []Component{
{Name: "配置仓库", State: "ok", Code: "repository_ok", Detail: "PostgreSQL配置查询成功", LastSuccess: now, CheckedAt: now},
{Name: "钉钉OAuth", State: status(verified), Code: ternaryCode(verified, "oauth_verified", "oauth_unconfigured"), Detail: "当前应用凭据验证状态", CheckedAt: now},
{Name: "管理员", State: countStatus(admins), Code: ternaryCode(admins > 0, "admin_configured", "admin_missing"), Detail: itoa(admins) + " 位有效管理员", CheckedAt: now},
{Name: "技术支持群绑定", State: bindingStatus(bindings, "support_group"), Code: bindingCode(bindings, "support_group"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
{Name: "问题AI表格绑定", State: bindingStatus(bindings, "question_table"), Code: bindingCode(bindings, "question_table"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
{Name: "FAQ表格绑定", State: bindingStatus(bindings, "faq_table"), Code: bindingCode(bindings, "faq_table"), Detail: "仅显示是否存在active绑定", CheckedAt: now},
}
if s.status != nil {
operational, probeErr := s.status.Components(ctx)
if probeErr != nil {
components = append(components, Component{Name: "运行探针", State: "error", Code: "component_probe_failed", Detail: "运行状态探针失败;未显示原始错误", CheckedAt: time.Now()})
} else {
components = append(components, operational...)
}
}
return Snapshot{SystemState: string(state), ClientIDMasked: maskID(id), SecretConfigured: s.secrets.Configured(ref), Components: components, Administrators: administratorViews}, nil
}
func (s *ControlService) VerifyCredentials(ctx context.Context, id, secret string) Verification {
r := s.verifier.Verify(ctx, id, secret)
return Verification{OK: r.OK, Code: r.Code, CheckedAt: r.CheckedAt}
}
func (s *ControlService) SaveCredentials(ctx context.Context, id, secret string) error {
_, oldRef, _ := s.repo.CandidateDingTalkConfig(ctx, s.tenant)
ref, err := s.secrets.Put("dingtalk", secret)
if err != nil {
return err
}
if err = s.repo.SaveDingTalkConfig(ctx, s.tenant, strings.TrimSpace(id), ref, false); err != nil {
_ = s.secrets.Delete(ref)
return err
}
if oldRef != "" && oldRef != ref {
_ = s.secrets.Delete(oldRef)
}
return nil
}
func (s *ControlService) SetCandidateActivator(activator CandidateActivator) { s.activator = activator }
func (s *ControlService) SetOperationalStatusProvider(provider OperationalStatusProvider) {
s.status = provider
}
func (s *ControlService) ActivateCandidate(ctx context.Context) error {
if s.activator == nil {
return errors.New("candidate activator unavailable")
}
_, ref, err := s.repo.CandidateDingTalkConfig(ctx, s.tenant)
if err != nil {
return err
}
if err = s.activator.ActivateCandidate(ctx); err != nil {
_ = s.repo.MarkDingTalkCandidateFailed(ctx, s.tenant, ref, "activation_failed")
return errors.New("candidate activation failed")
}
return nil
}
func (s *ControlService) CreateBootstrap(ctx context.Context, code string) error {
return s.repo.CreateBootstrapChallenge(ctx, s.tenant, code, time.Now().Add(10*time.Minute))
}
func (s *ControlService) ConfirmBootstrap(ctx context.Context) error {
return s.repo.ConfirmBootstrap(ctx, s.tenant)
}
func (s *ControlService) DisableAdministrator(ctx context.Context, staffID string) error {
staffID = strings.TrimSpace(staffID)
if staffID == "" {
return settings.ErrAdministratorNotFound
}
repo, ok := s.repo.(AdministratorRepository)
if !ok {
return errors.New("administrator management unavailable")
}
return repo.DisableAdministrator(ctx, s.tenant, staffID)
}
func maskID(v string) string {
v = strings.TrimSpace(v)
if len(v) <= 6 {
return v
}
return v[:3] + "***" + v[len(v)-3:]
}
func status(ok bool) string {
if ok {
return "ok"
}
return "unconfigured"
}
func countStatus(n int) string {
if n > 0 {
return "ok"
}
return "unconfigured"
}
func bindingStatus(m map[string]settings.ResourceBinding, k string) string {
if _, ok := m[k]; ok {
return "configured"
}
return "unconfigured"
}
func bindingCode(m map[string]settings.ResourceBinding, k string) string {
if _, ok := m[k]; ok {
return "binding_active"
}
return "binding_missing"
}
func ternaryCode(ok bool, success, failure string) string {
if ok {
return success
}
return failure
}
func itoa(n int) string {
if n == 0 {
return "0"
}
b := make([]byte, 0, 8)
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
return string(b)
}