fix: sync latest telesrv fixes

This commit is contained in:
A 2026-07-04 01:14:47 +08:00
parent b7269b135f
commit b1f74185f0
11 changed files with 203 additions and 72 deletions

View file

@ -2,16 +2,29 @@ package ai
import "telesrv/internal/domain"
const (
defaultToneEmojiFormal int64 = 4963195715414131468
defaultToneEmojiShort int64 = 5089558399201313570
defaultToneEmojiTribal int64 = 4906965037207257780
defaultToneEmojiCorp int64 = 5103015433682813448
defaultToneEmojiZen int64 = 5129871924314243582
defaultToneEmojiBiblical int64 = 5006296094481580688
defaultToneEmojiViking int64 = 5102866720440189629
)
func DefaultTones() []domain.AIComposeTone {
return []domain.AIComposeTone{
defaultTone("neutral", "Polish", "Make the draft clearer, smoother, and chat-ready while keeping the original intent. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("formal", "Formal", "Rewrite in a more professional, polished, and polite tone. Avoid casual wording and contractions. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("friendly", "Friendly", "Rewrite in a warmer, conversational tone with natural phrasing. Light contractions are acceptable. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("concise", "Concise", "Rewrite the draft to be shorter and easier to scan while keeping the key meaning. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("formal", "Formal", defaultToneEmojiFormal, "Rewrite in a more professional, polished, and polite tone. Avoid casual wording and contractions. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("short", "Short", defaultToneEmojiShort, "Rewrite the draft to be shorter and easier to scan while keeping the key meaning. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("tribal", "Tribal", defaultToneEmojiTribal, "Rewrite in a primal, chant-like style with short punchy phrasing and playful energy. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("corp", "Corp", defaultToneEmojiCorp, "Rewrite in a business-corporate style with clear, action-oriented wording. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("zen", "Zen", defaultToneEmojiZen, "Rewrite in a calm, mindful, minimal style with gentle phrasing. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("biblical", "Biblical", defaultToneEmojiBiblical, "Rewrite in a solemn, archaic, scripture-like style while preserving the original meaning. Avoid returning the exact original text when a safe wording improvement is possible."),
defaultTone("viking", "Viking", defaultToneEmojiViking, "Rewrite in a bold, saga-like style with confident phrasing. Avoid returning the exact original text when a safe wording improvement is possible."),
}
}
func defaultTone(slug, title, prompt string) domain.AIComposeTone {
func defaultTone(slug, title string, emojiID int64, prompt string) domain.AIComposeTone {
ex := domain.AIComposeToneExample{
From: domain.AIComposeText{Text: "Can you send me the file when you have time?"},
To: domain.AIComposeText{Text: "Could you send me the file when you have a moment?"},
@ -20,6 +33,7 @@ func defaultTone(slug, title, prompt string) domain.AIComposeTone {
Default: true,
Slug: slug,
Title: title,
EmojiID: emojiID,
Prompt: prompt,
ExampleEnglish: &ex,
}

View file

@ -33,7 +33,7 @@ func localTransform(text string, req domain.AIComposeRequest, tone domain.AIComp
return ensureSentencePunctuation(text)
case "friendly":
return ensureSentencePunctuation(text)
case "concise":
case "short", "concise":
return trimVerboseLead(text)
default:
return ensureSentencePunctuation(text)

View file

@ -91,6 +91,40 @@ func TestDefaultTonePromptsDiscourageEcho(t *testing.T) {
}
}
func TestDefaultTonesMatchOfficialDirectory(t *testing.T) {
want := []struct {
slug string
title string
emojiID int64
}{
{"formal", "Formal", defaultToneEmojiFormal},
{"short", "Short", defaultToneEmojiShort},
{"tribal", "Tribal", defaultToneEmojiTribal},
{"corp", "Corp", defaultToneEmojiCorp},
{"zen", "Zen", defaultToneEmojiZen},
{"biblical", "Biblical", defaultToneEmojiBiblical},
{"viking", "Viking", defaultToneEmojiViking},
}
got := DefaultTones()
if len(got) != len(want) {
t.Fatalf("DefaultTones len = %d, want %d", len(got), len(want))
}
for i, tone := range got {
if tone.Slug != want[i].slug || tone.Title != want[i].title {
t.Fatalf("DefaultTones[%d] = %s/%s, want %s/%s", i, tone.Slug, tone.Title, want[i].slug, want[i].title)
}
if !tone.Default {
t.Fatalf("DefaultTones[%d].Default = false, want true", i)
}
if tone.EmojiID != want[i].emojiID {
t.Fatalf("DefaultTones[%d].EmojiID = %d, want %d", i, tone.EmojiID, want[i].emojiID)
}
if tone.ExampleEnglish == nil {
t.Fatalf("DefaultTones[%d].ExampleEnglish = nil", i)
}
}
}
func TestComposeCallsProviderWithInstruction(t *testing.T) {
provider := &fakeProvider{text: "Please send the file when you have a moment."}
svc := NewService(memory.NewAIComposeStore(), WithProvider(provider))

View file

@ -84,27 +84,27 @@ func seedEffectsCatalog(parsed seedEffectsFileJSON) ([]domain.AvailableEffect, [
docByID[d.ID] = d
}
required := make(map[int64]struct{}, len(docByID))
storageID := func(sourceID int64) int64 {
if sourceID == 0 {
documentID := func(id int64) int64 {
if id == 0 {
return 0
}
if _, ok := docByID[sourceID]; !ok {
if _, ok := docByID[id]; !ok {
return 0
}
required[sourceID] = struct{}{}
return seedDocumentStorageID(sourceID)
required[id] = struct{}{}
return id
}
effects := make([]domain.AvailableEffect, 0, len(parsed.Result.Effects))
for i, ej := range parsed.Result.Effects {
if ej.ID == 0 || ej.EffectStickerID == 0 {
continue
}
staticID := storageID(ej.StaticIconID)
stickerID := storageID(ej.EffectStickerID)
staticID := documentID(ej.StaticIconID)
stickerID := documentID(ej.EffectStickerID)
if stickerID == 0 {
continue
}
animID := storageID(ej.EffectAnimationID)
animID := documentID(ej.EffectAnimationID)
effects = append(effects, domain.AvailableEffect{
ID: ej.ID,
Emoticon: ej.Emoticon,

View file

@ -322,15 +322,12 @@ func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey str
docIDs := make([]int64, 0, len(info.Result.Documents))
docs := make([]domain.Document, 0, len(info.Result.Documents))
docIDBySource := make(map[int64]int64, len(info.Result.Documents))
for _, dj := range info.Result.Documents {
sourceID := dj.ID
doc, err := s.importDocument(ctx, dj, stickersDir, index, stats)
if err != nil {
return err
}
if doc.ID != 0 {
docIDBySource[sourceID] = doc.ID
docIDs = append(docIDs, doc.ID)
docs = append(docs, doc)
}
@ -351,12 +348,12 @@ func (s *Service) importStickerSetDir(ctx context.Context, setDir, systemKey str
Masks: sj.Masks,
Archived: sj.Archived,
Installed: seedStickerSetInstalled(kind),
ThumbDocumentID: seedDocumentStorageID(derefInt64(sj.ThumbDocumentID)),
ThumbDocumentID: derefInt64(sj.ThumbDocumentID),
Thumbs: seedStickerSetPhotoSizes(sj.Thumbs),
ThumbDCID: s.dc,
ThumbVersion: sj.ThumbVersion,
DocumentIDs: docIDs,
Packs: seedStickerPacks(sj.Packs, info.Result.Packs, docIDBySource),
Packs: seedStickerPacks(sj.Packs, info.Result.Packs),
SortOrder: order,
SystemKey: systemKey,
}
@ -377,10 +374,9 @@ func (s *Service) importDocument(ctx context.Context, dj seedDocumentJSON, binDi
if dj.ID == 0 {
return domain.Document{}, nil
}
storageID := seedDocumentStorageID(dj.ID)
ref, _ := hex.DecodeString(dj.FileReference)
doc := domain.Document{
ID: storageID,
ID: dj.ID,
AccessHash: dj.AccessHash,
FileReference: ref,
Date: parseSeedDate(dj.Date),
@ -390,7 +386,7 @@ func (s *Service) importDocument(ctx context.Context, dj seedDocumentJSON, binDi
Attributes: seedDocumentAttributes(dj.Attributes),
}
// 主体 blobdoc:<server-owned-id>
// 主体 blobdoc:<document-id>
if mainPath, ok := index.main[dj.ID]; ok {
data, err := os.ReadFile(mainPath)
if err != nil {
@ -401,7 +397,7 @@ func (s *Service) importDocument(ctx context.Context, dj seedDocumentJSON, binDi
return domain.Document{}, err
}
if err := s.media.PutFileBlob(ctx, domain.FileBlob{
LocationKey: fmt.Sprintf("doc:%d", storageID),
LocationKey: fmt.Sprintf("doc:%d", doc.ID),
Backend: domain.MediaBackend(s.blobs.Name()),
ObjectKey: objectKey,
Size: int64(len(data)),
@ -441,7 +437,7 @@ func (s *Service) importDocument(ctx context.Context, dj seedDocumentJSON, binDi
}
ps.Size = len(data)
if err := s.media.PutFileBlob(ctx, domain.FileBlob{
LocationKey: fmt.Sprintf("doc:%d:%s", storageID, ps.Type),
LocationKey: fmt.Sprintf("doc:%d:%s", doc.ID, ps.Type),
Backend: domain.MediaBackend(s.blobs.Name()),
ObjectKey: objectKey,
Size: int64(len(data)),
@ -487,24 +483,9 @@ var seedThumbMarker = regexp.MustCompile(`_thumb\d+_`)
const seedInlineCachedDocumentThumbMaxBytes = 32 * 1024
const seedSyntheticDocumentThumbType = "m"
// Exported Telegram resources keep their original id in filenames/JSON, but
// telesrv owns the document catalog it serves. Imported high source ids are
// normalized once at seed time so RPC/storage use one server-owned id.
const seedExternalDocumentIDOffset int64 = 4_000_000_000_000_000_000
var seedThumbType = regexp.MustCompile(`PhotoSize_type([a-z])`)
var seedSyntheticTGStickerPreviewThumbPNG = makeSeedSyntheticTGStickerPreviewThumbPNG()
func seedDocumentStorageID(sourceID int64) int64 {
if sourceID <= 0 {
return 0
}
if sourceID > seedExternalDocumentIDOffset {
return sourceID - seedExternalDocumentIDOffset
}
return sourceID
}
func scanSeedDir(dir string) (seedDirIndex, error) {
idx := seedDirIndex{main: map[int64]string{}, thumb: map[int64]map[string]string{}}
entries, err := os.ReadDir(dir)
@ -826,7 +807,7 @@ func (s *Service) documentsNeedSeedRepair(ctx context.Context, ids []int64) (boo
return false, nil
}
func seedStickerPacks(setPacks, resultPacks []seedStickerPackJSON, docIDBySource map[int64]int64) []domain.StickerPack {
func seedStickerPacks(setPacks, resultPacks []seedStickerPackJSON) []domain.StickerPack {
packs := setPacks
if len(packs) == 0 {
packs = resultPacks
@ -834,12 +815,8 @@ func seedStickerPacks(setPacks, resultPacks []seedStickerPackJSON, docIDBySource
out := make([]domain.StickerPack, 0, len(packs))
for _, p := range packs {
documents := make([]int64, 0, len(p.Documents))
for _, sourceID := range p.Documents {
if id, ok := docIDBySource[sourceID]; ok {
documents = append(documents, id)
continue
}
if id := seedDocumentStorageID(sourceID); id != 0 {
for _, id := range p.Documents {
if id > 0 {
documents = append(documents, id)
}
}

View file

@ -79,13 +79,12 @@ func writeSeedDirFingerprint(h io.Writer, dir string) error {
}
func seedDocumentJSONLocationKeys(dj seedDocumentJSON, index seedDirIndex) []string {
storageID := seedDocumentStorageID(dj.ID)
if storageID == 0 {
if dj.ID == 0 {
return nil
}
keys := make([]string, 0, 1+len(dj.Thumbs))
if _, ok := index.main[dj.ID]; ok {
keys = append(keys, fmt.Sprintf("doc:%d", storageID))
keys = append(keys, fmt.Sprintf("doc:%d", dj.ID))
}
for _, tj := range dj.Thumbs {
ps, downloadable := seedPhotoSize(tj)
@ -93,11 +92,11 @@ func seedDocumentJSONLocationKeys(dj seedDocumentJSON, index seedDirIndex) []str
continue
}
if _, ok := index.thumb[dj.ID][ps.Type]; ok {
keys = append(keys, fmt.Sprintf("doc:%d:%s", storageID, ps.Type))
keys = append(keys, fmt.Sprintf("doc:%d:%s", dj.ID, ps.Type))
}
}
if seedDocumentJSONNeedsSyntheticTGStickerPreviewThumb(dj) {
keys = append(keys, fmt.Sprintf("doc:%d:%s", storageID, seedSyntheticDocumentThumbType))
keys = append(keys, fmt.Sprintf("doc:%d:%s", dj.ID, seedSyntheticDocumentThumbType))
}
return keys
}
@ -112,13 +111,12 @@ func (s *Service) seedDocumentJSONsReady(ctx context.Context, docs []seedDocumen
locationKeys := make([]string, 0, len(docs))
seenLocationKeys := make(map[string]struct{}, len(docs))
for _, dj := range docs {
storageID := seedDocumentStorageID(dj.ID)
if storageID == 0 {
if dj.ID == 0 {
continue
}
if _, ok := expected[storageID]; !ok {
expected[storageID] = dj
ids = append(ids, storageID)
if _, ok := expected[dj.ID]; !ok {
expected[dj.ID] = dj
ids = append(ids, dj.ID)
}
for _, key := range seedDocumentJSONLocationKeys(dj, index) {
if _, ok := seenLocationKeys[key]; ok {

View file

@ -642,9 +642,6 @@ func TestSeedMediaFromRealExport(t *testing.T) {
if d, ok, _ := media.GetDocument(context.Background(), first.SelectAnimationID); !ok {
t.Error("reaction select animation document missing")
} else {
if d.ID > seedExternalDocumentIDOffset {
t.Errorf("reaction document kept external source id: %d", d.ID)
}
if d.DCID != 2 {
t.Errorf("document dc_id not rewritten: %d", d.DCID)
}
@ -678,9 +675,6 @@ func TestSeedMediaFromRealExport(t *testing.T) {
if doc, ok, _ := media.GetDocument(context.Background(), sample.DocumentIDs[0]); !ok {
t.Fatalf("sample sticker document %d missing", sample.DocumentIDs[0])
} else {
if doc.ID > seedExternalDocumentIDOffset {
t.Fatalf("sample sticker kept external source id: %d", doc.ID)
}
thumb, ok := findCachedThumb(doc.Thumbs)
if !ok {
t.Fatalf("sample sticker document thumbs are not inline cached: %+v", doc.Thumbs)
@ -729,14 +723,37 @@ func writeEffectsSeed(t *testing.T, seedDir string, sourceID int64) {
}
}
func TestSeedDocumentStorageIDNormalizesExternalIDs(t *testing.T) {
func TestSeedDocumentIDsAreImportedVerbatim(t *testing.T) {
ctx := context.Background()
seedDir := t.TempDir()
const sourceID int64 = 5382305375846410902
const want int64 = 1382305375846410902
if got := seedDocumentStorageID(sourceID); got != want {
t.Fatalf("seedDocumentStorageID(%d) = %d, want %d", sourceID, got, want)
writeStatusPackWithoutThumbSeed(t, seedDir, sourceID, 17)
media := newFakeMediaStore()
blobs, err := NewLocalFS(t.TempDir())
if err != nil {
t.Fatalf("local fs: %v", err)
}
if got := seedDocumentStorageID(2222222); got != 2222222 {
t.Fatalf("small server id changed: %d", got)
svc := NewService(media, blobs, 2)
if _, err := svc.SeedMedia(ctx, seedDir, 0); err != nil {
t.Fatalf("seed media: %v", err)
}
set, ok, err := media.GetStickerSetByShortName(ctx, "StatusPack")
if err != nil || !ok {
t.Fatalf("StatusPack ok=%v err=%v", ok, err)
}
if len(set.DocumentIDs) != 1 || set.DocumentIDs[0] != sourceID {
t.Fatalf("set document ids = %+v, want [%d]", set.DocumentIDs, sourceID)
}
doc, ok, err := media.GetDocument(ctx, sourceID)
if err != nil || !ok {
t.Fatalf("GetDocument(%d) ok=%v err=%v", sourceID, ok, err)
}
if doc.ID != sourceID {
t.Fatalf("document id = %d, want %d", doc.ID, sourceID)
}
if _, ok, err := media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", sourceID)); err != nil || !ok {
t.Fatalf("main blob for raw id ok=%v err=%v", ok, err)
}
}

View file

@ -455,12 +455,17 @@ func (s *Server) enqueueRPC(ctx context.Context, c *Conn, msgID int64, body []by
// body 已是 enqueueRPC 入参的独立副本dispatch 里 b.Copy()),且每个任务只 run 一次,
// 无需再 append 拷贝;直接复用,省掉一份 inbound 在途内存。
if err := s.handleRPC(taskCtx, c, msgID, &bin.Buffer{Buf: body}); err != nil {
s.log.Info("RPC async handler failed",
fields := []zap.Field{
zap.Int64("msg_id", msgID),
zap.String("auth_key_id", hex.EncodeToString(c.authKeyID[:])),
zap.Int64("session_id", c.sessionID),
zap.Error(err),
)
}
if isClientDisconnect(err) {
s.log.Debug("RPC async handler canceled", fields...)
} else {
s.log.Info("RPC async handler failed", fields...)
}
return err
}
return nil
@ -518,6 +523,13 @@ func (s *Server) handleRPC(ctx context.Context, c *Conn, msgID int64, b *bin.Buf
}
fields = dbtrace.AppendZapFields(fields, "", dbStats.Snapshot())
if ctxErr := ctx.Err(); ctxErr != nil && err != nil {
// A canceled request context means the result cannot be delivered. Do not
// turn cancellation-derived handler errors into cacheable rpc_error replies.
s.log.Info("RPC canceled", append(fields, zap.NamedError("dispatch_error", err), zap.NamedError("context_error", ctxErr))...)
return ctxErr
}
if err != nil {
var rpcErr *tgerr.Error
if errors.As(err, &rpcErr) {

View file

@ -14,6 +14,7 @@ import (
"github.com/gotd/td/mt"
"github.com/gotd/td/proto"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"github.com/gotd/td/transport"
"telesrv/internal/rpc"
@ -141,6 +142,41 @@ func TestDuplicateRPCResultAcrossReconnectUsesSessionCache(t *testing.T) {
}
}
func TestCanceledRPCErrorIsNotCachedAcrossReconnect(t *testing.T) {
const dc = 2
handler := &canceledInternalRPC{
firstDone: make(chan struct{}),
}
addr, pub, _ := startTestServer(t, Options{DC: dc, RPC: handler})
conn, auth, cipher := dialHandshake(t, addr, dc, pub)
clientMsgID := proto.NewMessageIDGen(time.Now)
reqMsgID := clientMsgID.New(proto.MessageFromClient)
sendEncrypted(t, conn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})
_ = conn.Close()
select {
case <-handler.firstDone:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for canceled first rpc")
}
replayConn := dialTransportOnly(t, addr)
sendEncrypted(t, replayConn, cipher, auth, reqMsgID, &tg.HelpGetConfigRequest{})
result := readRPCResultForRequest(t, replayConn, cipher, auth.AuthKey, reqMsgID)
var cfg tg.Config
if err := cfg.Decode(&bin.Buffer{Buf: result.Result}); err != nil {
t.Fatalf("decode replay config: %v", err)
}
if cfg.ThisDC != dc {
t.Fatalf("replay config.ThisDC = %d, want %d", cfg.ThisDC, dc)
}
if calls := handler.calls.Load(); calls != 2 {
t.Fatalf("handler calls = %d, want 2 (canceled first result must not be cached)", calls)
}
}
type countingConfigRPC struct {
calls atomic.Int32
}
@ -172,6 +208,22 @@ func (h *blockingRPC) Dispatch(ctx context.Context, _ [8]byte, _ int64, _ *bin.B
func (h *blockingRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }
type canceledInternalRPC struct {
calls atomic.Int32
firstDone chan struct{}
}
func (h *canceledInternalRPC) Dispatch(ctx context.Context, _ [8]byte, _ int64, _ *bin.Buffer) (bin.Encoder, error) {
if h.calls.Add(1) == 1 {
<-ctx.Done()
close(h.firstDone)
return nil, tgerr.New(500, "INTERNAL_SERVER_ERROR")
}
return &tg.Config{ThisDC: 2}, nil
}
func (h *canceledInternalRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }
func readRPCResultForRequest(t *testing.T, conn transport.Conn, cipher crypto.Cipher, key crypto.AuthKey, reqMsgID int64) proto.Result {
t.Helper()
for i := 0; i < 12; i++ {

View file

@ -36,6 +36,33 @@ func TestAIComposeGetTonesReturnsDefaultsAndNotModified(t *testing.T) {
if tones.Hash == 0 || len(tones.Tones) == 0 {
t.Fatalf("getTones hash/tones = %d/%d, want non-empty", tones.Hash, len(tones.Tones))
}
want := []struct {
slug string
emojiID int64
}{
{"formal", 4963195715414131468},
{"short", 5089558399201313570},
{"tribal", 4906965037207257780},
{"corp", 5103015433682813448},
{"zen", 5129871924314243582},
{"biblical", 5006296094481580688},
{"viking", 5102866720440189629},
}
if len(tones.Tones) < len(want) {
t.Fatalf("getTones defaults = %d, want at least %d", len(tones.Tones), len(want))
}
for i, expected := range want {
tone, ok := tones.Tones[i].(*tg.AiComposeToneDefault)
if !ok {
t.Fatalf("getTones tones[%d] = %T, want *tg.AiComposeToneDefault", i, tones.Tones[i])
}
if tone.Tone != expected.slug {
t.Fatalf("getTones tones[%d].Tone = %q, want %q", i, tone.Tone, expected.slug)
}
if tone.EmojiID != expected.emojiID {
t.Fatalf("getTones tones[%d].EmojiID = %d, want %d", i, tone.EmojiID, expected.emojiID)
}
}
again, err := r.onAicomposeGetTones(ctx, tones.Hash)
if err != nil {
t.Fatalf("getTones(hash): %v", err)

View file

@ -50,13 +50,13 @@ func TestUploadGetFileRejectsInvalidRanges(t *testing.T) {
func TestFileLocationKeyUsesDocumentID(t *testing.T) {
key, ok := fileLocationKey(&tg.InputDocumentFileLocation{
ID: 1382305375846410902,
ID: 5382305375846410902,
ThumbSize: "m",
})
if !ok {
t.Fatal("fileLocationKey returned !ok")
}
const want = "doc:1382305375846410902:m"
const want = "doc:5382305375846410902:m"
if key != want {
t.Fatalf("key = %q, want %q", key, want)
}