AskaEth 351891714a feat(chat): 单条消息的编辑与删除(D3)
契约 §1 行 6a/6b 的两个端点与 §3.8 的两帧,整条链接完(不含「重发」——契约
明确没有重发端点,客户端用编辑后的内容再 POST /chat 即可,本模块不做)。

数据层
- SessionsApi 增 PATCH / DELETE api/v1/messages/{id}(与既有消息端点同源,因此
  MessageRepository 能直接复用、不必动 AppContainer),MessageRepository 增
  editMessage / deleteMessage,走既有 apiCall / Result 风格。
- 响应形状照契约不照旧文:PATCH 的 data 直接就是消息 DTO(没有 message 包一层),
  DELETE 回 {deleted: id};编辑内容按「非空、≤4000 rune(码点,不是 length)」
  预校验,不合格一个请求都不发。

帧解析
- WsEvent 增 message_edited / message_deleted,形态照 message_persisted 那一套:
  会话归属先取帧壳、其次取消息自身的 sessionId,未知键安全、未知帧与坏帧安静忽略、
  永不抛。
- message_edited 的 data 就是消息本身(与 message_persisted 的 data.message 不同形),
  照抄那一套会解出 null——这一点由单测钉住。

VM
- 按 id 原位改 content / 原位移除。两条路各自只有一个落地出口:本端 PATCH/DELETE 的
  成功回执与随后到达的广播都调同一对纯函数,而它们幂等(同一内容做两次返回同一实例)
  ⇒ 「响应 + 广播」双到达收敛成一次,不需要去重表、也不会出现两套状态源。
- 编辑只对 role == user 开放、删除对 user 与 assistant 都开放,判据是同一份纯函数,
  气泡(隐藏入口)与 VM(再挡一次)共用。
- 失败一律如实:弹层还开着就留在弹层里(输入不丢、可原地重试),弹层已被关掉就挂到
  页面顶部那行横幅,绝不静默。

UI
- 长按气泡打开动作菜单;由于正文包在 SelectionContainer 里、长按正文同时也是选字手势
  (两者抢同一个长按,谁先消费由运行时决定),另给一枚看得见的「⋯」
  (content-desc 消息操作)作为同一条入口,避免只有知道的人才会用。
- 编辑弹层:原始 content 作初值 + 码点计数 + 取消/保存;删除必须二次确认。
- ChatScreen 只加 24 行接线(两个回调 + 两个弹层的渲染)。

测试 +49(766 → 815,全绿):帧解析 14、纯逻辑 15、Retrofit 层 7、失败文案与落点 13。
2026-10-04 21:26:52 +08:00
S
Description
昔涟在安卓设备上的载体。
Apache-2.0
19 MiB
0 Stars 1 Watchers 0 Forks
2026-05-29 22:15:56 +08:00