罗德岛通讯与技术部

衣柜(D8)设计

WARDROBE

所以这一片落的不是服装数据(数据早就够),是让"当前穿着"变成一个可以改、且只有一个权威的状态。

状态:📐 设计已定案,未开工 · 2026-09-14 定稿 上游:docs/backlog.mdD8(那行有 2026-09-12 从 ZOOT 拆出来的四条机制,本文件按它写) 相关:unified-memory-design.md(按线状态 / 覆盖层)· conventions.md.char 五段)

1. 要解决的问题(一句话)

她能说"我穿着那件黑风衣",但说不出"我今天换了件别的" —— 因为那句话在 ## 锚 里,而锚是「不可变 · 永不被修改」(char_file.py 有拦)。

实测(2026-09-12 盘点,391 个 .char):

事实 数字
全文含服装描述 276
写进可变段 ## 物理标签 54
其余大部分 ## 锚身体: 一行里

所以这一片落的不是服装数据(数据早就够),是让"当前穿着"变成一个可以改、且只有一个权威的状态

2. 已定案(别再问)

决定 选择
谁触发 用户主动:聊天页「更多」菜单里一颗「衣柜」按钮 → 打开面板手选
写在哪一层 按线(与「## 我 / ## 物理标签 跟线」的既定决策一致)
付费方式 买断:花 LMD 买一次、永久拥有,之后反复穿不花钱(衣服是资产,与礼物那种消耗品区分开)
槽位 多槽位:v1 = 外套 / 内搭 / 配饰
入口 聊天页「更多」菜单(#chat-more-menu

⚠️ 入口的语义提醒:「更多」菜单现在装的是轮级 / 一次性动作(收藏、导出、回退、清空)。换装是持久状态,所以按钮文案要说清「这会改她的当前穿着」,别让它看起来和"导出聊天记录"同级。

3. 数据模型:四方,谁是谁的权威

① 目录   GARMENTS            id / 名称 / 槽位 / 价格(LMD) / 描述 / 写进标签的文本
② 拥有   per-user 买断集合    {garment_id, ...}          ← 全局,跨线共享
③ 穿着   per-line sidecar     {外套: id, 内搭: id, 配饰: id}   ← 按线
④ 渲染   ## 物理标签 的托管条目  「穿着:…」               ← 从 ③ 生成,不是状态

⚠️ 权威是 ③,不是 ④。 理由:## 物理标签 是「可变 · 追加」的条目列表(char_file.pySection.items: list[str]),深聊的自动检测也会往里追加。所以「读物理标签还原当前穿着」这条路必然会读到别人写的东西 —— 这个仓库已经栽过一次同款(归位标记只活在内容里 → 被覆盖写抹掉)。

因此:

  • 写入 ③,然后重渲染 ④
  • ④ 用带前缀的托管条目穿着:)标识"这几条归衣柜管";
  • 换装时按前缀找到旧行替换,不是往后追加 —— 否则穿十次就在她的物理标签里留十行;
  • 空槽位不渲染那一行(不写「配饰:无」)—— 空行会被模型当事实引用;
  • ## 物理标签 段的 .char(如 texas.char)要自动补这个段,不能假设它在(五段不是每个文件都齐,同「我段 =0 行 → 全写」那一类坑)。

3.1 存储位置

按线状态目录(data/users/{u}/lines/{safe(line)}/)加一个 sidecar(如 worn.json)。⚠️ 不要把状态塞进老 char 文件或覆盖层文本里再解析回来。

4. 机制①:一类可变状态只能有一个权威,且明说旧字段作废

这是 backlog 那行点名的硬要求,也是这一片最容易做错的地方。

ZOOT 的 prompt 原文(要抄的就是它):

衣着信息只能以衣柜当前状态为准,不得读取或沿用档案中的旧衣着字段

⚠️ 只说「新的优先」不够 —— 模型会在两处之间混着引,而这不报错。 ⚠️ 我们的场景比 ZOOT 更危险:ZOOT 那三张表是空的、没有"旧字段"要作废;我们的锚里有 276 份旧衣着描述,而且锚永远不会被改

所以注入侧必须带一段显式作废声明(措辞可在实现时定,语义必须是这两条):

  1. 当前穿着以「穿着:」那几行为准;
  2. ## 锚身体:/外观描述是加入我们之前的默认装束,不得当成此刻的穿着;被问到时不要混着引。

5. 接线

端点 作用 抄谁
GET /api/market/wardrobe 目录(按槽位分组)+ 我的拥有 + 当前穿着 market_gifts
POST /api/market/wardrobe/buy 扣 LMD → 进拥有集合 market_buy_gift
POST /api/market/wardrobe/wear 装到某个槽位(写 ③ → 重渲染 ④)
POST /api/market/wardrobe/take-off 卸下某个槽位

⚠️ 买与穿分开两个动作(和礼物「买 → 送」同形):买是经济行为、穿是状态行为,混成一个端点将来没法回答"她身上这件是什么时候买的"。

6. UI

聊天页「更多」菜单 →「衣柜」(sheet):

  • 槽位分列:外套 / 内搭 / 配饰,每列列出该槽位的衣服;
  • 已拥有:点一下 = 穿上(当前穿的那件有选中态);
  • 未拥有:显示价格,点了先买(要 confirmDialog 确认花钱);
  • 缺段 / 没有衣柜状态时显示空态,而不是空白。

7. 验收(比「能换」更硬)

  1. 换装不动 ## 锚 —— 守卫钉住锚段在换装前后逐字节一致(不可变那段有代码拦,守卫要证明它真的拦住了衣柜这条新路);
  2. 负例(backlog 的 done 定义):造一个「档案说 A、当前穿 B」的场景,断言注入里只有一个权威、且模型不混着引
  3. 问她「你今天穿什么」 → 答的是 B;
  4. ## 物理标签 段的干员(如 texas)也能正常穿、正常渲染、正常注入;
  5. 空槽位不产生空行;穿十次后物理标签里仍然只有一行「穿着:…」。

8. v1 明确不做(写下来是为了别顺手做)

不做 为什么
## 锚 迁移那 276 份旧衣着描述 独立第二批;而且只能迁"当前穿着"那一类,不是所有提到衣服的句子。锚的定义就是"永不被修改",大面积动它是另一件事
LLM 提议 / 审批层(她提议换装 → 待审) backlog ③ 记着 ZOOT 有这个设计,但它一行数据都没验证过;我们 v1 先只做用户主动
审计流水(谁/何时/为什么) backlog ④;等有第二种触发源(她自己换、环境换)再加才有意义
环境联动(天气 / 世界 / 节日) 用户明确选"自己点按钮";环境触发是 v2 的候选
多世界不同穿着 穿着按线,已经隐含了世界(世界挂在线上);不需要额外维度

9. 分批(每批都能单独上线 + 单独验收)

内容 验收
D8a 目录 + 拥有 + 买(GET/POST buy)+ 守卫 买一件扣 LMD、余额不足报错、重复买不重复扣
D8b 按线 sidecar + 渲染托管条目 + 注入声明(机制①) 验收 1/2/4/5 —— 这一批是风险最高的(碰注入与 prompt)
D8c 衣柜 sheet + 「更多」菜单入口 验收 3
D8d(可选) 她的反应句

10. 未验证的假设 / 风险

  • 锚的不可变拦在哪(2026-09-14 已核准)CharSection.immutablechar_file.py:37,锚在 :103 建为 True)+ validate_immutability(section):812,返回 True = 不可修改)。 ⚠️ 它是个「检查函数」,不是写入口的硬拦 —— 调用方得自己查。所以衣柜的写路径必须显式调用它,而守卫要证明的是「衣柜写不动锚」是因为它自己查了,不是因为碰巧没走到那条路。(backlog 里记的 char_file.py:708 是旧行号。)
  • ⚠️ 「穿着:」前缀会不会和深聊的物理标签自动检测撞车(它也会往同一个段追加):开工时要看 deep_chat.py 那套检测写出来的条目长什么样,前缀不能撞。
  • ⚠️ 注入预算:物理标签本来就在 prompt 里,多一行几乎不增加成本;但作废声明那几句话要算进 token 预算(现有注入是有配额管理的)。
  • ⚠️ 经济平衡:衣服价格要和礼物、交易所的物价水平对一下,否则会出现"一件大衣 = 十束花加十点信赖"这种明显不划算的比较。