owpengram-server/internal/compat/layerwire
2026-07-05 20:42:13 +08:00
..
_schema chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
gen chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
schema fix: harden channel rights and sticker compatibility 2026-07-01 22:38:13 +08:00
bench_test.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
client_aliases.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
corpus_test.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
fallback.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
gate_test.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
inbound.go feat: sync recent call and channel fixes 2026-07-01 14:34:59 +08:00
inbound_test.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
inbound_transform_test.go fix: harden channel rights and sticker compatibility 2026-07-01 22:38:13 +08:00
layout.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
README.md chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
tables.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
tables_gen.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
transcode_test.go fix: preserve formatted date message entities 2026-07-05 20:42:13 +08:00
walk.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00
walk_test.go chore: refresh gramsrv public release 2026-06-30 14:37:43 +08:00

layerwire 操作手册(多 Layer 向后兼容)

本文是 怎么操作runbook为什么这么设计docs/layer-compat-220-227-design.md。 改这个包前请先读完本文 + 设计文档。所有命令都从 telesrv 模块根目录执行。

这个包是干什么的

让 telesrv 同时正确服务 Layer 220227 的客户端,而业务 handler / gotd 永远只跑 canonical(227)、一行不改。

  • 出站:把 227 对象降级成老客户端能解的 wire 形态(Transcode)。
  • 入站:把老客户端发来的旧构造器升级成 227 请求,再交正常 gotd dispatcherUpgradeInbound)。

两个正交维度(务必分清

  • 官方层漂移:构造器在 layer N 的字段/CRC 与 227 不同。真值=官方 TDesktop api.tl 各层。自动从 schema 生成
  • 客户端构造器漂移:某客户端(如 DrKLO Android手维护的 TL 实际发了个旧 layer 的官方构造器,但它声明的整体 layer 却是新的。真值=该客户端源码TLRPC.java声明在 client-drift.tl / client_aliases.go

文件地图

文件 角色 谁改
schema/canonical-227.tl embed,运行期 walker 的 227 字段布局(= gotd td/_schema/tdesktop.tl 的副本) gotd 升级时 re-sync
_schema/layer-2NN.tl 历史层官方 schema从 TDesktop git 抽,仅生成期用,下划线=不编译/不 embed 升级/下探 floor 时抽取
schema/client-drift.tl 声明式客户端发的旧构造器老布局body 与 227 不同的) 发现客户端漂移时 +1 行
client_aliases.go 客户端漂移里 body 与 227 字节一致的,纯 老CRC→227CRC 发现纯换 CRC 漂移时 +1 条
tables_gen.go 生成产物(勿手改):官方层降级表 + 入站升级表 + 新类型集 gen 重生成
gen/main.go 生成器:对拍 schema、证明机械性、产 tables_gen.go 升级逻辑变更时
layout.go walk.go tables.go 通用解释器(读/丈量/递归转码) 核心,少动
fallback.go 出站手写兜底(结构性 / 227-only 类型) CoverageGate 报缺时
inbound.go 入站通用升级引擎 + fieldConverters + driftFieldRenames DriftCoverage 报缺时

核心命令

# 复核 schema 差异数字(不改文件)
go run ./internal/compat/layerwire/gen -report

# 重新生成 tables_gen.go官方层漂移表
go run ./internal/compat/layerwire/gen -emit internal/compat/layerwire/tables_gen.go

# 全部护栏(漂移门禁 + 对各历史层真实 schema 对拍 + 性能基准)
go test ./internal/compat/layerwire/
go test ./internal/compat/layerwire/ -run '^$' -bench . -benchmem   # 性能

# 改完务必:
gofmt -w internal/compat/layerwire/ && go build ./... && go vet ./internal/...

操作 1gotd 升级canonical layer 上移,例 227 → 230

gotd bump 是显式任务(见 AGENTS.md 铁律 #6。canonical schema 随之变化,按下列步骤同步。

  1. 同步 canonical schemagotd 的就是实际编出的字节):
    cp ../td/_schema/tdesktop.tl internal/compat/layerwire/schema/canonical-230.tl
    rm internal/compat/layerwire/schema/canonical-227.tl
    
    layout.go//go:embed schema/canonical-230.tlconst CanonicalLayer = 230
  2. 把原 canonical 层并入历史 TO 层:现在 227/228/229 成了"老层",从 TDesktop git 抽进 _schema/(见文末「抽取 api.tl@N」
  3. 改生成期常量gen/main.gocanonicalLayer = 230。(supportedFloor 不变。)
  4. 重生成 + 复核
    go run ./internal/compat/layerwire/gen -report   # 看 changed/new 数字是否合理
    go run ./internal/compat/layerwire/gen -emit internal/compat/layerwire/tables_gen.go
    
  5. 跑护栏、按报告 triage
    go test ./internal/compat/layerwire/
    
    • TestCoverageGate 失败 = 出现了 telesrv 可达但没处理的 227(新 canonical)-only / 结构性类型 → 去 fallback.go 加 by-type 兜底或结构性转换,或确认 telesrv 不发就加进 unemittedAllowlistgate_test.go,附理由)。
    • 生成器 -report 里 "structural" 列出的需手写转换(参照 fallback.go transcodePollAnswerVoters)。
  6. gofmt/build/vet/全量 go test。真机 220/老层/新层各一台回归。

操作 2下探 floor支持更老客户端例 220 → 215

  1. 从 TDesktop git 抽 layer-215.tl … layer-219.tl_schema/(见文末)。
  2. supportedFloorlayout.goSupportedFloor = 215 gen/main.gosupportedFloor = 215(两处都要)。
  3. go run ... -emit ... 重生成 → go test
  4. 越老的层结构性差异越多,按 TestCoverageGate / 生成器 report triage同操作 1 第 5 步)。

操作 3新增「客户端构造器漂移」最常见

触发:某客户端发的旧构造器导致 NOT_IMPLEMENTED(入站)或对端渲染异常;或主动审计客户端源码发现它发旧 CRC。

  1. 拿到老构造器的精确 TL 定义
    • 优先看该客户端源码的序列化DrKLO AndroidTMessagesProj/.../TLRPC.javaserializeToStream,按 writeInt32/writeString/... 顺序还原字段)。
    • 或它是某旧 layer 官方构造器:git -C ../tdesktop/tdesktop log -S"#<crc>" -- <api.tl> 找到所在层,再取该层定义。
  2. 判断 body 是否与 227 字节一致
    • 一致(只是 CRC 不同;典型=227 只追加了 flag-gated 可选字段而客户端不设)→ 往 client_aliases.go clientMethodAliases0x<老CRC>: 0x<227CRC>
    • 不一致(缺 flags 整数 / 字段类型变了 / 缺必填字段)→ 往 schema/client-drift.tl一行老布局 TL(用 method 的限定名,结果类型随便填合法值,引擎只按名字匹配 227
  3. 跑测试
    go test ./internal/compat/layerwire/ -run TestInbound
    
    • 绿 = 通用引擎已能自动升级(复制共享字段 + 插 flags=0 + 按 kind 补默认)。完事
    • TestInboundDriftCoverageneeds converter A->B = 有字段类型变更 → 往 inbound.go fieldConverters 加一条 "A->B"(可复用,参照 Vector<int>->Vector<InputMessage>)。
    • field X not defaultable 或字段改名 → 往 inbound.go driftFieldRenames"<method>\x00<227字段>": "<老字段>"(参照 bots.exportBotToken\x00bot)。
  4. 绝不为此写一个新的 handleLegacyXxx 解码 handler——那是旧做法已全删。统一走数据 + 通用引擎。

操作 4出站 TestCoverageGate 失败

说明 telesrv 现在会发某个"经保留字段可达"的 227-only / 结构性类型,但没处理。

  • 该类有同抽象类的老成员可降级 → fallback.gonewTypeFallbacksByType["<抽象类>"](如 PageBlock→pageBlockUnsupported)。
  • 结构性变更类型且 telesrv 真发 → fallback.go structuralTransforms 加手写转换。
  • 确认 telesrv 不发 → 加进 gate_test.go unemittedAllowlist必须附理由,引用出站构造器审计)。

护栏:每个测试拦什么

测试 拦截
TestWalkConsumesCanonicalObjects 解释器读不全某个 227 类型(字段布局漏)
TestTranscodeDowngradeValid 降级输出对 220..226 真实 schema 解析失败/有残留字节
TestTranscodeChangedTypeNestedInUnchangedContainer 「外层 CRC 不变但内含变更类型」被误整段拷贝
TestCoverageGate 出站 227-only/结构性类型无 handler 又不在 allowlistgotd bump/客户端升级引入新形态时报)
TestInboundDriftCoverage client-drift.tl 某条目无法自动升级(缺 converter/rename
TestInboundBodyTransforms / TestInboundCRCSwaps 入站升级产出不是合法 227 请求
TestNegotiatedLayerStickyContract layer 协商的 (layer, ok) 契约(避免缓存驱逐把老客户端误降回 227

运行期 fail-safe:出站遇未处理类型 → Transcode 返错 → 边界记日志并发 canonical 字节(连接存活,单对象可能渲染异常)。入站遇未覆盖旧 CRC → 落 gotd dispatcher → NOT_IMPLEMENTED(须按 AGENTS.md #5 进 compatibility trace + 矩阵)。护栏的意义就是把这些从"线上撞见"提前到"提交期/测试期发现"。

抽取 api.tl@N操作 1/2 用)

TD=../tdesktop/tdesktop
APITL=Telegram/SourceFiles/mtproto/scheme/api.tl

# 找 layer N 的提交(取最后一个写入 "// LAYER N" 的;可能有初版+修订,选最全的)
git -C "$TD" log --oneline -S"// LAYER N" -- "$APITL"

# 抽取(务必校验文件末尾确是 "// LAYER N"
git -C "$TD" show <commit>:"$APITL" > internal/compat/layerwire/_schema/layer-N.tl
tail -1 internal/compat/layerwire/_schema/layer-N.tl   # 应为: // LAYER N

各层→commit 对照见设计文档 §3 表220..227 的 canonical 抽取点)。gotd/tl 解析器能直接吃 TDesktop api.tl无需改格式。

稳态心法

喂新 schemagotd 或更老层)→ 跑 gen + go test → 护栏吐出短清单 → 人只处理新出现的 fallback / 结构性 / converter / rename。 不再有"运行时撞 NOT_IMPLEMENTED 再手写 handler"。