| 档案 | |
|---|---|
module |
avatar |
summary |
形象:仓库里叫「avatar」的东西其实是四样,互不相通 —— ① 换装人偶(HOOP Avatar,你捏的):现役 V2 屏(app/lib/avatar)是 manifest 驱动的 1662 画布分层渲染 + 肤色着色器 + 六档脸型,素材 55 张 webp 打进包;它捏完就丢,一行持久化都没有,而后端 hoop_avatars 表和 GET/PUT /v1/me/hoop-avatar 只服务于已经没有入口的 V1 屏,且 V1 带色号的件名后端正则拒收(不带色号的合法配方仍能存);② 预设头像库(你挑的):195 张 webp 筛一张,上传成 games 分身脸,不是主头像;③ 真人头像(别人看到的那张):对象键版本化、地址上的 ?v= 由一处登记全 App 重画、形状即身份(圆 / 圆角方 / 三角 / 圆角方挖空三角,有真图的 AI 与工作房改画圆角方块)、社群头像同步到公告与 General 两间房;④ 面具(舞会里那张不是你的脸):矢量画的、配方每人一份存 masque_identities.mask_config、稀有件服务端按默契硬控,只在假面舞会一对一配对里出现,没有任何房型用它。删号不抹换装配方、面具配方、舞会记录和图片(全靠永不触发的级联)。 |
updated |
2026-09-09 |
verified_ref |
c854ae835fbd2a843bafb14ab1868c75fde9698d |
source_ref |
[backend/internal/domain/avatar/avatar.go, backend/internal/domain/masque/{handler,model,repository,service}.go, backend/internal/domain/user/{handler,service}.go(只引头像那半), backend/internal/domain/community/community.go:129-231(只引同步), backend/internal/domain/auth/repository.go:401-483(只引删号), backend/migrations/000013 000018 000164 000286, app/lib/avatar/*.dart, app/assets/avatar/manifest.json, app/lib/screens/{hoop_avatar_studio,avatar_picker,avatar_viewer,virtual_avatar,mask_builder,masque,masque_chat,account}_screen.dart, app/lib/widgets/{hoop_avatar,masque_mask,me_nav_avatar,member_status_avatar}.dart, tools/gen_assets_catalog.py] |
status |
current |
review_after |
2026-12-09 |
owner |
nova |
supersedes |
[HOOP_AVATAR.md(07-03 写、只在 08-30 补过版本存档一节;14 条结构断言 7 条已推翻,其中「app/assets/avatar/ 是画师原始素材、只读不删不改」那条会引导人删掉现役素材;只搬走 Mike 08-30 两段原话、tag avatar-v1 取回法、「为什么用 tag 不另存文件夹」)] |
partially_supersedes |
[MASK_ASSETS.md(06-12 NOVA 写的照片级 PNG 面具采购单,§0 硬约束 / §1 一期 8 张预设 / §2 素材清单 / §3 验收全是从没走过的路,作废;留 §2 配方键名 + 解锁表(本文 §3.5 修正两处值语义)和 §4 编辑器配色), DESIGN_SYSTEM.md(§8 Avatars 的七处 file:line 全部漂移约 100 行、「415 行」已是 515,本文只替代行号那一层并补「有真图改画圆角方块」;形状语义与 12 色板仍以它为准,要查现状按它 §11 重跑), GHOST_ROOMS.md(只替代 :89「头像=绿幽灵👻」半句:👻 已按李敏 09-02 删,现在是空的绿圆角方块), WEREWOLF_ROOMS.md(只替代 :125 / :129 圆桌牌桌 + 点头像投票两句:那张屏不存在,07-16「狼人杀=聊天房」后方向已变)] |
这份怎么读:每一节先讲「它解决什么、用户看到什么」,再讲「为什么这样做、数据在哪、哪一步会断、断了留下什么、有什么保障和缺口」;代码位置、参数、行号收在每段末尾的「出处」里。 出处标记:
[code@c854ae83 文件:行]= 那棵树上的实现;[prod@日期,谁核]= 生产机当天核过;[decision@日期,谁]= 拍板过的决定。代码证明「那一版怎么做的」,证明不了「线上是这样」和「应该是这样」。 边界:真人头像的上传 / 裁剪 / 分身脸(persona)机制归 contacts.md 与 rooms(本文只写「头像怎么显示、怎么失效、什么形状」);假面舞会的配对 / 默契 / 聊天归本文 §3.5(它没有别的家);游戏影子 id 反查头像归 games.md;进包美术件的总目录assets.html由tools/gen_assets_catalog.py现扫(本文 §3.7 只记它的盲区);素材为什么在 git 不在 R2,唯一出处是ASSETS_R2.md开头 Jeff 07-18 那句,本文只引不抄。
0. 给接手的人:三分钟读懂形象
先认四样东西(它们在代码上互不相通;hoop_avatar 这个名字在仓里指着三个不同的地方):
- 换装人偶(HOOP Avatar)= 你捏的。个人中心「HOOP Avatar」进的是 V2 屏 app/lib/avatar/:manifest.json 里冻结的 1662 画布坐标,把 55 张 webp 按十层顺序贴上去,肤色走 GPU 着色器、脸型走 isolate 变形。V1 屏(hoop_avatar_studio_screen.dart,208 张 PNG 叠 Stack)08-30 起没有入口,故意留着当回退,tag avatar-v1。
- 预设头像库 = 你挑的。个人中心「虚拟人像」和引导卡进的 AvatarPickerScreen:195 张预生成 webp,按性别 / 年纪 / 肤色 / 眼镜筛一张,上传成 games 分身脸(uploadPersonaAvatar('games')),不是主头像。它是 fluttermoji 捏脸的替代品(Jeff v0.241 拍板),旧捏脸屏 virtual_avatar_screen.dart 是死代码。
- 真人头像 = 别人看到的那张。一个人 / 群 / 频道 / 社群 / AI / 工作房在全 App 各处显示的图,地址 /v1/users/{uid}/avatar?v=…(群 / 社群走各自的 /avatar),组件是 app/lib/widgets/hoop_avatar.dart(和换装人偶只是撞名)。
- 面具 = 你在舞会里那张不是你的脸。假面舞会(暗门 /mask)里的一对一匿名配对,矢量画的,配方每人一份存 masque_identities.mask_config。没有任何房型用它;鬼屋 / 狼人 / 影子房和面具零关系(影子房用的是真脸经影子 id 反查)。
它保证到什么程度:
1. 换装人偶今天捏完就丢。V2 屏 onDone 回调在唯一入口没传、文件里没有一个 ApiClient、没有本地写;退出即消失。后端 hoop_avatars 表和 GET/PUT /v1/me/hoop-avatar 还活着,但只有 V1 屏调它,而 V1 屏只有测试进得去。就算把 V1 接回去,染过色的也存不进:V1 只在选了色号时才把它编进件名 W-Hair_2#gold,后端白名单正则 ^[WM]-(Hair|…)_\d{1,3}$ 不认 #,400 后 App 只吐一句 toast;不带色号的合法配方(默认 _resetSel 清空色号)仍能存。V2 的配方(characterId / hair_01 / faceShapeId / toneId)和后端 schema 根本对不上,接回去要重定 schema。Jeff 07-16「Avatar 做好了就是我们的身份 → 存后端」与 08-30 之后的实现不一致;没找到改变这个决定的记录(CHANGELOG / 文档 / 测试都没有),是有意取舍还是交接漏了,待问。
2. 换装人偶从没出现在别人屏幕上。后端没有按 uid 读的公开端点(avatar.go 注释明写「身份铺开时再加」),message / conversation / community / calls / games / presence 无一处读 hoop_avatars。所有场景显示的都是真人头像 → 没有就首字母。
3. 真人头像换一张,全 App 立刻换脸:对象键每次上传新路径(avatars/{uid}/{uuid},CDN 天然失效),地址上的 ?v= 由 ApiClient._decode 这一处登记进 HoopAvatar._latestVer,「见过且版本不同」才 cacheEpoch++ 重画;第一次见到不算变。本机换完立刻 bust() 三连(磁盘 / 内存 / epoch)。别人那边没有 WS 推送,靠下一次任何接口回包捎来新版本号。
4. 形状即身份:圆 = 人,圆角方 = 群 / 频道 / 社群,三角 = AI,圆角方块挖空一个三角、洞里放首字母 = 工作房(Jeff 08-09 发图改的;08-02 的「外圈粗三角」是上一版);有真图的 AI 与工作房一律改画圆角方块(李敏 08-10 给了 20 个 AI 图标之后,三角只是「没图时的默认脸」)。首字母兜底按 runes 取(emoji 名不炸)、名字哈希取 12 色板。
5. 社群头像同步到两间房:传社群头像时同一个对象键写进公告频道和 General 群的 conversations.avatar_url,两边同一条判据「空或等于社群那个固定键 community-avatars/{cid} 才覆盖」(人自己给房换过别的图就不动);两间房的同步失败只记日志,上传照报成功;000286 是给老社群做的一次性回填(Jeff 08-23「我换了群组头像为什么这边还是 H」),它注释里「公告无条件补」是旧规则,代码现在两边一样。
6. 面具能用、有服务端硬控、没人拍板:配方 shape 0-2 / color 0-11 / border 0-3 / pattern 0-3 / decos ≤3,金边要默契 36 分、金丝纹 18 分、宝石要揭过面、孔雀翎要活跃 7 天,服务端逐项拒(ErrLocked);只有当前 active 的对家看得到你的面具和代号;揭面要 Lv4(66 分)且双方同意,揭了不可逆。自 06-12 上线后没有 Jeff / 李敏 / Leong / Mike 任何一条决定,图纸 MASK_ASSETS.md 写的是一条从没走过的 PNG 路线。
7. 删号不抹形象:删号是「抹字段不删行」(到期清理那一轮跑成功、hook 放行才抹;调度期限不是完成保证),hoop_avatars / masque_* 全靠 ON DELETE CASCADE,永远不触发;purge hook 只挂了 eKYC。换装配方、面具配方、代号、全部匿名聊天和 R2 里的舞会图片原封不动,而且这个人还会继续被舞会匹配到(配对查询不看 deleted_at,也不看拉黑)。
当前已核实没做的(§4 全表,先说五条):换装人偶持久化(V2 零、V1 存不进);把换装结果生成头像图当 avatarUrl;换装与圈子 / 灯 / 城堡打通(素材在包里,加一款发型 = 出一次 TestFlight,不可能有服务端下发的新装扮);暗影空间里的人长什么样(SHADOW.md 一个字没提);面具随默契动态变化 / 稀有度动画(TASKS 挂着没人认领)。
出了问题先查哪:§5 —— 「捏的人偶不见了」是现状不是故障;「换头像别人没变」先看回包有没有带 ?v=、再看是不是第一次见;「AI 头像怎么是方的」有真图就该是方的;「面具金边点不动」先看默契分;「面具没存上」先看有没有先 enter 过;「素材目录里找不到 avatar」生成器白名单少了一组。
1. 两段流程的一生
1.1 换一张真人头像:从相册到全 App 换脸
(换装人偶那条走不到「别人看到」,所以这里写真正会传播的那条;换装人偶的断点单独在 §3.2。)
你看到的:资料页点头像 → 我的头像全屏页 → 笔 → 「相册」或「Library」→ 选完立刻本机换脸 → 别人那边过一会儿也换了(不用重装、不用重登)。个人中心顶上那颗大头像点一下直接开相册,不弹单子(李敏 08-21:「点头像 = 直接换头像」)。
背后发生的:
1. 选图:相册走 POST /v1/users/me/avatar;「Library」进预设头像库,选一张 webp 走 POST /v1/me/personas/games/avatar —— 库里挑的是 games 分身脸,不是主头像。
2. 上传:服务端存到 avatars/{uid}/{uuid}{ext}(分身脸 avatars/{uid}/{scope}/{uuid}{ext}),版本化键:每次上传新路径,旧对象尽力删。user_profiles.avatar_url / personas.avatar_key 更新。
3. 本机立刻换:HoopAvatar.bust(path):按 cacheKey 清磁盘缓存(av-$path,不带 token 的稳定键)、清 PaintingBinding 内存缓存、cacheEpoch++ → 所有挂在屏上的头像组件重建,地址追加 &v=$epoch,ValueKey 变 = 换全新组件强制重载。
4. 别人怎么换:没有推送。他下一次调任何接口,回包里出现 /v1/users/{uuid}/avatar?v={新版本},ApiClient._decode 把它登记进 _latestVer(识别边界:正则只认 /avatar?v= 紧邻的形式,?scope=games&v= 这种不匹配;表只按 uid 索引不按 scope,所以分身脸的版本不是每张分开记);第一次见到这个人不算变(否则冷启动首屏白白重建几十次),见过且不同才 cacheEpoch++,全 App 立刻换脸。_withLatestVersion 用 Uri.replace 改 v,不手拼 —— 那条地址上可能还挂着 ?scope=network,拼坏了看到的是 403 不是错头像。
5. 取图:GET /v1/users/{id}/avatar 是裸路由,token 走 query 或 header;CDN 签名器开着就 302 到签名地址,否则后端代发字节。群 / 社群头像是原地覆盖同一个键(group-avatars/{convID}、community-avatars/{cid}),所以 App 侧要按地址 bust。
6. 显示:pathFor:有 avatarUrl 用它,userId 只是兜底;两条最后都过 _withLatestVersion —— 登记表里有这个 uid 就补上 v,没登记过才保持裸地址(那时才会命中旧缓存;:53-58 的注释说的是旧控制流)。房内成员用后端解析好的地址,分身脸优先(CASE WHEN persona.avatar_key … THEN …?scope= ELSE /avatar),规则在后端 SQL 不在 App。
哪一步会断、留下什么:第 1 步 App 侧确认请求没发出(asset 读不到、还没连上)= 什么都没写,一句 avatar_save_failed 留在原页;传到一半断线不算这一支;第 2 步服务端是 PutObject 之后才写库:写库明确失败 = 新对象已在存储里成孤儿、指针没动(限已确认未提交)、接口 500;执行结果不明或回包失败 = 可能已经成功;旧对象删失败是另一支,只多占存储;社群那条更长:先原地覆盖对象 → 更新 communities(失败时对象已经变了)→ 两间房各自 UPDATE(失败只记日志,上传仍报成功),回包带 followed 房 id 让本机清那两间的缓存,所以「同步」可能部分成功;第 3 步只管本机;第 4 步别人永远没调过带你头像的接口 = 他那边不换,直到下次见到;第 5 步没头像的人点「分享」= me_avatar_none_yet,不丢空文件出去;第 6 步只传 userId 不传 avatarUrl 的调用点 = 登记表里还没有这个 uid 时那处头像陈旧,登记过就跟着换(现状,不是 bug)。
出处:入口
[code@c854ae83 app/lib/screens/account_screen.dart:515-518](虚拟人像 → 头像库)、[code@c854ae83 app/lib/screens/avatar_viewer_screen.dart:99-116](相册)、:120(Library);头像库上传成 games 脸[code@c854ae83 app/lib/screens/avatar_picker_screen.dart:82-98];端点[code@c854ae83 backend/internal/domain/user/handler.go:34-42];对象键[code@c854ae83 backend/internal/domain/user/service.go:137]、:185,旧对象尽力删:143-145;取图 302 / 代发[code@c854ae83 backend/internal/domain/user/handler.go:217-270];pathFor[code@c854ae83 app/lib/widgets/hoop_avatar.dart:53-70],版本登记:85-108,bust:140-151,_withLatestVersion:113-126,epoch 重建:212-223;房内成员地址[code@c854ae83 backend/internal/domain/conversation/repository.go:528-529];群 / 社群键原地覆盖[code@c854ae83 backend/internal/domain/community/community.go:161-168];守卫app/test/avatar_version_one_source_0902_test.dart:38-94(第一次不算变 / 只动变的那个人 / 群头像不受影响 / 别的 query 参数不被冲掉 / 登记点在_decode)、avatar_url_keeps_version_test.dart、avatar_no_flash_test.dart、group_avatar_refresh_test.dart[decision@2026-09-02,Mike Ng:「有些地方 reflect 了有些地方还没」→ 头像版本号收成一份][decision@2026-08-21,李敏:「The profile image edit direct link to choose from album or Hoop's avatar library」][decision@2026-07(v0.241),Jeff:虚拟人像不再捏脸,改从预生成头像库挑]。
1.2 一张面具的一生:进舞会 → 捏 → 配对 → 聊 → 揭面 → 离开
你看到的:聊天输入框或搜索框打 /mask → 假面舞会:一个三字代号(如 K83)+ 一张面具(没捏过是 🎭)→ 「捏面具」选形状 / 主色 / 边框 / 花纹 / 装饰(灰的是没解锁)→ 「配对」随机配到圈子里一个人,只看到对方的代号和面具 → 聊天,默契分涨到 Lv2 能发图、Lv4 才有「摘面具」→ 双方都点才互相看到真名和真头像 → 「离开」结束这段。
背后发生的(全程没有一个事务,每步各自提交;没有推送通知,只有 WS):
1. 进舞会 POST /v1/masque/enter:EnsureIdentity = SELECT → 没行就随机 code(24 字母 × 100,只 2400 种、无唯一约束)+ 随机 emoji 面具 INSERT ON CONFLICT DO NOTHING → 再 SELECT(这次错误被吞,失败时回的是本地生成的那对,和库里可能不一样);再拉配对列表和成就(MaskAchievements,失败静默当全 false:编辑器里稀有件变灰、提交带它会被拒;已存的配方不动,现有渲染不变)。
2. 捏面具 PATCH /v1/masque/mask:handler 读 body 截到 4096,service 拒 > 2048(两个上限);逐项校范围,稀有件按成就硬控(ErrLocked);存的是校验通过的原始 body(json.RawMessage),落进 jsonb 列(不是逐字节保真,但未知键会保留并回吐给对家);然后 UPDATE … WHERE user_id(裸 UPDATE 不是 upsert)再 EnsureIdentity 回读:没 enter 过先 PATCH = UPDATE 影响 0 行、回包前补建一行默认 identity、回 200、配方丢了;UPDATE 成功但回读失败 = 报错但已存。
3. 配对 POST /v1/masque/match:ActiveCount ≥ 3 → ErrFull;FindEligiblePartner 从一度 + 二度好友圈 ORDER BY random() LIMIT 1,只排除「已和我 active 配过的」—— 不查对方的 active 数、不看 deleted_at、不看拉黑;CreatePairing → EnsureIdentity(partner)(这步失败 = 配对已 active 在库里、接口报错)→ WS masque.paired 给对方(带我的 partner_mask_config);回给我的 Pairing 没带对方的 mask_config,App 直接插进列表 → 刚配上那一刻看到的是 🎭,刷新才对。
4. 聊天 POST …/messages:先查上一条是谁发的(错误吞)→ INSERT → UPDATE 默契(一来一回 +2、当天首次 +10);INSERT 成功 UPDATE 失败 = 消息在库里、接口 500、WS 没推;刷新能不能看到取决于 History:它是 ORDER BY id ASC LIMIT 300 无分页,只拿最早 300 条,超过之后新消息刷新也拿不回。对外的 Message 只有 from_self,服务端主动输出的身份字段揭面前一个不出(用户自己在聊天 / 图片 / 配方未知键里自曝拦不住)。
5. 发图 POST …/image(Lv2):解码 → 重编码 JPEG q85(剥 EXIF / GPS)→ PutObject 到 masque/{pairing}/{uuid}.jpg → 落库 → 推;对象写成落库失败 = 孤儿对象,无清理。取图 GET …/media/{msg} 裸路由自己解 token,后端代发,private, max-age=3600,不签名不走 CDN(和真人头像不一样)。
6. 揭面 POST …/unmask(Lv4 = 66 分):SetUnmask 读 → 改一列 → 再读;双方都 true → 互推真身(Profile 只回 /v1/users/{id}/avatar,不回对象键);Profile 失败时 Revealed=true 但没资料,界面「已揭面」却空,下一次查询成功才补上。没有「重新戴上」。
7. 离开 POST …/leave:status='ended' + WS;消息和图片一个不删。举报 = 先写 reports(带真身)再 Leave,两步不在一个事务。
哪一步会断、留下什么:见各步括号;总的说:任何一步的「回包失败」都不能推「没生效」,这个包里没有一个事务边界。
出处:暗门
[code@c854ae83 app/lib/screens/chat_screen.dart:2363-2366]、[code@c854ae83 app/lib/screens/search_screen.dart:71-75];路由[code@c854ae83 backend/internal/domain/masque/handler.go:24-36];进舞会[code@c854ae83 backend/internal/domain/masque/repository.go:23-44],代号:371-377;成就静默[code@c854ae83 backend/internal/domain/masque/service.go:54-64];捏面具service.go:78-117(两个上限[code@c854ae83 backend/internal/domain/masque/handler.go:52]/[code@c854ae83 backend/internal/domain/masque/service.go:80]),裸 UPDATE[code@c854ae83 backend/internal/domain/masque/repository.go:48-52];配对service.go:119-150,候选查询[code@c854ae83 backend/internal/domain/masque/repository.go:219-244];聊天[code@c854ae83 backend/internal/domain/masque/repository.go:309-345];发图service.go:213-243,对象键:233;揭面service.go:265-301,SetUnmaskrepository.go:189-216;离开 / 举报service.go:303-325;上限[code@c854ae83 backend/internal/domain/masque/model.go:80-91];App 侧[code@c854ae83 app/lib/screens/masque_screen.dart:82](3 段)、[code@c854ae83 app/lib/screens/masque_chat_screen.dart:438](Lv4)、[code@c854ae83 app/lib/screens/mask_builder_screen.dart:83-136]、[code@c854ae83 app/lib/widgets/masque_mask.dart:6-46];表[code@c854ae83 backend/migrations/000013_masque.up.sql:5-32]、[code@c854ae83 backend/migrations/000018_mask_config.up.sql:3];守卫:后端两个包零测试,App 侧面具零行为测试(只有弹窗玻璃样式popup_glass_b6_0902_test.dart扫到它)。
2. 用户能看到的能力(怎么用 / 限制 / 入口)
| 能力 | 怎么用 / 限制 | 后端入口 | App 落点 |
|---|---|---|---|
| 捏换装人偶 | 个人中心「HOOP Avatar」;男女各一套;发型女 9 款 / 男 14 款、眼 / 嘴 / 眉(女 0 款眉)、7 档肤色、6 档脸型;没有上衣、没有鼻子素材(层在、图没有);不存 | 无(V2 不调接口) | app/lib/avatar/hoop_avatar_screen.dart |
| V1 换装(存档) | 无入口;kHoopAvatarVersion = 'Avatar Version 1';9 槽位、12 发色 12 衣色、700ms 防抖自动存;藏了 Bottom / Shoes 入口 |
GET/PUT /v1/me/hoop-avatar(≤ 16 件、件名白名单) |
hoop_avatar_studio_screen.dart(只测试可达) |
| 挑预设头像 | 个人中心「虚拟人像」/ 引导卡「魔法」/ 我的头像页「Library」;195 张按性别 / 年纪 / 肤色 / 眼镜筛(hair / hat 标签有但筛不了);存成 games 分身脸 | POST /v1/me/personas/games/avatar |
avatar_picker_screen.dart |
| 换 / 看 / 分享自己头像 | 资料页点头像 → 全屏页;笔 = 相册或 Library;分享没头像不丢空文件 | POST /v1/users/me/avatar |
avatar_viewer_screen.dart |
| 看别人头像 | 别人资料页长按(短按是羁绊页);群 / 社群资料页点头像放大 | GET /v1/users/{id}/avatar(裸路由) |
PeerAvatarViewerScreen |
| 头像形状 | 圆 / 圆角方 / 三角 / 圆角方挖空三角(工作房);有真图的 AI 与工作房 = 圆角方,图加载失败露出底下的标记;首字母按 runes | — | widgets/hoop_avatar.dart |
| 群 / 社群头像 | owner / admin 才能传社群头像;传了同步公告 + General | POST /v1/communities/{id}/avatar、POST /v1/conversations/{id}/avatar |
群 / 社群资料页 |
| 假面舞会 | 暗门 /mask;代号 + 面具;最多 3 段 active 配对(只管发起方);Lv1 贴纸 / Lv2 图 / Lv4 揭面 |
/v1/masque/*(12 条) |
masque_screen、masque_chat_screen |
| 捏面具 | 形状 3 / 主色 12 / 边框 4 / 花纹 4 / 装饰 6 选 ≤ 3;稀有件灰 + 服务端硬控 | PATCH /v1/masque/mask |
mask_builder_screen.dart |
| 房里的头像 | 幽灵房 = 空的绿圆角方块(👻 已删);狼人房 = 金色;游戏房 = 影子 id 反查的 games 脸,回落真脸 | GET /v1/games/{id}/avatars/{guid} |
conversations_screen.dart:2427-2440 |
出处:入口
[code@c854ae83 app/lib/screens/account_screen.dart:535-538]、:515-518;V2 素材缺口(层在图没有)[code@c854ae83 app/lib/avatar/hoop_avatar_screen.dart:304-306];V1 常量[code@c854ae83 app/lib/screens/hoop_avatar_studio_screen.dart:35-40],防抖:286,藏槽位:124;后端上限[code@c854ae83 backend/internal/domain/avatar/avatar.go:29-33];社群头像权限[code@c854ae83 backend/internal/domain/community/community.go:129-135];房头像颜色[code@c854ae83 app/lib/screens/conversations_screen.dart:2427-2440];影子脸[code@c854ae83 backend/internal/domain/games/handler.go:447-458];别人头像长按[code@c854ae83 app/lib/screens/user_profile_screen.dart:474]。
3. 内部怎么运作
3.1 四样东西的边界(和三个撞名)
app/lib/widgets/hoop_avatar.dart= 真人头像组件(515 行);app/lib/avatar/hoop_avatar.dart= 换装人偶导出桶(12 行);app/assets/hoop_avatar/= V1 换装素材。三者互不相干,account_screen.dart同时 import 前两个。app/assets/avatar/= 换装人偶 V2 现役素材(55 webp + manifest,13 MB);app/assets/avatars/= 预设头像库(195 webp)。差一个 s。HOOP_AVATAR.md说前者是「画师原始素材、只读不删不改」是 07-03 的旧话,照它去清理会删掉线上的 Avatar。- 「V2」在仓里指两件相隔 45 天的东西:CHANGELOG v0.179 的「HOOP Avatar V2」= 07-16 画师第二批 105 件(V1 线);
HOOP_AVATAR.md的「Avatar Version 2」= 08-30 Mike 交的整包(现役)。本文只用 Version N 指整代替换,画师批次另说。 - 一句话边界:换装人偶是你捏的,预设头像库是你挑的,真人头像是别人看到的那张,面具是你在舞会里那张不是你的脸。 前三者今天在代码上互不相通;面具是彻底独立的第四条线。
出处:
[code@c854ae83 app/lib/screens/account_screen.dart:41-45](两个 import + 「V1 故意留着不删」);[code@c854ae83 app/pubspec.yaml:193-200](三个素材目录四行紧挨着)。
3.2 换装人偶:V2 怎么画、V1 怎么存、为什么接不上
V2 怎么画:AvatarSelection(characterId / hairId / eyesId / mouthId / toneId 必填,browsId / topId / faceShapeId 可空)→ AvatarPainter 每帧按 manifest.layerOrder(hair_back → body_base → clothing → face_base → eyes → brows → nose → mouth → hair_shadow → hair_front)重排,不信调用方顺序;每层原尺寸 1:1 贴到 1662 画布,缩放只在最后对整张做一次;1:1 时 FilterQuality.none + isAntiAlias=false(否则睫毛糊、层边接缝);重绘判据是 signature 指纹(脸型必须进指纹);肤色只对 body_base / face_base / eyes / brows / nose / mouth 六层走 skin.frag;脸型变形是重活(六档 2253 ms 女 / 1298 ms 男)搬到 isolate,整幅 warp 只留当前一档(以前六格各留一颗 = 常驻 211 MB)。素材:女 27 张 / 男 28 张,manifest 引用与磁盘一张不多一张不少;目录编号是跳的(catalogue 只上架了这些);女生 0 款眉、男女 0 件上衣、0 个鼻子,对应分页不显示。启动一次解码全部 55 张;AvatarBundle.load() 抛错 → _boot 的 catch 落到 _error,整屏一行裸 Text('$_error') 没重试;它一直不完成(不抛)→ 永远转圈 —— headless widget test 里 FragmentProgram.fromAsset 就是后者,所以 ui_screenshot_test.dart 整份 skip: true(跳过 ≠ 验过);真机上是哪种本轮没测。
V1 怎么存:进屏 _discover() 从 AssetManifest 归类 208 张 → _restore() GET(exists:false 不是错;失败静默用默认,这时随手一动、只要提交的是不带色号的合法配方且成功,就把默认档写回去覆盖真存档)→ 每次点选 700 ms 防抖 PUT,parts = {槽位: "W-Hair_2"},选了色号才拼成 "W-Hair_2#gold";后端校 gender ∈ {W,M}、≤ 16 件、槽位白名单、件名白名单,单句 upsert 整行覆盖(不可能部分写入);校验 400 在写库之前,确认一个字节不写;数据库明确报错 = 那句 upsert 没提交;执行结果不明(超时、连接断)和提交后回包失败 = 可能已写;App 侧四种都只是顶栏 avatar_save_failed 2 秒淡出,没有重试、没有暂存。
为什么接不上:① V2 屏 onDone 在入口没传、无 API import;② V1 带色号的件名后端拒、不带的能存(App 测试 avatar_sheet_upper_body_0827_test.dart:382 钉的正是 M-Bottom_3#blue;debugOnSave 之后代码照样调 putHoopAvatar,那条测试有没有隔离 HTTP 要看它的 mock,本文未核);③ V2 配方结构(角色 / 多层发型 / 脸型 / 肤色)不是九槽位件名;④ hoop_avatars.parts 没有版本字段,老档新档分不开。那条「换版本必红」的守卫只盯 V1 目录,V2 换的是入口和另一个目录,所以它一直绿,kHoopAvatarVersion 至今写 V1。
素材在包里的代价:加一款发型 = 出一次 TestFlight(v1.585 记过);V1 的 20 MB PNG 仍打进每个安装包(故意留,删要先问)。发型抠图 / 配准 / 脸型规格的车间(hoop-avatar-workshop、FACE_SHAPE.md、scripts/v2/)全在本仓库外,四项验收阈值从哪来这里读不到。
出处:选择模型
[code@c854ae83 app/lib/avatar/avatar_stack.dart:4-22],默认:55,指纹:46-47;渲染[code@c854ae83 app/lib/avatar/avatar_renderer.dart:10-14,52-66];肤色[code@c854ae83 app/lib/avatar/skin_recolor.dart:13-21];脸型 isolate[code@c854ae83 app/lib/avatar/faceshape_worker.dart:32]、耗时[code@c854ae83 app/lib/avatar/faceshape_image.dart:13];内存[code@c854ae83 app/lib/avatar/hoop_avatar_screen.dart:91-103],onDone:34、:241-244,启动:135-153,错误屏:311-315,分页缺素材:304-306;manifest[code@c854ae83 app/assets/avatar/manifest.json:4];跳过的测试[code@c854ae83 app/test/ui_screenshot_test.dart:38-48];V1[code@c854ae83 app/lib/screens/hoop_avatar_studio_screen.dart:22-40](Mike 原话 + 两常量)、:256(GET)、:279-280(失败静默)、:286-300(防抖 + 件名#)、:124(藏槽位);后端[code@c854ae83 backend/internal/domain/avatar/avatar.go:29-33]、:48-61(读,坏 JSON 静默变空)、:71-76(upsert)、:86-87(路由)、:106-137(校验);表[code@c854ae83 backend/migrations/000164_hoop_avatars.up.sql:14-19];守卫[code@c854ae83 app/test/hoop_avatar_version_test.dart:21-36](只看 V1 目录)、[code@c854ae83 app/test/avatar_sheet_upper_body_0827_test.dart:382]、hoop_avatar_v2_smoke_test.dart、avatar_faceshape_worker_pixels_0908_test.dart、avatar_layer_reuse_0909_test.dart[decision@2026-07-16,Jeff:「Avatar 做好了就是我们的身份」→ 存后端配方不存图;先只活在 HOOP Avatar 页里][decision@2026-08-27,Mike Ng:专注上半身,下半身入口先藏][decision@2026-08-30,Mike Ng:「把这个版本 archive 起来先…名为 Avatar Version 1…新的你自己名 Avatar Version 2」][decision@2026-09-09,Mike Ng:选脸型要点两次 / 回默认才换 → 预解码整图 + 每层 ValueKey(v1.957)]。
3.3 预设头像库
195 张 webp + manifest,7 个标签维度(gender / age / eth / hair / glasses / hat / …),UI 只暴露 4 个;选一张读 asset 字节 → uploadPersonaAvatar('games') → bust('/v1/users/{me}/avatar?scope=games')。它替代的是 fluttermoji 捏脸(Jeff v0.241),旧捏脸屏留着但无引用,fluttermoji 依赖仍在 pubspec。manifest 与磁盘一张不多一张不少。
出处:
[code@c854ae83 app/lib/screens/avatar_picker_screen.dart:25-41,82-98,145-167];死屏[code@c854ae83 app/lib/screens/virtual_avatar_screen.dart:1-7];守卫avatar_picker_golden_test.dart。
3.4 真人头像:键、版本号、形状、同步
见 §1.1。补三条:工作房现役形状是 _WorkroomMarkPainter:圆角方块挖空一个三角、洞里放首字母(Jeff 08-09 发图),有真图才在上面叠圆角方图,图加载失败露出底下的标记;08-02 那版「内三角 78% + 外圈粗描边」的 _TriangleRingPainter 和 coreSide 0.78 到不了,是死代码;有真图的 AI 也画圆角方块;社群 → 两间房同步:uploadAvatar 顺手两条 UPDATE,判据 coalesce(avatar_url,'') IN ('', $2),$2 是固定的 community-avatars/{cid}(空或还是社群这个键才覆盖,人自己换过的不动);顺序是四步:PutObject 覆盖对象 → UPDATE communities(失败回 500,对象已变)→ UPDATE 公告房 → UPDATE General 房(这两条失败只记日志、继续),回包带 followed 房 id,000286 是一次性回填并抬 updated_at(否则增量同步水位下不去),它注释里「公告无条件补」已是旧规则;取址规则在后端 SQL(私聊 = 对方 /avatar,群 = conversations.avatar_url,成员 = 分身脸优先),App 只管兜底和版本。
出处:形状
[code@c854ae83 app/lib/widgets/hoop_avatar.dart:23-38,234-330],例外:309-311,死画笔:437,491-515,色板:154-181,首字 runes:202-215;同步[code@c854ae83 backend/internal/domain/community/community.go:184-231];回填[code@c854ae83 backend/migrations/000286_community_rooms_follow_avatar.up.sql:3-39];取址[code@c854ae83 backend/internal/domain/conversation/repository.go:1392-1396]、[code@c854ae83 backend/internal/domain/calls/log.go:366-368];守卫hoop_avatar_shapes_golden_test.dart、hoop_avatar_triangle_ring_golden_test.dart、ai_avatar_shape_probe_test.dart、hoop_avatar_emoji_initial_0907_test.dart、community_avatar_shows_0823_test.dart、group_avatar_neutral_0908_test.dart[decision@2026-08-01,Jeff:「全部与 AI 及工作室都用三角为图像,三个尖角都要是有圆弧的」][decision@2026-08-02,Jeff:「不动AI头像,只改工作房,外面加粗线三角围着」][decision@2026-08-10,李敏:20 个 AI 图标 → 有真图改画圆角方块][decision@2026-08-23,Jeff:社群换头像公告和 General 要跟着换][decision@2026-09-02,李敏:「remove all of these emoji as icon」→ 幽灵房 👻 删]。
3.5 面具:配方、解锁、值语义
见 §1.2。配方键名与 MASK_ASSETS.md 一字不差,两处值语义那份图纸写错:shape 2 = 蝶形(不是皇冠);border 0 = 黑曜(不是「无」),1 金 / 2 银 / 3 玫瑰金。解锁表(唯一并排写下的一处):金边 border 1 = 默契 ≥ 36(Lv3);金丝纹 pattern 3 = ≥ 18(Lv2);宝石 deco gem = 揭过面;孔雀翎 deco peacock = 活跃 ≥ 7 天;默契阈值 [0,6,18,36,66],Lv1 贴纸 / Lv2 图 / Lv4 揭面。mask_config.v 读进来从不用;created_at 只写不读;MaskAchievements 那条查询不带 status='active' 用不上部分索引、sender_id 无索引 —— 每次 enter / setMask 都跑它,代价可能高(执行计划未 EXPLAIN,不写成必然全表扫)。面具不存图:配方 → 客户端 MaskPainter 现画,仓里没有一张面具 PNG。
出处:值语义
[code@c854ae83 app/lib/widgets/masque_mask.dart:6-20],色板:49-63;解锁位[code@c854ae83 app/lib/screens/mask_builder_screen.dart:102-124];服务端[code@c854ae83 backend/internal/domain/masque/service.go:54-64,86-112];阈值[code@c854ae83 backend/internal/domain/masque/model.go:78-91];成就查询[code@c854ae83 backend/internal/domain/masque/repository.go:55-62];索引[code@c854ae83 backend/migrations/000013_masque.up.sql:21-32]。
3.6 删号与残留
删号 = 抹 user_profiles 字段不删 users 行(否则别人和他的聊天 / 群成员 / 战绩外键崩),代价是别的表上 ON DELETE CASCADE 永远不触发,要靠 hook 在同一事务里抹;今天只挂了 eKYC 的 Eraser。所以到期清理那一轮跑成功、hook 放行时:头像置 NULL、昵称 Deleted user,但 hoop_avatars、masque_identities(配方 + 代号)、masque_pairings、masque_messages、R2 里 masque/… 图片全在;FindEligiblePartner 不看 deleted_at,他还会被配到。舞会 ended 的配对也没有清理任务。
出处:
[code@c854ae83 backend/internal/domain/auth/repository.go:401-407],抹字段:468-476;hook 只挂 kyc[code@c854ae83 backend/internal/server/router.go:79];级联列[code@c854ae83 backend/migrations/000164_hoop_avatars.up.sql:15]、[code@c854ae83 backend/migrations/000013_masque.up.sql:6];候选不看删号[code@c854ae83 backend/internal/domain/masque/repository.go:219-244]。
3.7 素材目录的盲区
docs/assets.html 由 tools/gen_assets_catalog.py 现扫 app/assets/,GROUPS 白名单有 avatars(预设头像 195)和 hoop_avatar(V1 纸娃娃 208),没有 avatar → 现役 V2 素材在素材总目录里隐形,重跑也不会出现,「663 件 / 230.5 MB」少数了 55 件 13 MB;面具零条目(本来就没图)。它从没进过 00-目录.md。
出处:
[code@c854ae83 tools/gen_assets_catalog.py:33-35]。
4. 已知缺口与待核事项(逐行看状态)
| 事项 | 状态 | 出处 / 怎么核 |
|---|---|---|
| 换装人偶持久化:V2 零;V1 带色号件名后端拒(不带的能存);V2 配方与 schema 不合;parts 无版本字段 | 已核实没做 / 硬阻塞(Jeff 07-16 的「存后端」与 08-30 后的实现不一致,没找到改决定的记录) | hoop_avatar_screen.dart:34、avatar.go:29、hoop_avatar_studio_screen.dart:291 |
| 换装人偶给别人看(按 uid 读的公开端点)/ 生成头像图当 avatarUrl / 与圈子灯城堡打通 | 已核实没做;且素材在包里就不可能有服务端下发的装扮 | avatar.go:9-10;CHANGELOG v1.585 |
「换版本必红」守卫只盯 V1 目录,V2 换入口它没红;kHoopAvatarVersion 仍 V1 |
现状 / 假绿守卫 | hoop_avatar_version_test.dart:21 |
| V2 素材:女 0 眉、男女 0 上衣、0 鼻子;layerOrder 的 clothing / nose 两层永远空 | 现状(catalogue 只上架了这些) | manifest;hoop_avatar_screen.dart:304-306 |
V2 唯一 UI 测试 skip: true(着色器在 widget test 里挂住);验收只在真机 / 网页版 |
现状:跳过 ≠ 验过 | ui_screenshot_test.dart:38-48 |
发型 / 脸型的车间与规格(hoop-avatar-workshop、FACE_SHAPE.md、scripts/v2/)不在本仓 |
待核:接手的人读不到验收阈值从哪来 | CHANGELOG v1.851 / v1.906 |
| V1 20 MB PNG 仍打进包;fluttermoji 依赖与死屏仍在 | 现状(故意留,删要先问) | pubspec.yaml:193;virtual_avatar_screen.dart:1-7 |
| 头像库 hair / hat 标签有、筛不了 | 现状 | avatar_picker_screen.dart:25-35 vs :145-167 |
只传 userId 的头像调用点:登记表没这个 uid 时保持裸地址(旧缓存);有就补 v |
现状 | hoop_avatar.dart:59-70,116-126 |
版本登记只认 /avatar?v= 紧邻形式,?scope=…&v= 不匹配;表按 uid 不按 scope |
现状 / 识别边界 | hoop_avatar.dart:89-91 |
| 真人头像上传 PutObject 后写库,写库失败留孤儿对象;社群同步四步(对象 → communities 失败回 500 → 公告 → General,后两条失败只记日志) | 现状 | user/service.go:135-145,183-193;community.go:158-231 |
舞会历史 LIMIT 300 无分页,超过后新消息刷新拿不回 |
现状 | masque/repository.go:270-273 |
| 删号不抹换装配方 / 面具配方 / 舞会记录 / 舞会图片;删号的人仍会被配对 | 已核实没做(hook 只挂 kyc) | router.go:79;masque/repository.go:219-244 |
| 面具:配方落 jsonb 未知键保留并回吐(≤ 2048)、两个上限 4096 / 2048、裸 UPDATE 没 enter 先 PATCH 丢配方、Match 回包漏对方配方、3 段上限只管发起方、check-then-act 无锁、候选不看拉黑、代号 2400 种无唯一约束、成就查询索引适配不足(执行计划未核)、图片孤儿无清理 | 现状 / 缺口(全部源码推导,未实测) | §1.2 / §3.5 出处 |
| 后端 avatar / masque 两包零测试;App 面具零行为测试 | 现状 | ls backend/internal/domain/{avatar,masque} |
| 暗影空间里的人长什么样(头像照常?换脸?复用面具?) | 待拍板(SHADOW.md 一个字没提,也没人记过这条债) | docs/SHADOW.md |
| 面具随默契动态变化、稀有度动画、更多组件 | 没做,无人认领(面具线自 06-12 无任何决定) | TASKS.md:240,317 |
素材总目录漏扫 app/assets/avatar/,且从没登记进 00-目录 |
现状 | gen_assets_catalog.py:33-35 |
| 画布尺寸四个说法(迁移 2140×2044 / avatar.go 2140×4344 / V1 4280×8688 / V2 1662) | 现状:只有 V2 的 1662 是现役 | 000164:3、avatar.go:6、hoop_avatar_studio_screen.dart:227 |
DESIGN_SYSTEM.md §8 行号全漂 |
现状:按它 §11 重跑,别手改 | 00-目录.md:187 |
生产:R2 里 masque/ 对象量、avatars/ 旧对象删干净没、CDN 签名器开没开 |
待核 | — |
5. 出问题先查哪里 · 动它之前
排障一条路:
1. 「我捏的人偶不见了」:现状,V2 不存;不是故障。别去查后端表,那张表只有 V1 在写。
2. 「我换了头像别人还是旧的」:先看他那边有没有调过任何带你头像地址的接口(没有推送);再看是不是第一次见到你(第一次不算变);再看回包里的地址是不是 ?scope=…&v= 这种登记不到的形式;最后看那处调用是不是只传了 userId 且他从没登记过你。
3. 「AI / 工作房头像怎么是方的」:有真图就该是方的,三角(AI)/ 挖空三角(工作房)只是没图时的默认脸;图加载失败会露出底下的标记。
4. 「社群换了头像公告没跟」:看公告房 avatar_url 是不是被人手动设过别的(判据是空或社群固定键才覆盖);再看日志里那两条 UPDATE 有没有报错(它们失败上传照样成功);老社群是不是 000286 之前就设过。
5. 「面具金边点不动」:看默契最高分够不够 36;MaskAchievements 查询失败会静默变全 false(只影响编辑器解锁和提交,已存配方不变)。
6. 「面具存了没生效」:先 enter 过没有(裸 UPDATE);body 是不是超 2048。
7. 「刚配上看到的是 🎭」:Match 回包不带对方配方,刷新即对。
8. 「素材目录里没有 avatar」:生成器白名单少一组。
9. 「换装屏一直转圈」:加载抛错会落到错误屏,一直转圈是加载的 Future 没完成(headless widget test 里着色器就是这样);真机 / 网页版看,不要在 widget test 里等它。
改它之前:
- 别按 HOOP_AVATAR.md 动 app/assets/avatar/:那是现役素材,不是原始素材。
- 要给 V2 接持久化:先定新 schema(角色 / 多层发型 / 脸型 / 肤色 / 版本字段),avatar.go 的正则和 000164 都要动,不是「把 onDone 接上」;V1 那条件名带 # 的路也没通过,别照抄。
- 动 HoopAvatar 版本号:登记点只有 ApiClient._decode 一处,别在别处再存一份地址(Mike 09-02 那病就是十几处各存一份);改 ?v= 用 Uri.replace 别手拼。
- 动形状:三角 / 挖空三角 / 圆角方的例外规则在组件里,不在 DESIGN_SYSTEM.md;改前跑 ai_avatar_shape_probe_test 看 A / B 对照。
- 动面具:配方值语义以 masque_mask.dart 为准,不是 MASK_ASSETS.md;SetMaskConfig 改 upsert 前先看有没有人依赖「没 enter 就 200」;加索引前看 MaskAchievements 那两条查询。
- 动删号:要抹形象就在 PurgeHook 里挂,和 kyc 同一事务;别去改成删 users 行。
- 加发型 / 脸型素材:每加一款 = 出一次 TestFlight;男女两套车间脚本不同(--male);manifest 引用与磁盘要一张不多一张不少(smoke test 会红)。
- 别信的旧图纸:HOOP_AVATAR.md 除 08-30 那节全过期;MASK_ASSETS.md §0/§1/§3 是没走的路;DESIGN_SYSTEM.md §8 行号全漂;GHOST_ROOMS.md:89 的 👻 没了;WEREWOLF_ROOMS.md:125,129 那张牌桌屏不存在。
6. 不能碰的数(当前值 / 实现出处 / 守卫或未覆盖)
🔒 = Jeff / 李敏 / Mike 明确锁定的;其余是普通配置值,改之前把守卫一起改。
| 数 | 当前值 | 实现出处(c854ae83) | 守卫 / 未覆盖 |
|---|---|---|---|
| V2 画布 / 层数 / 肤色 / 脸型 | 1662;10 层;7 档;6 档 | app/assets/avatar/manifest.json |
hoop_avatar_v2_smoke_test.dart(清单 / 贴图 / 着色器 / 底板) |
| V2 素材 | 女 27 / 男 28 张 webp | app/assets/avatar/{female,male} |
同上(引用与磁盘一致) |
| V2 缩略图 / 网格列 | 288 px;4 列 | hoop_avatar_screen.dart:103,807 |
未覆盖 |
| V1 🔒 | 'Avatar Version 1';208 张;tag avatar-v1 |
hoop_avatar_studio_screen.dart:35,40 |
hoop_avatar_version_test.dart:22-54(只盯 V1 目录) |
| V1 存档 | ≤ 16 件;件名 ^[WM]-(9 槽)_\d{1,3}$;700 ms 防抖 |
avatar.go:29-33;hoop_avatar_studio_screen.dart:286 |
后端未覆盖;App avatar_sheet_upper_body_0827_test.dart |
| 预设头像库 | 195 张;4 个筛选维度 | app/assets/avatars;avatar_picker_screen.dart:145-167 |
avatar_picker_golden_test.dart |
| 头像组件 🔒 | 默认 radius 20;12 色板;圆角方 0.3;三角圆角 0.24;工作房洞里字母 y 0.16(0.78 那版到不了) | hoop_avatar.dart:45,154-158,361,424-427,259,291 |
hoop_avatar_shapes_golden_test.dart、hoop_avatar_triangle_ring_golden_test.dart |
| 个人中心大头像 🔒 / 底栏「我的」 | 直径 120;30 | account_screen.dart:55;me_nav_avatar.dart:48 |
account_big_avatar_golden_test.dart、me_nav_avatar_identity_test.dart |
| 面具配方 | shape 0-2 / color 0-11 / border 0-3 / pattern 0-3 / decos ≤ 3;body ≤ 2048(handler 截 4096) | masque/service.go:80-87;handler.go:52 |
未覆盖 |
| 面具解锁 | 金边 36 / 金丝纹 18 / 宝石揭过面 / 孔雀翎 7 天 | masque/service.go:59-64 |
未覆盖 |
| 舞会 | 3 段 active;阈值 [0,6,18,36,66];Lv1 贴纸 / Lv2 图 / Lv4 揭面;一来一回 +2、当天首次 +10;图 12 MB、JPEG q85;历史最早 300 条无分页 | masque/model.go:80-91;repository.go:330-336;handler.go:21;service.go:172,230 |
未覆盖 |
| 代号 | 24 字母 × 100 = 2400 种 | masque/repository.go:371-377 |
未覆盖 |
7. 出处与旧记录去向
- 代码(全部
[code@c854ae83]):backend/internal/domain/avatar/avatar.go(137 行,唯一文件);backend/internal/domain/masque/{handler,model,repository,service}.go;backend/internal/domain/user/{handler,service}.go头像那半;community/community.go:129-231;conversation/repository.go:528-529,1392-1396;calls/log.go:366-368;games/handler.go:447-458;auth/repository.go:401-483;config无形象相关开关。App:app/lib/avatar/{avatar_stack,avatar_renderer,avatar_manifest,avatar_images,skin_recolor,faceshape_field,faceshape_image,faceshape_worker,hoop_avatar_screen}.dart;screens/{hoop_avatar_studio,avatar_picker,avatar_viewer,virtual_avatar,mask_builder,masque,masque_chat,account}_screen.dart;widgets/{hoop_avatar,masque_mask,me_nav_avatar,member_status_avatar,avatar_blur_stage,avatar_zoom}.dart;app/assets/{avatar,avatars,hoop_avatar};tools/gen_assets_catalog.py、tools/bake_hoop_avatar.py(V1 烘焙,源目录hoop_avatar_src/已不存在)。 - 迁移:000013 masque 三表、000014 默契列、000018
mask_config、000164hoop_avatars、000286 社群房跟头像回填。本模组最新 000286;仓库最新 000364。 - Redis:无。WS:masque 五种事件(
masque.paired / message / unmask_wish / unmasked / ended);头像没有 WS 事件。推送通知:无。 - 生产:本文没有在生产机核任何项(R2
masque/与avatars/对象、CDN 签名器、网页版渲染)→[prod@ 待核]。 - 决定(时间由旧到新):07-16 Jeff「Avatar 做好了就是我们的身份」存配方不存图、先只活在 HOOP Avatar 页;07-18 Jeff 头像层留 git 不搬 R2(
ASSETS_R2.md);07(v0.228)Jeff「画的东西太少,帮我丰富」→ 12 色染色;07(v0.241)Jeff 虚拟人像改从预生成库挑;08-01 / 08-02 Jeff 三角形状语言、工作房外圈粗三角;08-09 Jeff 发图改工作房为圆角方块挖空三角、洞里放字母;08-09 Jeff 三张真头像左中右;08-10 李敏 20 个 AI 图标 → 有真图改圆角方;08-21 李敏 点头像直接换;08-23 Jeff 社群头像公告 General 跟着换;08-27 Mike 专注上半身、下半身藏;08-28 Mike 人物放大、4 列网格;08-30 Mike 存档 Version 1、交 Version 2 整包;08-31 / 09-04 / 09-06 Mike 发型 4→5→9(女)、7→14(男);09-02 Mike 头像版本号收一份;09-02 李敏 去 emoji 图标(👻 删);09-07 Mike 脸型引擎「做 1」+ Shape 分页;09-09 Mike 选脸型要点两次 → v1.957。面具线:06-11 / 06-12 上线后无任何决定。 - 未核清单:⓪ V1 那条
#blue测试有没有隔离 HTTP;成就查询的执行计划;真机上着色器是抛错还是挂起;① 生产未核(见上);② 并发交错(舞会配对 / 揭面)是源码推导;③ V2 在真机 / 网页版的渲染只在 Mike 录屏里见过,CI 里那份跳过;④ 女鞋站姿那类美术问题无法从代码判断;⑤ 车间脚本与规格文件在仓外;⑥avatar-v1tag 在别人 clone 里 fetch 到没有。 - 旧记录去向:
HOOP_AVATAR.md整份替代(只搬 Mike 08-30 两段原话、tag 取回法、「用 tag 不另存文件夹」判据进 §3.2;00-目录.md:205那行「bake 脚本 → 728×1477」三个关键词全是 V1 的,要改);MASK_ASSETS.md§0/§1/§3 作废、§2 键名与解锁表 + §4 配色并入 §3.5(修正 shape 2 / border 0 两处值),顶部加一行「零素材落地」;DESIGN_SYSTEM.md§8 行号层由本文替代、其余按它 §11 重跑;GHOST_ROOMS.md:89、WEREWOLF_ROOMS.md:125,129各替代那几句,回写请求留给房型那份的维护者;ASSETS_R2.md、SHADOW.md、assets.html不动(后者要登记进00-目录.md并标明漏扫);等你拍板.md要加两件:换装人偶要不要接回持久化(要就重定 schema)、暗影空间里的人长什么样;面具线加一件:要不要有个拍板人。