package service import ( "context" "encoding/json" "strings" "sync" "time" "github.com/Tencent/WeKnora/internal/im" "github.com/Tencent/WeKnora/internal/im/wechat" "github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/types" "github.com/google/uuid" ) type digitalHumanChannels interface { InvalidateDeletedChannels([]string) ListChannelsByAgent(string, uint64) ([]im.IMChannel, error) GetChannelByIDAndTenant(string, uint64) (*im.IMChannel, error) CreateChannel(*im.IMChannel) error UpdateChannel(*im.IMChannel) error DeleteChannel(string, uint64) error } type digitalHumanQRProvider interface { GetLoginQRCode(context.Context) (*wechat.QRCodeResult, error) PollQRCodeStatus(context.Context, string) (*wechat.LoginResult, error) } type digitalHumanQRLogin struct { mu sync.Mutex scope types.DigitalHumanScope humanID string code string expires time.Time channel *im.IMChannelSummary } type DigitalHumanWeChatStatus struct { Status string `json:"status"` Channel *im.IMChannelSummary `json:"channel,omitempty"` } // Personal WeChat credentials are scoped to the creator even for tenant admins. func (s *DigitalHumanService) ownedHuman(ctx context.Context, id string) (*types.DigitalHumanDetail, types.DigitalHumanScope, error) { scope, err := digitalHumanScope(ctx) if err != nil { return nil, scope, err } human, err := s.repo.Get(ctx, scope, id) if err == nil && human.OwnerID != scope.OwnerID { return nil, scope, types.ErrDigitalHumanNotFound } return human, scope, err } func (s *DigitalHumanService) CheckOwner(ctx context.Context, id string) error { _, _, err := s.ownedHuman(ctx, id) return err } func (s *DigitalHumanService) WeChatChannels(ctx context.Context, id string) ([]im.IMChannelSummary, error) { human, scope, err := s.ownedHuman(ctx, id) if err != nil { return nil, err } channels, err := s.channels.ListChannelsByAgent(human.PrimaryAgentID, scope.TenantID) if err != nil { return nil, err } result := make([]im.IMChannelSummary, 0, len(channels)) for _, channel := range channels { if channel.Platform == "wechat" { result = append(result, im.SummarizeIMChannel(channel)) } } return result, nil } func (s *DigitalHumanService) WeChatQRCode(ctx context.Context, id string) (*wechat.QRCodeResult, error) { _, scope, err := s.ownedHuman(ctx, id) if err != nil { return nil, err } result, err := s.qr.GetLoginQRCode(ctx) if err != nil { logger.Errorf(ctx, "Digital human WeChat QR request failed: %v", err) return nil, digitalHumanInputError("微信二维码获取失败,请检查网络后重试") } if result == nil || result.QRCode == "" || result.QRCodeURL == "" { return nil, digitalHumanInputError("微信未返回有效二维码,请重试") } s.qrMu.Lock() defer s.qrMu.Unlock() if s.qrLogins == nil { s.qrLogins = make(map[string]*digitalHumanQRLogin) } for token, login := range s.qrLogins { if time.Now().After(login.expires) { delete(s.qrLogins, token) } } if len(s.qrLogins) >= 1024 { return nil, digitalHumanInputError("正在连接的微信账号较多,请稍后重试") } // The browser receives an owner-bound ticket, never an upstream polling token. token := uuid.NewString() s.qrLogins[token] = &digitalHumanQRLogin{scope: scope, humanID: id, code: result.QRCode, expires: time.Now().Add(5 * time.Minute)} return &wechat.QRCodeResult{QRCodeURL: result.QRCodeURL, QRCode: token}, nil } func (s *DigitalHumanService) WeChatPoll(ctx context.Context, id, ticket string) (*DigitalHumanWeChatStatus, error) { human, scope, err := s.ownedHuman(ctx, id) if err != nil { return nil, err } s.qrMu.Lock() login := s.qrLogins[ticket] s.qrMu.Unlock() if login == nil || login.scope != scope || login.humanID != id || time.Now().After(login.expires) { return nil, digitalHumanInputError("二维码不存在、已过期或不属于当前账号,请重新连接") } login.mu.Lock() defer login.mu.Unlock() if time.Now().After(login.expires) { return nil, digitalHumanInputError("二维码已过期,请重新连接") } if login.channel != nil { if _, err := s.ownedWeChatChannel(ctx, id, login.channel.ID); err != nil { return nil, err } return &DigitalHumanWeChatStatus{Status: "confirmed", Channel: login.channel}, nil } result, err := s.qr.PollQRCodeStatus(ctx, login.code) if err != nil { logger.Errorf(ctx, "Digital human WeChat status request failed: %v", err) return nil, digitalHumanInputError("微信连接状态获取失败,请重试") } if result == nil { return nil, digitalHumanInputError("微信未返回连接状态,请重试") } if result.Status != "confirmed" { return &DigitalHumanWeChatStatus{Status: result.Status}, nil } if result.BotToken == "" || result.ILinkBotID == "" { return nil, digitalHumanInputError("微信授权未返回完整凭证,请重新扫码") } // Recheck after the long poll in case the human was deleted while scanning. if err := s.CheckOwner(ctx, id); err != nil { return nil, err } credentials, _ := json.Marshal(map[string]string{ "bot_token": result.BotToken, "ilink_bot_id": result.ILinkBotID, "ilink_user_id": result.ILinkUserID, "baseurl": result.BaseURL, }) channel := &im.IMChannel{TenantID: scope.TenantID, AgentID: human.PrimaryAgentID, Platform: "wechat", Name: human.Name + " · 微信", Mode: "longpoll", OutputMode: "full", SessionMode: "user", Enabled: true, Credentials: types.JSON(credentials)} if err := s.channels.CreateChannel(channel); err != nil { logger.Errorf(ctx, "Digital human WeChat channel creation failed: %v", err) if strings.HasPrefix(err.Error(), "duplicate_bot:") { return nil, digitalHumanInputError("此微信账号已经连接,请先删除原连接或使用其他账号扫码") } return nil, digitalHumanInputError("微信已确认,但保存连接失败,请重试") } summary := im.SummarizeIMChannel(*channel) login.channel = &summary return &DigitalHumanWeChatStatus{Status: "confirmed", Channel: login.channel}, nil } func (s *DigitalHumanService) ownedWeChatChannel(ctx context.Context, humanID, channelID string) (*im.IMChannel, error) { human, scope, err := s.ownedHuman(ctx, humanID) if err != nil { return nil, err } channel, err := s.channels.GetChannelByIDAndTenant(channelID, scope.TenantID) if err != nil || channel == nil || channel.AgentID != human.PrimaryAgentID || channel.Platform != "wechat" { return nil, types.ErrDigitalHumanNotFound } return channel, nil } func (s *DigitalHumanService) DeleteWeChatChannel(ctx context.Context, humanID, channelID string) error { channel, err := s.ownedWeChatChannel(ctx, humanID, channelID) if err != nil { return err } return s.channels.DeleteChannel(channel.ID, channel.TenantID) } func (s *DigitalHumanService) SetWeChatChannelEnabled(ctx context.Context, humanID, channelID string, enabled bool) error { channel, err := s.ownedWeChatChannel(ctx, humanID, channelID) if err != nil { return err } channel.Enabled = enabled return s.channels.UpdateChannel(channel) }