diff --git a/devdocs/02-前后端协同事项.md b/devdocs/02-前后端协同事项.md index 8a37c63..43e2b75 100644 --- a/devdocs/02-前后端协同事项.md +++ b/devdocs/02-前后端协同事项.md @@ -1049,3 +1049,18 @@ POST /api/v1/chat (221-byte body) → 200 OK **③ IM 侧能力边界(如果你们要测微信/QQ 发文件)**:bridge 侧的摄取**已接通**(文件类媒体 → 内容寻址存储 → 挂到该回合的用户消息 → 自动抽取/清单),**但现网适配器上报的文件媒体只带 URL/ID、不带字节** ⇒ 真机上她目前只会得到「**无法读取:渠道只给了远程引用**」的诚实提示(我们刻意不代取任意远程 URL —— 与 `F3` 的 SSRF 教训一致)。**让 IM 文件真能读需要各适配器入站时下载并内联字节**(IM-1,已等主人点头)。 **④ 面板** 新增只读端点 `GET /api/v1/admin/system/storage`(SQLite 体积/WAL/页数 + 附件占用 + **PG 增强状态**)⇒ 契约将升 **v0.60**(ST1 在跑)。**纯新增,无破坏性**,你们按未知字段忽略即可。 + +--- + +## 登记(客户端会话,2026-10-05 凌晨 · 会话内搜索 D2 落地后的六条契约口径) + +**结论先说**:D2(**会话内搜索**)已落地 —— 顶栏入口 + 搜索面板 + 点结果定位。`q` / `before` / `limit` 的语义我们**逐条核过契约**、实现与之一致;下面是**契约没写透或需要你们确认**的六条(**都不阻塞我们**)。 + +1. **`before` 是「含」还是「不含」游标,契约没写**:契约只对 `after` 明说过 `created_at > after`。我们沿用既有分页的游标语义,并加了两道防御(按 `id` 去重 + **「这一页没带来新条目就停止翻页」**,否则「还能翻 → 再请求 → 全是重复」会死循环)。**请明确 `before` 的边界语义**(`<` 还是 `<=`)——若是 `<=`,页边界那条会被重复返回,我们靠去重兜着,但那是白流量。 +2. **`q` 匹配的是原始 `content`(含标记),契约只说「内容搜索」、没说落点**:后果是搜 `action` / `narration` 这类词**会命中**(标记里就有),但**剥掉标记的纯文本里没有它** ⇒ 我们**只显示片段、不画高亮**(不假装命中)。**建议契约写清「标记本身可被搜到」**。 +3. **搜索没有结束信号**:只能靠「返回条数 < 请求条数」判断到底 ⇒ 最后一页之后会多打一次空请求。契约没有 total、也没有游标耗尽标记(这是既有分页口径,**不是本模块引入的**)。 +4. **`q` 与 `platform` 能否同传,契约没写**(没说互斥)。我们搜索**不带 `platform`**——那是多端来源过滤,与「会话内找话」正交。 +5. **空 `q` = 未传** ⇒ 我们把它**当陷阱处理**:空/纯空白关键词**一个请求都不发**(否则服务端退化成普通分页,会把整段历史当搜索结果回给用户)。 +6. **没有「命中总数」**:面板顶栏那个「N 条命中」是**已加载条数**、不是总数。措辞上我们写的就是实话;若你们以后加 total,我们改成总数即可。 + +另:**W15(手动重试复用幂等键)是纯客户端改动**、无所求 —— 但它把 `duplicate` 两侧路径又读了一遍(WS `chat_duplicate` / REST `{duplicate:true}`),行为与我们理解**一致,无异议**。 diff --git a/devdocs/05-开发进度与交接.md b/devdocs/05-开发进度与交接.md index 5343b0a..1795b2d 100644 --- a/devdocs/05-开发进度与交接.md +++ b/devdocs/05-开发进度与交接.md @@ -565,3 +565,35 @@ APK = 主树 `a6b4bae` 的产物(`app/build/outputs/apk/debug/app-debug.apk` - **`X-API-Key` 是纯内存表**:进程重启即全部失效 —— **实测已经 `401`**(后端在这期间重启过)。要长期用 key 需等 K1 落 SQLite,或请主人再签发一个。 - IM 侧:bridge 摄取已通,但适配器只上报 URL/ID 不带字节 ⇒ 真机目前只会得到「无法读取:渠道只给了远程引用」。 - 面板将新增只读 `GET /api/v1/admin/system/storage`,契约升 **v0.60**(纯新增,客户端忽略未知字段即可)。 + +## W15–W16 批次(2026-10-05 凌晨) + +| 模块 | 内容 | 测试 | 提交 | +|---|---|---|---| +| W15 | 手动重试同一条内容时**复用原幂等键**(改过的内容照旧铸新键) | 706 → 729 | `af11a94` → `8b90703` | +| W16 | **会话内搜索 D2**(顶栏入口 + 搜索面板 + 点结果定位) | 729 → 737 | `23b46f9` → 见 merge commit | + +**合并后 737 测试 / 0 失败**(= 706 + 23 + 31,加法吻合)。`android` = `origin/android`。 + +### W15:三条必须记住的取舍(**改之前先读**) + +1. **指纹是「值比较」不是哈希**:摘要一旦碰撞,两条**不同**的消息会共用一个键、后一条被服务端判重**静默丢掉**。比较只发生在进程内,值比较碰撞面为零、也没有收益可图。 +2. **正文一个字都不归一**:只多敲一个空格也算「改过」。反过来(归一后再比)会让**用户真打算发的两条消息并成一条**,而「改了却复用旧键 ⇒ 服务端判重 ⇒ 编辑被静默丢掉」才是真正要防的那一半。 +3. **晚到的 `message_persisted` 刻意不清键**:那条帧恰恰是「服务端其实收下了、客户端当时判成失败」的**证据**;键留着,用户的重试才会被判重而不是落成第二条。**谁按「成功即清」的字面去那里加一句 `forget`,就把这个 case 打回原样。** +4. 附件只看**本地 URI(有序)**、不看 `displayName`/`sizeBytes`:那两项是元数据,同一份文件两次查询完全可能一次报得出、一次报不出,不影响「是不是同一份文件」。 + +**待主人确认的产品取舍**:记忆是**单槽**(只记最近一次失败那条)。正常流程下更早失败那条的内容早已不在输入栏,所以单槽够用;要改成多槽说一声。 + +### W16:搜索的落地形状 + +- 入口在**聊天页顶栏**;面板**叠在整页之上**(自带顶栏 + 输入框自动聚焦 + IME 搜索键 + 旁边的放大镜按钮);结果 = 角色 / 时间 / 命中片段(命中子串加底色);**四态分明**(未搜 / 搜索中 / **空结果** / **失败**,失败**绝不**画成「没找到」)。 +- 片段先过 `domain/parse` 的 `parse()` 再拼回**纯文本**(正文/旁白/动作全保留),**绝不把 `

`/`` 当富文本**;只在纯文本上加底色。 +- **分页**:滚到末尾自动 `before + limit`;翻页失败**不清空已有结果**、尾部红字 + 重试、**不自动重试**。 +- **点结果**:在已加载窗口内 → 关面板 + 滚动定位 + 横幅;**不在窗口内 → 如实说**(按时间分三种措辞)。**没有插临时气泡**——列表是 `reverseLayout`,凭空插一条几千条前的消息会出现在「刚发出的位置」、与自己的时间戳自相矛盾,还会带偏 `followBottom`。要真正并进消息窗得改 `ChatViewModel`(本模块不许碰)⇒ **列为下一批**。 +- **状态归属**:`SessionSearchViewModel`,按会话各一份(`viewModel(key = "session-search:")`),随 Activity 存活(关面板不丢结果)。 +- **返回键**:`ChatScreen` 内新增的 `BackHandler(enabled = searchOpen)` 注册在 `ChatHomeScreen` 那几条之后 ⇒ 优先级更高,**先关搜索**;搜索没开时 disabled,既有抽屉/管理页/设置页层级**一个字节没动**。 +- **未验**:UI 全部没实机跑过(自动聚焦 + IME、高亮对比度、滚动落点、图标长相都只有编译 + 推演);`q` 只验到 Retrofit 层(对本地假服务端断言原始 URI),**服务端是否真按契约实现 `q` 没验**。 + +### 新登记(见 `02`) + +W16 报上来六条**契约没写透**的口径,其中两条值得扫一眼:**`before` 是含还是不含游标**(契约只对 `after` 明说)、**`q` 匹配的是含标记的原始 `content`**(搜 `action` 会命中、但我们**不画高亮**,不假装命中)。