feat: sync AI compose and ChatBot features

This commit is contained in:
A 2026-07-03 19:43:20 +08:00
parent 35e5d38f4d
commit b7269b135f
75 changed files with 5426 additions and 123 deletions

View file

@ -42,8 +42,12 @@ type userStickerSetInstaller interface {
InstallUserStickerSet(ctx context.Context, userID int64, setID int64, kind domain.StickerSetKind, archived bool, installedDate int) error
}
type aiChatGenerator interface {
GenerateTextStream(ctx context.Context, req domain.AITextGenerationRequest, emit func(domain.AIComposeText) error) (domain.AIComposeText, error)
}
// RouterHooks 是 rpc 层回调router 创建后经 SetRouterHooks 延迟注入,打破
// router↔bots 的构造循环;两个能力都依赖 TL/连接层边界,不能在 app 层实现):
// router↔bots 的构造循环;这些能力都依赖 TL/连接层边界,不能在 app 层实现):
// - RevokeBotSessionstoken revoke 后撤销 bot 的全部已登录 session
// authorization + 强制断连)。
// - PushBotCommandsChanged命令变更后给在线相关用户推 updateBotCommands
@ -56,24 +60,34 @@ type RouterHooks interface {
PushStickerSetsChanged(ctx context.Context, userID int64, kind domain.StickerSetKind)
}
// TextDraftPusher 推送 @ChatBot AI 流式回复的 transient 文本草稿,由 rpc 层转换为
// UpdateUserTyping/sendMessageTextDraftAction。它独立于普通 bot hooks避免 BotFather
// 和 @Stickers 的测试/依赖被 AI 对话能力污染。
type TextDraftPusher interface {
PushBotTextDraft(ctx context.Context, botUserID, userID, randomID int64, text string)
}
// replyLockStripes 是回复串行化条带数:同一用户的 BotFather 回复落同一条带、
// 串行执行(状态机 RMW 原子 + 回复保序),不同用户并发;固定大小不随用户数增长。
const replyLockStripes = 256
// Service 提供 bot 账号业务。
type Service struct {
users store.UserStore
bots store.BotStore
messages store.MessageStore
blocker blockChecker
channels publicChannelUsernameResolver
stickers stickerSetCreator
installer userStickerSetInstaller
hooks RouterHooks
userCache store.UserCache
cache *botProfileCache
log *zap.Logger
now func() time.Time
users store.UserStore
bots store.BotStore
messages store.MessageStore
blocker blockChecker
channels publicChannelUsernameResolver
stickers stickerSetCreator
installer userStickerSetInstaller
aiChat aiChatGenerator
hooks RouterHooks
textDrafts TextDraftPusher
userCache store.UserCache
cache *botProfileCache
log *zap.Logger
now func() time.Time
chatBotStreamThrottle time.Duration
// replySeq 是回复 randomID 在 crypto/rand 失败时的兜底单调序列。
replySeq atomic.Int64
replyLocks [replyLockStripes]sync.Mutex
@ -150,6 +164,24 @@ func WithUserStickerSets(c userStickerSetInstaller) Option {
}
}
// WithAIChatGenerator 注入内置 @ChatBot 使用的 AI 文本生成器。
func WithAIChatGenerator(g aiChatGenerator) Option {
return func(s *Service) {
if g != nil {
s.aiChat = g
}
}
}
// WithAIChatStreamThrottle 调整 @ChatBot 流式草稿推送的最小时间间隔(测试用)。
func WithAIChatStreamThrottle(d time.Duration) Option {
return func(s *Service) {
if d >= 0 {
s.chatBotStreamThrottle = d
}
}
}
// invalidateUserCache 在 bot 的 users 行变更(含 version bump后清缓存。
// 失效失败只记日志:缓存最长 TTL 后自愈,不阻塞写路径。
func (s *Service) invalidateUserCache(ctx context.Context, botUserID int64) {
@ -197,15 +229,29 @@ func (s *Service) SetRouterHooks(h RouterHooks) {
}
}
// SetTextDraftPusher 注入 @ChatBot 流式草稿推送边界。
func (s *Service) SetTextDraftPusher(p TextDraftPusher) {
if s != nil {
s.textDrafts = p
}
}
func (s *Service) SetAIChatGenerator(g aiChatGenerator) {
if s != nil {
s.aiChat = g
}
}
// NewService 创建 bots 服务。
func NewService(users store.UserStore, bots store.BotStore, messages store.MessageStore, opts ...Option) *Service {
s := &Service{
users: users,
bots: bots,
messages: messages,
cache: newBotProfileCache(botProfileCacheMaxEntries, botProfileCacheTTL),
log: zap.NewNop(),
now: time.Now,
users: users,
bots: bots,
messages: messages,
cache: newBotProfileCache(botProfileCacheMaxEntries, botProfileCacheTTL),
log: zap.NewNop(),
now: time.Now,
chatBotStreamThrottle: defaultChatBotStreamThrottle,
}
for _, opt := range opts {
opt(s)