Files
ai_site/platform/internal/logic/applogic/sync_heal.go
whm 362dcee242 feat: heal dead sync channels and block deleting system default (Z12h)
Ensure default channel and rebind Agent/Binding on list, ticket, agents/me, and ensure-binding so SyncPage does not stay on deleted-channel errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 08:57:30 +08:00

132 lines
4.2 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 applogic
import (
"context"
"fmt"
"log"
"strings"
"sync"
"aijianzhan/platform/internal/agentstore"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/svc"
)
// HealResult Z12h 自愈结果。
type HealResult struct {
ChannelID string `json:"channel_id"`
BindingsFixed int `json:"bindings_fixed"`
AgentsFixed int `json:"agents_fixed"`
ChannelCreated bool `json:"channel_created,omitempty"`
}
var healTenantMu sync.Map // tenantID -> *sync.Mutex避免 SyncPage 并行列表双建默认通道
func lockTenantHeal(tenantID int64) func() {
v, _ := healTenantMu.LoadOrStore(tenantID, &sync.Mutex{})
mu := v.(*sync.Mutex)
mu.Lock()
return mu.Unlock
}
// channelAlive 通道是否仍属于该租户。
func channelAlive(store *dbsync.FileStore, tenantID int64, channelID string) bool {
id := strings.TrimSpace(channelID)
if id == "" || store == nil || tenantID <= 0 {
return false
}
_, err := store.GetChannelForTenant(id, tenantID)
return err == nil
}
func agentNeedsSyncHeal(store *dbsync.FileStore, a agentstore.Account) bool {
cid := strings.TrimSpace(a.ChannelID)
alive := channelAlive(store, a.TenantID, cid)
if cid != "" {
if !alive {
return true // 挂死通道
}
return strings.TrimSpace(a.OnlineDBID) == "" // 通道在但缺 online
}
// 无通道:仅 active与 Z12c 启用即绑一致),勿动 pending
return a.Status == agentstore.StatusActive || a.Status == ""
}
// HealTenantSyncBind Z12h确保公司默认同步通道存在并运行重挂挂死通道的 Agent/Binding。
// online_db_id 有值则保留(同一 remote 上的逻辑库名不变);仅改 channel_id。
func HealTenantSyncBind(ctx context.Context, svcCtx *svc.ServiceContext, tenantID int64) (*HealResult, error) {
if svcCtx == nil || svcCtx.DBSync == nil || tenantID <= 0 {
return nil, nil
}
unlock := lockTenantHeal(tenantID)
defer unlock()
cfg := svcCtx.Config.DBSync
driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver))
if driver == "" {
driver = dbsync.DriverPostgres
}
before, _ := svcCtx.DBSync.Store().FindSystemDefaultChannel(tenantID)
ch, err := svcCtx.DBSync.EnsureAndStartSystemDefaultChannel(dbsync.DefaultChannelOpts{
TenantID: tenantID,
RemoteDriver: driver,
RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN),
Name: fmt.Sprintf("公司默认同步 #%d", tenantID),
})
if err != nil {
return nil, err
}
out := &HealResult{
ChannelID: ch.ID,
ChannelCreated: before == nil || before.ID != ch.ID,
}
// Binding通道空或已删除 → 改挂默认通道(保留 online_db_id
list, err := svcCtx.DBSync.Store().ListBindings(tenantID, "")
if err == nil {
for _, b := range list {
if channelAlive(svcCtx.DBSync.Store(), tenantID, b.ChannelID) {
continue
}
b.ChannelID = ch.ID
if strings.TrimSpace(b.OnlineDBID) == "" {
b.OnlineDBID = dbsync.ResolveOnlineDBID("", ch.ID)
}
if _, e := svcCtx.DBSync.Store().EnsureBinding(b); e != nil {
log.Printf("dbsync heal binding tenant=%d local=%s: %v", tenantID, b.LocalDatabaseID, e)
continue
}
out.BindingsFixed++
}
}
// Agent挂死通道 / 缺 online / active 无通道 → AttachSyncBind保留 online_db_id
if svcCtx.Agents != nil {
agents, err := svcCtx.Agents.List(ctx, tenantID)
if err == nil {
for i := range agents {
a := agents[i]
if !agentNeedsSyncHeal(svcCtx.DBSync.Store(), a) {
continue
}
online := strings.TrimSpace(a.OnlineDBID)
if online == "" {
online = dbsync.ResolveOnlineDBID("", ch.ID)
}
dbName := strings.TrimSpace(a.DatabaseName)
if dbName == "" {
dbName = fmt.Sprintf("agent_%d", a.AgentID)
}
if _, e := svcCtx.Agents.AttachSyncBind(ctx, a.AgentID, tenantID, ch.ID, online, dbName, a.Status == agentstore.StatusActive || a.Status == ""); e != nil {
log.Printf("dbsync heal agent tenant=%d agent=%d: %v", tenantID, a.AgentID, e)
continue
}
out.AgentsFixed++
}
}
}
if out.BindingsFixed > 0 || out.AgentsFixed > 0 || out.ChannelCreated {
log.Printf("dbsync heal tenant=%d channel=%s bindings=%d agents=%d created=%v",
tenantID, ch.ID, out.BindingsFixed, out.AgentsFixed, out.ChannelCreated)
}
return out, nil
}