package applogic import ( "encoding/json" "fmt" "strings" "time" "aijianzhan/platform/internal/agentstore" "aijianzhan/platform/internal/authx" "aijianzhan/platform/internal/bindcodestore" "aijianzhan/platform/internal/dbsync" "aijianzhan/platform/internal/userstore" ) // AgentMe Z12b:智能体自查绑定(无需「管理智能体」)。 func (l *AuthLogic) AgentMe() (*agentstore.Account, error) { if !authx.IsAgent(authx.Role(l.ctx)) { return nil, fmt.Errorf("仅智能体可访问") } if l.svcCtx.Agents == nil { return nil, fmt.Errorf("agent store unavailable") } tid := authx.TenantID(l.ctx) aid := authx.UserID(l.ctx) acc, err := l.svcCtx.Agents.Get(l.ctx, tid, aid) if err != nil { return nil, err } return acc, nil } type BindCodeCreateReq struct { ChannelID string `json:"channel_id"` OnlineDBID string `json:"online_db_id"` DatabaseName string `json:"database_name"` MaxUses int `json:"max_uses"` ExpiresHours int `json:"expires_hours"` // 0=168h Note string `json:"note"` } func (l *AuthLogic) CreateBindCode(req BindCodeCreateReq) (*bindcodestore.BindCode, error) { if l.svcCtx.BindCodes == nil { return nil, fmt.Errorf("bind code store unavailable") } tid := authx.TenantID(l.ctx) if tid <= 0 { return nil, fmt.Errorf("未加入公司") } channelID := strings.TrimSpace(req.ChannelID) online := strings.TrimSpace(req.OnlineDBID) dbName := strings.TrimSpace(req.DatabaseName) if channelID == "" && l.svcCtx.DBSync != nil { if ch, err := l.svcCtx.DBSync.Store().FindSystemDefaultChannel(tid); err == nil && ch != nil { channelID = ch.ID if online == "" { online = dbsync.ResolveOnlineDBID("", ch.ID) } } else { // 尝试创建默认同步通道 cfg := l.svcCtx.Config.DBSync driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver)) if driver == "" { driver = dbsync.DriverPostgres } saved, err := l.svcCtx.DBSync.Store().EnsureSystemDefaultChannel(dbsync.DefaultChannelOpts{ TenantID: tid, RemoteDriver: driver, RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN), }) if err != nil { return nil, fmt.Errorf("无默认同步通道:%w", err) } channelID = saved.ID if online == "" { online = dbsync.ResolveOnlineDBID("", saved.ID) } } } if channelID == "" { return nil, fmt.Errorf("请先启用智能体以生成默认同步通道,或指定 channel_id") } if online == "" { online = dbsync.ResolveOnlineDBID("", channelID) } exp := time.Duration(req.ExpiresHours) * time.Hour return l.svcCtx.BindCodes.Create(l.ctx, tid, authx.UserID(l.ctx), bindcodestore.CreateInput{ ChannelID: channelID, OnlineDBID: online, DatabaseName: dbName, MaxUses: req.MaxUses, ExpiresIn: exp, Note: req.Note, }) } func (l *AuthLogic) ListBindCodes() ([]bindcodestore.BindCode, error) { if l.svcCtx.BindCodes == nil { return nil, fmt.Errorf("bind code store unavailable") } tid := authx.TenantID(l.ctx) return l.svcCtx.BindCodes.List(l.ctx, tid) } func (l *AuthLogic) RevokeBindCode(code string) error { if l.svcCtx.BindCodes == nil { return fmt.Errorf("bind code store unavailable") } return l.svcCtx.BindCodes.Revoke(l.ctx, authx.TenantID(l.ctx), code) } type BindCodeRedeemReq struct { Code string `json:"code"` HostKey string `json:"host_key"` Name string `json:"name"` // 可选:无 agent 时注册用 } type BindCodeRedeemResp struct { OK bool `json:"ok"` TenantID int64 `json:"tenant_id"` AgentID int64 `json:"agent_id"` ClientID string `json:"client_id,omitempty"` ChannelID string `json:"channel_id"` OnlineDBID string `json:"online_db_id"` DatabaseName string `json:"database_name,omitempty"` SyncBound bool `json:"sync_bound"` Message string `json:"message,omitempty"` } func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp, error) { if l.svcCtx.BindCodes == nil || l.svcCtx.Agents == nil { return nil, fmt.Errorf("bind service unavailable") } code := strings.TrimSpace(req.Code) hostKey := strings.TrimSpace(req.HostKey) if code == "" || hostKey == "" { return nil, fmt.Errorf("code and host_key required") } bc, err := l.svcCtx.BindCodes.Redeem(l.ctx, code) if err != nil { return nil, err } acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey) if err != nil { name := strings.TrimSpace(req.Name) if name == "" { name = "离线终端" } created, secret, _, regErr := l.svcCtx.Agents.Register(l.ctx, bc.TenantID, name, hostKey) if regErr != nil { return nil, fmt.Errorf("register agent: %w", regErr) } _ = secret acc = created } online := strings.TrimSpace(bc.OnlineDBID) if online == "" { online = dbsync.ResolveOnlineDBID("", bc.ChannelID) } dbName := strings.TrimSpace(bc.DatabaseName) if dbName == "" { dbName = fmt.Sprintf("agent_%d", acc.AgentID) } updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, bc.TenantID, bc.ChannelID, online, dbName, true) if err != nil { return nil, err } _ = l.writeBindAudit("bind_code_redeem", updated.TenantID, updated.AgentID, map[string]any{ "code": bc.Code, "channel_id": bc.ChannelID, "online_db_id": online, }) return &BindCodeRedeemResp{ OK: true, TenantID: updated.TenantID, AgentID: updated.AgentID, ClientID: updated.ClientID, ChannelID: updated.ChannelID, OnlineDBID: updated.OnlineDBID, DatabaseName: updated.DatabaseName, SyncBound: true, Message: "绑定成功", }, nil } type PhoneLookupReq struct { Phone string `json:"phone"` } type PhoneLookupResp struct { Exists bool `json:"exists"` TenantID int64 `json:"tenant_id,omitempty"` TenantName string `json:"tenant_name,omitempty"` MaskedName string `json:"masked_name,omitempty"` NeedConfirm bool `json:"need_confirm"` Message string `json:"message,omitempty"` } func maskDisplayName(name string) string { name = strings.TrimSpace(name) if name == "" { return "***" } r := []rune(name) if len(r) == 1 { return string(r[0]) + "*" } if len(r) == 2 { return string(r[0]) + "*" } return string(r[0]) + strings.Repeat("*", len(r)-2) + string(r[len(r)-1]) } func (l *AuthLogic) PhoneLookup(req PhoneLookupReq) (*PhoneLookupResp, error) { if l.svcCtx.Users == nil { return nil, fmt.Errorf("user store unavailable") } phone, err := userstore.NormalizePhone(req.Phone) if err != nil { return &PhoneLookupResp{Exists: false, Message: "手机号格式不正确"}, nil } u, err := l.svcCtx.Users.GetByPhone(l.ctx, phone) if err != nil || u == nil { return &PhoneLookupResp{Exists: false, NeedConfirm: false, Message: "无此成员;请使用绑定码或联系管理员"}, nil } if u.TenantID <= 0 { return &PhoneLookupResp{Exists: true, NeedConfirm: false, Message: "该手机号账号尚未加入公司"}, nil } tenantName := "" if t, err := l.svcCtx.Users.GetTenant(l.ctx, u.TenantID); err == nil && t != nil { tenantName = t.Name } return &PhoneLookupResp{ Exists: true, TenantID: u.TenantID, TenantName: tenantName, MaskedName: maskDisplayName(u.DisplayName), NeedConfirm: true, Message: fmt.Sprintf("已找到账号「%s」所属「%s」,是否绑定到本机?", maskDisplayName(u.DisplayName), tenantName), }, nil } type PhoneConfirmReq struct { Phone string `json:"phone"` HostKey string `json:"host_key"` Name string `json:"name"` Confirm bool `json:"confirm"` // 必须 true LocalDBID string `json:"local_database_id"` } type PhoneConfirmResp struct { OK bool `json:"ok"` TenantID int64 `json:"tenant_id"` AgentID int64 `json:"agent_id"` ChannelID string `json:"channel_id"` OnlineDBID string `json:"online_db_id"` DatabaseName string `json:"database_name,omitempty"` SyncBound bool `json:"sync_bound"` Message string `json:"message,omitempty"` } func (l *AuthLogic) PhoneConfirm(req PhoneConfirmReq) (*PhoneConfirmResp, error) { if !req.Confirm { return nil, fmt.Errorf("须明确确认绑定(confirm=true)") } if l.svcCtx.Users == nil || l.svcCtx.Agents == nil || l.svcCtx.DBSync == nil { return nil, fmt.Errorf("bind service unavailable") } phone, err := userstore.NormalizePhone(req.Phone) if err != nil { return nil, fmt.Errorf("手机号格式不正确") } hostKey := strings.TrimSpace(req.HostKey) if hostKey == "" { return nil, fmt.Errorf("host_key required") } u, err := l.svcCtx.Users.GetByPhone(l.ctx, phone) if err != nil || u == nil { return nil, fmt.Errorf("无此成员;请使用绑定码") } if u.TenantID <= 0 { return nil, fmt.Errorf("该账号尚未加入公司") } cfg := l.svcCtx.Config.DBSync driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver)) if driver == "" { driver = dbsync.DriverPostgres } ch, err := l.svcCtx.DBSync.Store().EnsureSystemDefaultChannel(dbsync.DefaultChannelOpts{ TenantID: u.TenantID, RemoteDriver: driver, RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN), }) if err != nil { return nil, fmt.Errorf("ensure channel: %w", err) } acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey) if err != nil { name := strings.TrimSpace(req.Name) if name == "" { name = "离线终端 · " + maskDisplayName(u.DisplayName) } created, _, _, regErr := l.svcCtx.Agents.Register(l.ctx, u.TenantID, name, hostKey) if regErr != nil { return nil, regErr } acc = created } online := dbsync.ResolveOnlineDBID("", ch.ID) // 个人落点:按用户隔离 online_db_id online = fmt.Sprintf("%s_u%d", online, u.UserID) dbName := fmt.Sprintf("%s", strings.TrimSpace(u.DisplayName)) if dbName == "" { dbName = fmt.Sprintf("user_%d", u.UserID) } updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true) if err != nil { return nil, err } localID := strings.TrimSpace(req.LocalDBID) if localID == "" { localID = "host:" + hostKey } _, _ = l.svcCtx.DBSync.Store().EnsureBinding(dbsync.Binding{ TenantID: u.TenantID, UserID: u.UserID, LocalDatabaseID: localID, OnlineDBID: online, ChannelID: ch.ID, DatabaseName: dbName, DisplayName: dbName, Note: "phone-confirm", }) _ = l.writeBindAudit("phone_confirm_bind", u.TenantID, updated.AgentID, map[string]any{ "phone": phone, "user_id": u.UserID, "channel_id": ch.ID, "online_db_id": online, }) return &PhoneConfirmResp{ OK: true, TenantID: updated.TenantID, AgentID: updated.AgentID, ChannelID: updated.ChannelID, OnlineDBID: updated.OnlineDBID, DatabaseName: updated.DatabaseName, SyncBound: true, Message: "绑定成功", }, nil } func (l *AuthLogic) writeBindAudit(action string, tenantID, agentID int64, detail map[string]any) error { if l.svcCtx.Audit == nil { return nil } if detail == nil { detail = map[string]any{} } detail["agent_id"] = agentID b, _ := json.Marshal(detail) return l.svcCtx.Audit.Log(l.ctx, tenantID, authx.UserID(l.ctx), action, string(b)) }