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

149
internal/domain/ai.go Normal file
View file

@ -0,0 +1,149 @@
package domain
import "errors"
const (
// AICompose 最大文本长度沿用普通消息正文限制,避免 provider 请求被客户端巨大值打爆。
MaxAIComposeTextLength = MaxMessageTextLength
// MaxAIComposeEntityCount 限制 TextWithEntities 的实体数量。
MaxAIComposeEntityCount = MaxMessageEntityCount
// AICompose tone 的 appConfig 默认值与 TDesktop/DrKLO 消费点一致。
MaxAIComposeToneTitleLength = 12
MaxAIComposeTonePromptLength = 1024
AIComposeToneSavedLimitDefault = 5
AIComposeToneSavedLimitPremium = 20
AIComposeToneExamplesNum = 3
)
var (
ErrAIComposeDisabled = errors.New("ai compose disabled")
ErrAIComposeInvalid = errors.New("ai compose invalid")
ErrAIComposeToneInvalid = errors.New("ai compose tone invalid")
ErrAIComposeToneNotFound = errors.New("ai compose tone not found")
ErrAIComposeToneLimitExceeded = errors.New("ai compose tone limit exceeded")
ErrAIComposeRateLimited = errors.New("ai compose rate limited")
ErrAIComposeProviderUnavailable = errors.New("ai compose provider unavailable")
ErrAIComposeProviderTimeout = errors.New("ai compose provider timeout")
)
type AIComposeText struct {
Text string
Entities []MessageEntity
}
func (t AIComposeText) Clone() AIComposeText {
out := t
out.Entities = append([]MessageEntity(nil), t.Entities...)
return out
}
type AIComposeToneRefKind string
const (
AIComposeToneRefDefault AIComposeToneRefKind = "default"
AIComposeToneRefID AIComposeToneRefKind = "id"
AIComposeToneRefSlug AIComposeToneRefKind = "slug"
)
type AIComposeToneRef struct {
Kind AIComposeToneRefKind
DefaultTone string
ID int64
AccessHash int64
Slug string
}
func (r AIComposeToneRef) Empty() bool {
return r.Kind == "" && r.DefaultTone == "" && r.ID == 0 && r.Slug == ""
}
type AIComposeRequest struct {
UserID int64
Text AIComposeText
Proofread bool
Emojify bool
TranslateToLang string
Tone AIComposeToneRef
}
type AIComposeResult struct {
ResultText AIComposeText
DiffText *AIComposeText
}
type AITextGenerationRequest struct {
UserID int64
Text AIComposeText
Instruction string
}
type AIComposeTone struct {
Default bool
Creator bool
ID int64
AccessHash int64
OwnerUserID int64
Slug string
Title string
EmojiID int64
Prompt string
InstallsCount int
AuthorID int64
DisplayAuthor bool
CreatedAt int64
UpdatedAt int64
Saved bool
ExampleEnglish *AIComposeToneExample
}
func (t AIComposeTone) Clone() AIComposeTone {
out := t
if t.ExampleEnglish != nil {
ex := t.ExampleEnglish.Clone()
out.ExampleEnglish = &ex
}
return out
}
type AIComposeToneExample struct {
From AIComposeText
To AIComposeText
}
func (e AIComposeToneExample) Clone() AIComposeToneExample {
return AIComposeToneExample{
From: e.From.Clone(),
To: e.To.Clone(),
}
}
type AIComposeToneInput struct {
UserID int64
DisplayAuthor bool
EmojiID int64
Title string
Prompt string
}
type AIComposeToneUpdate struct {
Ref AIComposeToneRef
UserID int64
DisplayAuthor *bool
EmojiID *int64
Title *string
Prompt *string
}
type AIComposeTones struct {
Hash int64
Tones []AIComposeTone
}
func (t AIComposeTones) Clone() AIComposeTones {
out := t
out.Tones = make([]AIComposeTone, 0, len(t.Tones))
for _, tone := range t.Tones {
out.Tones = append(out.Tones, tone.Clone())
}
return out
}

View file

@ -469,6 +469,9 @@ type MessageWebPage struct {
Description string `json:"description,omitempty"`
Author string `json:"author,omitempty"`
Photo *Photo `json:"photo,omitempty"`
// ComposeToneEmojiID 映射 webPageAttributeAiComposeTone,用于 TDesktop 渲染
// addstyle tone 分享卡片;0 表示普通网页。
ComposeToneEmojiID int64 `json:"compose_tone_emoji_id,omitempty"`
ForceLargeMedia bool `json:"force_large_media,omitempty"`
ForceSmallMedia bool `json:"force_small_media,omitempty"`

View file

@ -23,6 +23,9 @@ const (
MessageEntityEmail MessageEntityType = "email"
MessageEntityPhone MessageEntityType = "phone"
MessageEntityBankCard MessageEntityType = "bank_card"
MessageEntityDiffInsert MessageEntityType = "diff_insert"
MessageEntityDiffReplace MessageEntityType = "diff_replace"
MessageEntityDiffDelete MessageEntityType = "diff_delete"
)
const (
@ -101,6 +104,8 @@ type MessageEntity struct {
DocumentID int64
// Collapsed 仅 blockquote 使用。
Collapsed bool
// OldText 仅 AI compose diff_replace 使用;普通消息输入不会接受该实体。
OldText string
}
// Message 是账号视角下的一条私聊消息。
@ -113,6 +118,7 @@ type Message struct {
From Peer
Date int
EditDate int
HideEdited bool
Out bool
Silent bool
NoForwards bool
@ -405,6 +411,7 @@ type EditMessageRequest struct {
Entities []MessageEntity
Media *MessageMedia
EditDate int
HideEdited bool
OriginAuthKeyID [8]byte
OriginSessionID int64
// SetReplyMarkup 置位时替换 reply_markup(ReplyMarkup 为 nil/空 = 清空键盘);

View file

@ -14,6 +14,11 @@ const (
StickersBotUserID int64 = 1063110917
// StickersBotAccessHash 固定不变;与 postgres 种子行双写,必须保持一致。
StickersBotAccessHash int64 = 5213187021149032991
// ChatBotUserID 是内置 @ChatBot 账号。它把私聊文本转给 server AI provider 链。
ChatBotUserID int64 = 1250000007
// ChatBotAccessHash 固定不变;与 postgres 种子行双写,必须保持一致。
ChatBotAccessHash int64 = 6332902371644871201
)
// OfficialSystemUser 返回第一阶段内置的官方系统账号。
@ -55,6 +60,19 @@ func StickersBotUser() User {
}
}
// ChatBotUser 返回内置 @ChatBot 账号。
func ChatBotUser() User {
return User{
ID: ChatBotUserID,
AccessHash: ChatBotAccessHash,
FirstName: "ChatBot",
Username: "ChatBot",
Verified: true,
Bot: true,
BotInfoVersion: 1,
}
}
// SystemUserByID 返回内置系统账号;非系统账号返回 ok=false。
// 所有对 777000 的硬编码注入点统一经此函数,新增内置账号只改这里。
func SystemUserByID(id int64) (User, bool) {
@ -65,6 +83,8 @@ func SystemUserByID(id int64) (User, bool) {
return BotFatherUser(), true
case StickersBotUserID:
return StickersBotUser(), true
case ChatBotUserID:
return ChatBotUser(), true
}
return User{}, false
}
@ -76,7 +96,7 @@ func IsSystemUserID(id int64) bool {
func SystemUserByPhone(phone string) (User, bool) {
phone = NormalizePhone(phone)
for _, id := range []int64{OfficialSystemUserID, BotFatherUserID, StickersBotUserID} {
for _, id := range []int64{OfficialSystemUserID, BotFatherUserID, StickersBotUserID, ChatBotUserID} {
u, ok := SystemUserByID(id)
if !ok || u.Phone == "" {
continue