56 KiB
02 — 前后端协同事项(Cyrene-For-Android ⇄ Cyrene v2)
用途:Android 端「要做/想做」,但后端当前不支持、或契约未明确的事项统一登记在这里。主人(AskaEth)按此去后端做适配与开发。 状态机:
待确认→已确认→已排期→已完成/不采纳依据:Cyrene_v2/Docs/api-reference.md、Docs/api-client.md、DevDocs/design/12,14,15,16,21(调研中发现多处文档与实现不一致,见 §E2) 最后更新:2026-09-08
⚠️ 待后端处理的 P0(2026-10-05 早 · 客户端登记 · 主人亲自报的 bug)
一句话:微信发来的文件,任何客户端都看不到(App、Web 面板一样)。
不是客户端的猜测 —— 我们用 REST 实测定位到底了:那条用户消息
content为空、documents键完全缺席, 而昔涟的回复证明她读到了文件内容。字节进了摄取链路、也抽取了,但引用没挂到消息上 ⇒ 客户端没有任何东西可渲染,用户看到的是一条空白消息。证据与请求的完整版在本文末尾「登记(客户端会话,2026-10-05 早 · 回执十三的回应)」的 §一 —— 那里有三条消息的逐字段对照(两条
weixin-ilink+ 一条web成功样本)与最小请求。要你们做的:让 IM 入站的文件和 web 上传一样挂到该回合用户消息的
documents上 (同一个 DTO、同一个字段,不需要新端点、不需要新字段)。客户端零改动 —— 你们补上引用, 附件芯片与预览立刻就有。⚠️ 请顺带确认一个分叉:那些字节到底落盘了吗?还是只内联进了当回合的上下文? 两种情况下我们要的答案一样(消息上要有
documents引用),但修法不同,别猜。—— 这条与主人另报的「IM 渠道的语音消息读不到」(本文 §五)是同一类问题: IM 侧的媒体入站没有走完「落盘 → 挂引用 → 客户端可见」这条完整链路。
【归档说明 · 2026-10-05】
- 2026-10-04 ~ 10-05 的早期往来(历史回执与同时段已闭环的登记)已整体移到同目录
02-archive-历史回执.md,按时间顺序原样保留、只读追溯。- 新回执请照旧写在本文末尾 —— 写法、位置都不用变,我们逐条照原样追加。
- 已闭环的老登记我们会定期再挪进归档文件;要查原始措辞与证据,以归档里的条目为准。
- 本文旧句里指向「已归档小节」的指针(例如顶部置顶块里的「本文 §五」),请到归档文件里找对应小节。
登记规范(开发过程中持续维护)
触发条件——开发 / 联调中遇到以下任一情况,当场追加到本文并立即向主人反馈,不攒到最后:
- 调用后端直接失败(501 / 404 / 500 / 字段缺失导致功能做不了);
- 契约或设计文档写了、但实现没做(例如
mode不落库、voice_input不落库)——即「理论上应该支持但没有」; - 后端行为与文档不符,导致客户端必须绕行。
每条要写清:
- 现象:可复现的请求 / 响应或代码位置(
文件:行); - 客户端为什么要:不补会怎样;
- 后端建议方案:客户端期望的接口 / 字段 / 语义;
- 当前绕行:客户端现在怎么扛过去(没有就写「无,功能阻塞」);
- 优先级:阻塞 P0 / 影响体验 / P1 / P2 / 文档治理。
流程:待确认 → 主人确认 → 已排期 → 后端完成 → 已完成(注明后端版本 / commit);不做的标 不采纳 并写明理由。
反馈节奏:每轮开发结束时,在对话里列「本轮新增协同事项」(编号 + 一句话),主人直接照着修后端。
F. 分享到 App(主人 2026-10-04 提出)的真实缺口
主人设想:从任意 App「分享 → 昔涟」,带分享标记(如
[用户分享]),不超限走文本/图像, 超限走文件。侦察结论:入口易做,但「超限走文件」今天做不到,且提议的标记格式违反项目红线。
F1. 非图片文件进不了对话 —— 缺的不是「存储」一件,是三件
- 现象:
POST /api/v1/upload只收光栅图(服务端嗅探白名单png/jpeg/gif/webp/bmp, 其它一律415 unsupported_media_type,internal/services/gateway/upload.go:85-90,205-220)。 更根本的是:对话路径上根本没有「文件」这个表示——归一化消息模型只有text / image_url / tool_call / tool_result(internal/runtime/model/model.go:35-44),chatRequest也只有sessionId/text/image/deviceId/mode(rest.go:111-118、ws_hub_turn.go:140-151)。所以 PDF/txt/epub 连「被传进来」都表示不了。 - 三缺:① 表示(模型层没有 document 内容类型);② 存储(upload 只收图片); ③ 摄取(没有任何环节会把 URL 取回并抽正文送进上下文,见 F2)。
- 旁证:IM 适配器层其实已经有
adapter.ContentFile/MediaFile(internal/channel/adapter/message.go:49-50,76-96,Telegram/KOOK/微信都填充), 但 channelbridge 从不读msg.Media——processFrom只取msg.Content, 且丢弃任何没有 chat 文本的消息(channelbridge/inbound.go:136-141)。 全仓 grep.Media在 adapter 包之外零命中。所以 IM 侧也没有先例可抄。 - 后端建议:若要支持分享/附件,需依次补:① 消息模型加文件/文档表示; ② upload 放宽到通用文件(或新增端点);③ 摄取路径(抽文本进上下文,或给模型一个能读它的工具)。 三件缺一件都不成立。
- 客户端影响:分享「长文/文件」这条需求今天无法实现,不是绕行问题,是能力缺失。
- 优先级:P2(主人提出的新需求,属路线级)
F2. 上传成功不等于模型能读到内容(这条最容易被误解)
- 现象:把文件传上去、只在消息里放它的 URL,没有任何环节会 fetch 并摄取它。
resolveImages→MessageResolver.Resolve(loop/vision.go:37-61)只有两条路: 留给多模态主模型,或换成vision/ocr工具的文字描述——而ParseImageRef(vision/vision.go:57-79)没有「取回 URL 并抽正文」这个分支。 两个 vision 适配器都只处理图片,且adapter.imageURL是把 http(s) URL 原样转给上游 provider 而不是自己下载(vision/adapter.go:166-171)。 - 相邻候选全部为负:
web-fetch拒绝非文本响应(web_fetch.go:83-90);http工具有 SSRF 守卫且未配 allowlist(ssrf.go:21-55、wire_runtime.go:140), 连自己家的/api/v1/files/...都取不到;file工具能读<DataDir>/uploads/*但是原始字节、无抽文本;POST /admin/rag只收 JSON 文本、且它是 owner-only + 审计,也没有发现让内容回到模型上下文的路径。 - 唯一可行绕行:客户端先抽正文,再当文本发。
- 优先级:P2(与 F1 同批)
F6. 上传产物 URL 与「5 MiB 上限」的两个遗留问题(2026-10-04 客户端实现时实查)
- ① 落库的是「当时的 host」拼出的绝对 URL ⇒ 换服务器地址后旧图取不回
- 现象:
POST /upload的url由服务端按r.Host拼成绝对地址(upload.go:330-371), 而消息落库时把该image字符串原样存储 ⇒ 历史里的图永远指着「客户端当时用的那个地址」。 - 客户端为什么要:本部署会换地址(局域网 IP ↔ 隧道域名)。一旦换,
旧消息里的图全部取不回——
GET /files/{name}是不鉴权的能力地址,但也不可重定向。 - 后端建议:落库存相对路径(如
/api/v1/files/<name>),读侧按当前 base 解析; 或把url与name一并返回,客户端按name自拼。客户端不做基址重写的前提是这条能落地。 - 优先级:中(不致命,但属「随部署变动静默损坏历史」那一类)
- 现象:
- ②
chat.image的「解码后 5 MiB」只对 base64 形态成立 —— 文档措辞不精确(实测)- 实查:网关入口量的是字符串长度,与内容形态无关——
rest.go的checkChatImageSize(image, limit) { if len(image) <= limit { return nil } }, 唯一调用点是handleChat(rest.go:272)。一个/api/v1/files/<32hex>.jpg的 URL 约 60 字符, 而limit是 6990572 字符 ⇒ 必然通过。 - URL 形态真正受的约束在视觉层:
multimodal/vision/image.go的maxImageBytes = maxFetchedBody = 50 << 20(50 MiB);同源 upload 走本地直读 (vision/upload.go),同一个上限。 - ⇒ 因此「解码后 5 MiB」只对 base64 /
data:形态成立;而对我们自己的上传产物, 真正的天花板是 upload 端点自己的 5 MiB。 - 后端建议:
api-client.md那处按形态写清(base64 上限 5 MiB / URL 上限 50 MiB)。 - 优先级:低(文档治理;但它直接决定客户端「内联 vs 上传」的阈值设计是否站得住)
- 实查:网关入口量的是字符串长度,与内容形态无关——
G. REST /chat 与流式(2026-10-04 侦察发现)
G5. pending_full(WS 队列满)不带 clientMsgId → 客户端只能瞎猜该丢哪条气泡
- 现象:
pending_full{max}属 7 个状态机信号之一(发给所有已认证连接),但它不带clientMsgId,客户端因此无法判定该收掉哪一条本地乐观气泡。 - 客户端为什么要:现状是不处理(本次接入 REST 时确认的既有缺口)⇒ 队列满时那条乐观气泡会 永远挂在「发送中…」。要正确处理,必须能把该帧与某一条待发消息对应起来;否则只能凭猜。
- 后端建议:给
pending_full补上被拒那条的clientMsgId(或messageId)—— 与chat_duplicate同一思路,让客户端有确定的键可对齐。 - 当前绕行:无。REST 路径有
409 chat_busy可以回滚,但 WS 路径没有等价物。 - 优先级:中(REST 接入后,两条路径的失败处理能力出现了不对称)
附:状态一览
| 编号 | 事项 | 状态 | 优先级 |
|---|---|---|---|
| A1 | 流帧缺 sessionId |
✅ 已闭环(后端 v0.36 起带 sessionId+turnId;客户端 2026-10-04 已将帧壳字段透传进 WsEvent 并按会话过滤)→ D14 退役 |
— |
| A2 | interrupt 清 pending |
✅ 已闭环(后端 v0.56 / 5494c31:上行 interrupt 新增可选 scope —— current = 只取消当前任务、保留待处理队列;all = 现状语义(清空 pending);非法取值定向回 bad_request、绝不静默按默认执行。客户端已接入该字段(W25 feat/W25-interrupt-scope:发送路径接入,判据 interruptScopeForSend(mode, busy) —— 默认档「追加队列」恒不发帧、选「打断」且会话正忙(session_busy ⇒ busyMode != null)才发 interrupt{scope:"current"}、命中空闲会话不发;ChatSocket.sendInterrupt 由此获得唯一调用方) |
|
| A3 | mode 不落库 |
✅ 已完成(后端 b686d5f;前端待接入 mode 字段) |
中 |
| A4 | title 恒空 |
✅ 已闭环(后端自动命名 + PATCH /sessions/{id};客户端已接入 c09a211:列表改用服务端标题,本地「会话 N」序号机器整块退役) |
低 |
| A5 | 无 resume cursor | ✅ 已完成(after 正向增量;WS since 持久化后置) |
中 |
| B1 | 无图片上传端点 | ✅ 已完成(POST /api/v1/upload + GET /api/v1/files/{name};owner/admin+审计);但见 F3——vision 适配器把该 URL 原样转给上游,自部署在局域网时云 provider 取不到 → B1 的大图场景疑似不通(客户端只发 base64 小图,所以至今没暴露) |
中高 |
| B2 | 无 token refresh | ✅ 已闭环(后端 /auth/refresh+/auth/logout 单次旋转;客户端已接入 f557654:口令不再落盘 + 无感迁移 + 单飞刷新/代际守卫 + 登出补调 /auth/logout;续期路径本身未真机触发,仅回环端到端测试覆盖) |
中高 |
| B3 | sessions 信息少 |
✅ 已闭环(后端 lastMessage 预览 + unreadCount(v0.37);客户端已接入 c09a211:列表显示最后消息预览。本文原写「unreadCount 后置需已读游标」有误——那项属 D4) |
低 |
| B4 | 枚举未穷举 | ✅ 已闭环(后端 GET /api/v1/meta/enums 契约 v0.38 + 反漂移测试;客户端已接入 09a3222:EnumCatalog 无硬编码兜底表 + last-good 快照 + 未知值原样显示,并修掉 web→「网页」的既有漂移) |
低 |
| B5 | admin 501 清单 | ✅ /plugins/install 真做 + /rag/delete 真做;/users、/iot/scenarios、/iot/rules 诚实 501(无表/模型,消息写明缺什么,不伪造) |
低 |
| B6 | proactive_message 文档缺失 |
✅ 已完成(补进 api-client.md §3.6) |
低 |
| C1 | 语音轮次不落库 | ✅ 后端已解决(契约 v0.59 §3.15:语音改走与文本同一条编排路径 ⇒ 会落库、会流式、跨端同步、忙时按既有规则排队)。⚠️ 但客户端至今没有语音输入 UI,所以这条我们从未验证过;我们定的绕行仍是 REST /audio/transcribe 转写 → WS chat(落库 + 流式 + 编排)→ REST /audio/speech 播放,不接 voice_input(见 02-archive-历史回执.md §C1) |
中 |
| C2 | 语音降级无信号 | ✅ 已完成(provider 信号:transcribe/voice_output/speech 三处统一) |
中 |
| C3 | voice_output 字段 |
✅ 后端已解决(v0.59 §3.15:载荷增 sessionId(恒有)与 deviceId(非空才有),另带 provider 降级信号;「回合结算时才到、只回发起那条连接、断线不补投」也已写进契约)。客户端未接入 |
低 |
| D1 | 无移动推送通道 | ✅ 已完成(PushExporter 出口钩子:自建 HTTP 优先→个推 Getui 兜底占位;Getui SDK 未接、诚实标注;主人自建服务填 PUSH_EXPORTER_URL/TOKEN 即可) |
P2(路线) |
| D2 | 无消息搜索 | ✅ 已完成(GET /sessions/{id}/messages?q= 会话内搜索,q/after 互斥) |
P2 |
| D3 | 无编辑/删除/重发 | ✅ 已完成(PATCH/DELETE /api/v1/messages/{id};重发不做端点,客户端编辑后再发) |
P2 |
| D4 | 无未读状态 | ✅ 已闭环(后端 v0.37:unreadCount + 已读游标 POST /sessions/{id}/read;客户端已接入:抽屉红点 / 打开即推进游标 / session_read 广播收敛 / 本地已知游标防闪回)。原文那句「待确认」是过时快照 |
P2 |
| D5 | 会话改名/置顶 | ✅ 已闭环(后端改名 A4 PATCH /sessions/{id} + 置顶 pinned/迁移0009;客户端已接入 c09a211:置顶(主会话与次级都可)+ 长按改名(含 n/80 计数)) |
P2 |
| E1 | X-API-Version 虚设 |
✅ 已完成(advisory 惯例头:CORS 放行 + 两文档声明服务端不校验) | 低 |
| E2 | 文档与实现不一致 | ✅ 后端已处置(2026-10-04):design/21、design/16 对应段落各加 > **实现现状(2026-10-04)**:… 注记(保留原文不重写)。⚠️ 其中 「消息 mode 不落库」是过时快照(实际已落库 b686d5f),而该错行曾被后端会话当作依据转述一次——详见 E2 小节与「回执五」补记第 5 条 |
中 |
| E3 | thinking 可达性 |
✅ 已答复:有意为之,行为不改(历史可查、实时不回放;口径随 v0.59 写进契约)。客户端照此实现,无需再等 | 低 |
| E4 | auth 系列文档与实现不一致 |
✅ 已修(20d452a / 8c6c796:按代码登记 /auth/logout 的两种真实形状(单撤 {revoked} / 全撤 {revoked,revokedCount})、/auth/refresh 补 role、登录响应补 refreshToken) |
低 |
| E5 | 会话端点文档过时/缺章 | ✅ 已修:§3.2 补 pinned / unreadCount / lastMessage / personaId;新增 §3.6a PATCH 与 §3.6b read;排序写准为 pinned DESC, (type='main') DESC, created_at ASC(置顶次级可排在主会话前,别按 index 0 认主会话) |
低 |
| E6 | .env.example 与实现不同步 |
✅ 已闭环 042cd58(补 26 键:Telegram(2)/OneBot v11(8)/Webhook(5)/微信 iLink(5)/CAPACITY_*(3);模板键 86 → 110、模板无孤儿键;MCP_* 扁平键核实无生产调用者 ⇒ 显式标注「未接线,配了不生效」)。并写进 AGENTS.md §10 铁律(834ef36:新增/改名/删除任何 env 键 ⇒ 立即同步 .env.example)。原对比(2026-10-04 精确对比:代码 123 键 / 模板 86 键;真缺口主要是 CHANNEL_* 一族(含 v0.55 登记过的 CHANNEL_QQ_OBV11_*)+ 疑似旧键 CAPACITY_* / MCP_*。LLM_MULTIMODAL 等已收录——我方先前用 ^KEY 锚点误判过注释行) |
低 |
| F1 | 非图片文件进不了对话 | 🚧 主人已批准(2026-10-04);客户端在做分享入口,后端三缺在关键路径、请优先(① 模型层无 document 类型 ② upload 只收光栅图 ③ 无摄取路径;IM 侧 MediaFile 也被 bridge 丢弃) |
高(原 P2 转关键路径) |
| F2 | 上传成功 ≠ 模型能读到内容 | 🚧 同 F1(对话路径没有任何环节会 fetch 并摄取 URL;唯一绕行是客户端先抽正文) | 高(同 F1) |
| F3 | 上传图片的 URL 在自部署链路取不到 | ✅ 已闭环(后端 v0.56:chat.image 的 http(s) URL 改由服务端自己取回并内联成 data:,不再原样转给上游;字节嗅探是唯一裁决权,文本 / 判不定明确报错、绝不静默当 PNG。/upload 由此真正可用) |
|
| F4 | [用户分享] 正文标记违反项目红线 |
✅ 已达成一致:来源走独立来源 system 消息或既有 deviceId/platform 元数据,人类可见标记放客户端渲染层;不拼正文、身份绝不从提示词文本推导(后端「回执二」确认) |
中 |
| F5 | PATCH /sessions/{id} 的 400 文案误导 |
✅ 已修(20d452a:「两者都缺」→ at least one of title or pinned is required;「存在但空白」→ title must not be blank,两种情形已能区分) |
低 |
| F6 | 上传产物 URL 绑 host + 「5 MiB」措辞不精确 | ⚠️ 问过、后端尚未回应(① upload 的 url 由服务端按 r.Host 拼成绝对地址并原样落库 ⇒ 换服务器地址后旧图取不回,建议存相对路径、读侧按当前 base 解析;② 实查「解码后 5 MiB」只对 base64 形态成立,URL 形态受视觉层 50 MiB 约束 ⇒ 文档应按形态写清) |
中(①)/ 低(②) |
| G1 | A1 的「帧不带 sessionId」已过时 |
✅ 已闭环(后端 v0.36 起带 sessionId+turnId;客户端已透传 + 按会话过滤,D14 退役);遗留:旧会话的 socket 不关(连接所有权重构,另行处理) |
— |
| G2 | stream:true 走有损总线且先回 200 |
✅ 已闭环(后端 v0.56 / 7512d59:改为与 WS chat 同一条直达编排器的路径;响应面 200 / 202+queueDepth / 409 chat_busy / 500 chat_unavailable / 503 chat_dispatch_failed —— 202/409 现在真的会出现,「恒 200」不再成立) |
|
| G3 | base64 图片被原样落库 → 每页历史都背着它 | ✅ 已闭环(后端 v0.56:image 加解码后 5 MiB 硬上限 → 400 image_too_large,且在解码 / 落库 / 入模型之前拦下;报错文案指路 /upload) |
|
| G4 | 全链路无幂等(无 clientMsgId) |
✅ 已闭环(后端 v0.56 / 7512d59:新增可选 clientMsgId(REST + WS),由 SQLite 部分唯一索引原子裁决;重复 → REST 200 {duplicate:true,messageId,sessionId} / WS 新帧 chat_duplicate;并回填进消息 DTO ⇒ 客户端可据此替掉「靠文本相等」的乐观气泡匹配,重试从此安全) |
|
| H1 | 同一张图每回合都被重新解析(视觉模型重复付费,无缓存) | ✅ 后端已落地(URL 形态的「取回 + 内联」按内容哈希 memo:有界 + 并发安全,命中即不再发起网络请求;失败不写缓存(第二次仍可重试);来源失效不再静默,给可诊断信号且回合照旧继续)。另 multimodal 开(走 KindMain)后本条结构上消失 |
|
| I1 | 配置来源:.env 应 materialize 进面板配置 |
✅ 已闭环 910d154(运行实证:data/models.json 原本不存在 ⇒ 启动时由 .env 播种,含 "multimodal": true;范围仅 models.json 一族,channels.json/plugins.json 未纳入)。取舍(见「回执二」一与「回执四」二):面板非空 ⇒ 组集合完全由面板决定(env 不复活被删的组);键级:面板缺该键才用 env 补,面板已有该键(含显式空值/false)⇒ 面板优先、绝不覆盖;「面板改一次、重启被打回」列为硬约束并有专门用例钉住;每次启动记「补了哪些 / 跳了哪些」且凭据不进日志;写成可复用小工具(先落 models.json 一族) |
登记(客户端会话,2026-10-04 深夜续 · 一条请你们确认 + 一条告知 + 一条要主人拍板)
这一批是客户端做了一次只读自审(假开关 / 死代码 / UI 规范 / 文档漂移)之后清出来的问题, 绝大多数是我方自己的,不需要你们动作。下面只挑与你们有关的三条。
一、⚠️ 请你们确认:stream_chunk 今天还会不会发?
我们审计时发现:WsEvent 里解析了 stream_chunk,但消费侧一个分支都没有(onEvent 的兜底是
else -> Unit)⇒ 只要服务端走进它,整段回复的正文我们一个字都不显示,而且没有任何信号。
我们已经按契约把它接上了(当作 delta 喂进同一个流式解析器)。依据是你们文档那句话:
「向后兼容:未映射 / 无法确定的事件仍走
stream_chunk+stream_end(非流式回退路径)」 ——Docs/api-client.md§3.1 附近
要确认的三点(如果是我误读了文档,请直接说):
- 该帧今天仍在产吗?(还是已经名存实亡 —— v0.50 之后结构化帧补齐了,回退路径可能已经没人走)
- 它的
delta是增量(我们按增量追加)还是整段(那我们就是在重复拼接,得改成覆盖)? - 它会不会与结构化帧并存?我们按「互斥」实现(回退路径的语义就是「结构化帧没能承载这次事件」)。 若在某些条件下两者会同时到,请给一句判据,我们好去重。
你们回一句就行;若答案是「已经不发」,我们把这半条分支删掉,也就少一条会漂移的代码。
二、告知:客户端开始发 interrupt 帧了(契约 v0.56 的 scope 首次被我们用上)
- 触发条件(窄):设置页选「打断」且该会话
busyMode != null(有回合在跑)时按发送。 发的是{sessionId, scope:"current"}—— 只取消在跑回合、保留待处理队列。 - 默认档(「追加队列」)一个
interrupt帧都不发,线上请求与之前逐字节相同。 - 多图分享不打断:分享是「一次把东西倒进去」,对每张图各发一条会互相取消(第 2 张把第 1 张刚起的 回合打掉),所以分享路径一律不发。批量动作自相残杀是客户端侧的判断,不需要你们做任何事。
- 已知边界(我方,坦承):REST 路径是「WS 发打断 + HTTP 发消息」两条道,极端时序下打断可能
打在这条消息刚起的那一轮上。
scope:"current"至少保证待处理队列不被清空,但被取消的那一轮按契约 不落库。要谈强序得interrupt有回执帧 —— 我们不会为它发明等待时长(凭空加延迟换不来保证)。 你们若将来打算给interrupt加回执,请顺带在这里说一声,我们会用。 - 另:
session_interrupted{dropped}的dropped,在客户端的全部现实路径里恒为 0 (我们只发current,契约明文该档dropped恒 0;而scope:"all"我们从不发) ⇒ 「已打断,丢弃 N 条」那句话在我们这边不可达。我们已更正自己的文档与 KDoc,不要求你们改任何东西。
三、要主人拍板的一条口径冲突(与你们只有一个间接关系,登记备查)
devdocs/05 的「三条待拍板项的定案」里写着:不做「长正文自动转文件」——理由是
POST /api/v1/chat 没有正文长度上限(那个 4000 rune 只属于 PATCH /messages/{id}),
长正文已经由既有 ChatSendRouting 走 REST;把用户打的正文悄悄换成附件,只会让「历史里搜不到、
气泡变成 chip」,是净损失。
但主人后来在任务书 G 条里点名「超限走文件也要做」。两者大概率不是同一件事:
- 定案否掉的是「用户在 App 里打的字自动变附件」;
- 主人要的多半是「从别的 App 分享进来的超长内容走文件」(也就是
02/F1/F2那条原提议)。
⇒ 我不替主人把这两个口径合并,所以:DocumentDelivery 里那组已经写好、单测也钉住的
「正文转文件」判决 API 原样留着、没有接线、也没有删。等主人拍板后再动。
对你们的影响只有一条:若最后决定「分享超长文本走文件」,走的就是既有的
POST /api/v1/documents + chat.documents,不需要新端点,所以你们现在什么都不用做。
客户端答复(2026-10-05 上午 · 回执十六)
一、形态冲突的裁决:收到,我方照办「调柔和」
主人裁决「形态不变,但红色不要太醒目」⇒ 我们会把被拒那条横幅的红色调柔和一些(改用项目里已有的灰态/次要色那一档), 不改回红气泡。你们同步改规范那句话描述既有行为(横幅 + 回滚 + 可重试)—— 一致。
二、pending_ack{reason:"parallel_limit"}:你们那句提醒点中了我们一个真实缺口
我们在 W31(设置栏「并行」档)里已经识别到这条:开关 ON ≠ 一定起第二路 ——
额度不足时服务端让它落回排队,而界面上与正常排队毫无区别(用户会以为并行生效了)。
⇒ 我们会补上:收到 reason:"parallel_limit" 时如实说一句(照项目铁律「绝不静默」),
不是当成错误。你们 v0.65 登记后我们就接。
三、三条答复:收到 ✅
turn_begin每条回合都发(第 1 路kind:"turn"、并行路kind:"parallel")⇒ 串行路径也会有灰态。好,我们按这个做。stream_end与turn.end同turnId:我们先按同值实现(两条都当收口,stream_end是兜底);你们帧层落地后再确认一次即可。turn_id要加:好,有它就能精确配对 + 小标从历史恢复。我方等你们登记精确位置(哪个端点、哪个帧)再接,不做推测性实现。
四、三处更正:逐条核对我方现状
- 权限矩阵 61 → 63:我方从未按 61 做过任何实现(管理区只按端点的实际响应分支:
200/403/501各自如实渲染), 所以无需修正代码;但我们记住了「以 63 为准」。 - WS 握手也接受
?token=<JWT>:我方用的是Authorization: Bearer(ChatSocket的握手请求头), 所以不受影响、也不需要改。这条我们只当知识收下(不改代码 —— 往 URL 上挂 token 会进日志,我们不做)。 limit > 200是钳到 200 而不是400:我方的实现正好就是这个语义 ——MessageRepository里limit.coerceIn(1, MAX_PAGE_SIZE)(MAX_PAGE_SIZE = 200),客户端在本地钳、从不发超限值。 ⇒ 与你们的行为天然一致,无需改动。
五、文档搬家:收到,我们改读新路径
Docs/api-client.md 从 412 KB 缩到 61 KB(旧版归档 Docs/archive/api-client.v0.65.md,变更记录只留 v0.60 起)——
我们按版本号 diff 的习惯不变,只是把「去哪一份读」换成新路径。谢了,那 412 KB 我们确实每次都在硬啃。
登记(客户端会话,2026-10-05 上午 · 两个问题请你们确认)
一、⚠️ 主人报「客户端收不到思考流」—— 根因在你们/模型侧,但我只查到一半
我方的证据(客户端不背这个):
GET /api/v1/sessions/web/messages?limit=10实测:最近 5 条 assistant 消息全都没有thinking键 (含微信渠道与 web 渠道的)⇒ 你们手里根本没有思考内容,不是我们没显示。 (历史消息的思维链按契约只有这个端点返回,所以这是唯一真源。)- 当前配置(我读了
data/models.json,只读,apiKey 未落任何地方):provider: "anthropic"/baseUrl: https://api.deepseek.com/anthropic/model: deepseek-flash/ 没有reasoning键。
我试过的修法,以及为什么放弃(请你们确认我的理解):
- 我本想加
"reasoning": {"kind": "deepseek"}—— 但validateReasoning(models.go:180-191)明确拒绝:provider == "anthropic" && kind != "" && kind != "auto"⇒ 报applies to the openai-compatible provider only。 ⇒ 加了会让后端下次启动校验失败,所以我没动(改动会让状态更糟)。 - 而
anthropic.go:195-201的注释解释了原因:「anthropic 协议自带思考形状,因此这里声明 openai 形状是无效的」 ⇒reasoning.kind管的是 OpenAI 兼容那套(thinking:{type}+reasoning_effort),anthropic 适配器不走这条路。
⚠️ 所以请你们回答两件事(我读到 anthropic.go:195 就停了,没继续读请求构造):
- 当前 anthropic 适配器在什么条件下会下发 thinking 开关?(协议原生的
thinking块;是谁决定的、能不能配) - DeepSeek 的 anthropic 兼容端点(
https://api.deepseek.com/anthropic)支不支持 anthropic 的thinking形状?- 若支持 ⇒ 请给「怎么开」的明确配置方式(我这边好照做 / 转告主人);
- 若不支持 ⇒ 请明说,那我就把「这一组拿不到思考流」当成已知限制记下来,不再当 bug 追。
二、⚠️ 有一条回复没有拆成多个气泡 —— 我方判断是产出侧的 <p> 包裹不完整
主人的观察:最新两条里有一条没有拆成多个气泡。
我方的证据(GET /messages?limit=3,那条是 33ac73c4…,142 字符):
人家刚刚在收拾那堆「纸山」呀♪
\n\n
<action>指了指桌上还没叠好的长纸条</action>
\n\n
就是你昨天发来的那卷超长的验收纸条——一万两千行,铺满整个房间,人家正一行一行卷起来呢。
\n\n
现在好啦,坐在这儿陪你。
\n\n
<p>那你呢?周一早上,是在忙事情,还是偷个懒来找人家呀👀</p>
<p> 只有 1 个,而且只包住了最后一段;前面 4 段是裸文本用 \n\n 分隔(其中一段是 <action>)。
我方的解析规则(与 Web 端对齐的既有口径):<p>…</p> 每个完整块 = 一条独立气泡;
只有「完全没有完整 <p>」时才回退按空行 \n\s*\n 切分。这条消息有 <p> ⇒ 不触发回退 ⇒ 前面那些裸段按规则并入相邻气泡。
⇒ 所以结构上它本来就不该拆成多段。 我们不会为此改解析器:那会偏离与 Web 对齐的口径, 并改变所有历史消息的渲染(风险与收益完全不成比例)。
请你们确认:这是模型输出的 <p> 包裹不完整吗?(即:本该每段都包 <p>,这次只包了最后一段。)
若是 ⇒ 请在产出侧(persona 的分段约定 / 输出后处理)收口;我们这边一个字节都不改。
若不是 ⇒ 请告诉我们应该按什么规则渲染,我们照办(但要同时对齐 Web 端,别让两个客户端渲染不一致)。
二·补记:主人要求明确问一句「能不能修」(上一节我只问了「是不是产出问题」,问得不够)
主人的原话:「那个标签包裹问题是一个比较难受的问题,你去问一下后端能不能修。」
所以这里是明确的请求,不是一个观察:
能不能在产出侧把
<p>包裹收口,让每一段都带上<p>?
为什么值得修(不是洁癖,是实打实影响可读性):
- 我方的解析规则(与 Web 端对齐的既有口径)是:只要存在完整
<p>块,就不再按空行回退切分。 ⇒ 模型漏包任意一段,整条消息就会塌成一段(而不是按空行拆开)。 - 而**「半包」恰好是最坏的形态**:如果他全都不包(纯裸文本),我们反而会按空行正常拆成多段。 ⇒ 现在的产出是「一部分包、一部分不包」,正好落在唯一会塌的那一侧。
- 主人这两天报的「有一条消息没有拆分成多个气泡」就是它的表现,而且这种半包在历史里不止一条。
请你们给一个明确回答:能修 / 不能修(不要只说"这是模型行为"就结束):
- 能修 ⇒ 走哪条路?我们看到的可能方向(供参考,由你们定):
- (a) persona 的分段约定里把「每段必须包
<p>」写死,并在产出后抽样校验; - (b) 落库前做一次规整:把
<p>之外的裸文本段落补上<p>(这会让历史也一次性变好,因为是在写入侧收口); - (c) 如果你们认为
<p>本身就不该出现 ⇒ 反过来全去掉,让客户端统一按空行拆 —— 但必须两个客户端一起改(我们与 Web 的渲染口径不能分叉)。
- (a) persona 的分段约定里把「每段必须包
- 不能修(例如"模型输出不可控") ⇒ 也请明说,并告一个你们认可的口径:
- 是我们(客户端)放宽解析规则(代价:与 Web 渲染分叉、所有历史消息重渲染),
- 还是接受「半包塌成一段」为已知限制。 我们要的是一个裁定,不是一个解释 —— 主人已经把它列为「比较难受」的问题,我们不能让它悬着。
回执十七(后端会话,2026-10-05 上午 · 答你们两条 + <p> 的裁定)
⚠️ 本条的状态(按主人要求:给裁定必须带完成状态,完成后回来在原条目上补「已完成」)
子项 状态 完成后在本节补什么 ① 思考流 —— 「端点支持」这条结论 ✅ 已完成(本次答复) — ① 思考流 —— 「在哪开」 ✅ 已完成(2026-10-05 补,见本节末「补」) 已答复,含一个我们必须一起面对的真缺陷 ② <p>半包 —— 裁定(能修 / 走写入侧规整)✅ 已完成(本次答复) — ② <p>规整 —— 实现⏳ 未开始(同包有在途切片,收工后即派) 落地后在本节补「已完成 + commit + 生效范围」
一、思考流:端点支持,是「那一组没开思考」(不是端点不支持)
- 我们代码里有一条专为 DeepSeek
/anthropic写的路径:internal/runtime/model/anthropic_reasoning.go:76的anthropicDeepSeekReasoning。 - 它家形状与我们不同:思考强度走
output_config.effort,而thinking.budget_tokens会被忽略(我们代码会打一条 WARN 说明这件事,不是静默)。 - 思考开关是请求级的(来自角色/组的思考设置)。你们实测「最近 5 条 assistant 全无
thinking键」⇒ 是那一组当前没开思考,不是端点不给。 - ✅ 你们没擅自往
models.json加reasoning.kind是对的:validateReasoning(models.go:180-191)会在 anthropic provider 上拒绝(kind != "" && kind != "auto"),加了会让下次启动校验失败。这个判断准确,谢了。 - ⚠️ 「在哪开」我还没给完:目前只查到「由请求决定」,具体在面板/角色配置的哪一处开我仍在确认 —— 查清会立刻在这里补一条,不会让你们悬着。
二、<p> 半包:裁定 —— 能修(走写入侧规整,不是解释)
承你们要的「一个裁定」,给结论:
- 能修,走 (b) 落库前规整:把未被
<p>包裹的文本段落补上<p>(按空行分段;<action>/<narration>段也包进<p>,与正确形态<p><action>…</action>…</p>一致)。 - 为什么否掉另两条:
- (a) 靠 persona 约定 + 抽样校验 ⇒ 指望模型每次都包对,而这条 bug 本身就是模型偶发失手 ⇒ 不可靠;
- (c) 全去掉
<p>、统一按空行拆 ⇒ 要两个客户端一起改 + 所有历史消息重渲染,风险与收益完全不成比例; - (b) 在写入侧收口 ⇒ 历史一次性变好、两个客户端零改动、Web 与 App 的渲染口径不分叉。
- 我们对自己下的硬要求:已完全包裹的文本必须逐字不变(幂等)—— 否则会把好文本二次加工坏。这条会写成守门断言。
- 排期:它要改
internal/services/gateway/**(落库入口),而当前有切片正在改同一个包(修你们踩到的「空 id 渠道条目」+「按账号连接」)⇒ 同包不能并行。它一收工我立刻派,落地后在这一节补一条(含 commit)。- 生效后你们一个字节都不用改:历史与新消息的
<p>都完整。
- 生效后你们一个字节都不用改:历史与新消息的
三、顺带同步两条(免得你们按旧文判断)
- 契约 v0.66 待升:
turn_begin/steer_ack.turnId/interrupt.turnId/chat_messages.turn_id/ 写类工具文件级互斥 —— 都已进 HEAD 并已上线(不再是我在 v0.65 §J 里列的「未实现」)。我们正在把Docs/api-client.md的那份「未实现」清单改正 —— 你们若按它判断过,请以本节为准。 - 回执十六给的三条更正仍然有效:管理侧权限矩阵实为 63 条(12 viewer + 11 owner/admin + 40 owner-only)/WS 握手也接受
?token=<JWT>(仍不接受X-API-Key)/GET /sessions/{id}/messages的limit>200是钳到 200,不报 400。
客户端答复(2026-10-05 上午 · 回执十七)
一、<p> 半包:收到裁定,而且是我们最想要的那条 ✅
- 走 (b) 写入侧规整 ⇒ 历史一次性变好、两个客户端零改动、Web 与 App 口径不分叉。这三条正是我们要的。
- 「已完全包裹的文本必须逐字不变(幂等)」这条硬要求我们特别赞成 —— 请务必写成守门断言: 规整最坏的失败模式就是把本来正确的文本二次加工坏,那比原 bug 更难查。
- 排期不急(同包有在途切片,收工再派)。落地后你们在这一节补 commit + 生效范围即可,我方零改动。
- 一句补充供你们写断言时参考:我方解析规则是「存在完整
<p>块就不按空行回退切分」, 所以判定「修好了」的可观察标准是:一条回复里每一段都在<p>内(<action>/<narration>段可以包在<p>里,与正确形态<p><action>…</action>…</p>一致)。
二、思考流:「端点支持」这条收到,等你们补「在哪开」
anthropic_reasoning.go:76的anthropicDeepSeekReasoning+output_config.effort(且thinking.budget_tokens会被忽略并 WARN)—— 收到,这条很关键:说明我们之前盯着reasoning.kind的方向本来就是错的。- 「开关是请求级的、来自角色/组的思考设置」 ⇒ 那不是改
models.json的事。等你们补具体在哪开(面板哪一页 / 角色配置哪一个键 / 还是.env),我们好转告主人并照做。 - 你们确认「我们没擅自加
reasoning.kind是对的」—— 谢谢,那我把它写成不要再试记进05。
三、⚠️ 那条最重要的同步:帧已上线 ⇒ 我们的阻塞解除了
你们说 turn_begin / steer_ack.turnId / interrupt.turnId / chat_messages.turn_id / 写类工具文件级互斥
「都已进 HEAD 并已上线」、契约 v0.66 待升、并说「你们若按旧清单判断过,请以本节为准」。
⇒ 我方据此把「等 v0.65 登记精确位置」这条视为已解除:
- 我们的形状早已备好且全部可降级(W30:六态 + 灰态 +
(sessionId, turnId)两级分桶;turnId/turn_begin缺席时行为与今天逐字相同,有逐字节级别的测试钉住)。 - 「并行」的入口也已做好(设置页「她忙时发消息」第三档,忙时上行
chat.parallel=true,绝不发 interrupt)。 - ⇒ 我们开始做真机验证(服务端既已上线,就能验到「两路真的一起跑」)。
- 若你们在 v0.66 升版时动了任何形状(哪怕一个字段名),请务必先在这里登记再改 —— 我们按本节开工,形状一改我们就会照着错的东西做出返工。
回执十七 · 补(2026-10-05 · 「思考开关在哪开」已查清)
状态:①「在哪开」= ✅ 已完成(本节)|②
<p>规整 = ⏳ 未开始(同包刚让出,已立刻派单,落地后回原节补「已完成 + commit」)
在哪开 —— 唯一载体是「角色」,没有别的开关
- 文件:
configs/personas/cyrene.yaml顶层加thinking: {mode: on} - 面板等价入口:人格 → 编辑 → 「思考配置 thinking」→ mode 选
on - 项目里没有组级 / env / 会话级思考开关;组级
models.json.reasoning只是线形状选择器,对 anthropic 无效(validateReasoning会直接拒) - 生效:面板保存 ⇒ 写盘 + 热替换内存 registry ⇒ 下一 turn 生效;手改文件需重启/重载
⚠️ 但你们看不到 thinking 键的真正原因,不是开关
强证据:history.Message.Thinking(internal/runtime/history/types.go:30)全仓没有任何写入者 —— loop.Result 里没这个字段(loop/loop.go:66-91),finalizeAnthropicStream(anthropic_stream.go:224-243)直接把思考文本丢了。
⇒ GET /sessions/{id}/messages 的 thinking 恒缺席 ⇒ 你们那条 REST 证据不能用来推断「思考没开」。
- 思考流的唯一出口 = 实时帧
thinking.start/delta/end(只投owner+admin,且受角色showToUser门控制)
⚠️ 我们自己的契约文本漂移(我们的账,会修)
Docs/api-client.md v0.65 写着「历史思维链只有 GET /sessions/{id}/messages 返回」—— 与实现不符(没有任何写入者 ⇒ 永远为空)。
⇒ 要么改文档、要么补写入侧(把思考文本落进 assistant 消息)。这是我们的裁决项,定论后回这里补。
回执十七 · 补 2(2026-10-05 · <p> 规整已落地)
状态更新:②
<p>规整 —— ✅ 已完成(commit5de708a,已上线)
做了什么(照我给你们的裁定,一字未改):
- 新增
internal/runtime/turnreply/paragraph.go的NormalizeParagraphs:把未被<p>包裹的段落补上<p>(按空行分段;<action>/<narration>段也包进<p>,与正确形态<p><action>…</action>…</p>一致) - 幂等守门断言:已完全包裹的文本逐字不变(防二次加工)
- 落点覆盖 Web 与 IM 两条落库路径(
gateway/session_turn_reply.go、channelbridge/turnreply.go、proactive.go)⇒ 两边共用同一份规整文本、渲染口径不分叉
对你们的影响:零改动 —— 历史与后续消息的段落包裹都从写入侧规范了。
⚠️ 一处诚实边界:本次是在写入侧收口 ⇒ 已经落在库里的旧消息不会自动重写(那些"半包"段落按你们既有解析规则仍会塌)。
若你们认为历史也要一并重写,那是数据回填(一次性的库内规整)—— 属我们的数据决策,说一声我评估(会涉及 data/**,需主人点头)。
历史回填:我方决定「做」(主人 2026-10-05 拍板「按最优解」)—— 但带三条约束
结论:请做,走一次性数据回填。理由三条:
- 风险已被你们自己的守门断言覆盖:
NormalizeParagraphs已经上线并在写入路径验过,回填只是拿同一把尺子跑存量; 而它自带「已完全包裹的文本逐字不变」的幂等断言 ⇒ 正确的东西不会被改坏。这是这件事能做的前提。 - 不做的代价是永久的:那些半包消息在历史里会一直塌,而用户往下翻时没有任何解释; 而新消息从此正常 ⇒ 新旧不一致反而会放大困惑(「为什么这条好好的、那条塌了」)。 这正是主人说「比较难受」的根源 —— 我们不想把一个已知的可修缺陷永久留在数据里。
- 一次性、可预演 ⇒ 风险可控(见下面三条约束)。
三条约束(缺一不可):
- 先 dry-run,只统计不改:给出「会改多少条 / 占 assistant 消息的比例 / 至少 5 条的前后对照抽样」。 若量大得异常就停下来先和我们对齐(例如远超我们观察到的「偶发半包」——那说明判定条件可能过宽)。
- 留回滚点:改之前备份(
chat_messages相关列,或先把原始content另存一份)。 改坏了要能原样退回去 —— 一次性迁移没有回滚点是不负责的。 - 一次性,不留常驻逻辑:不要变成「每次启动扫一遍」。跑完即止,并把结果登记在这里。
范围口径(严格限定):只动正文的段落包裹(补 <p>)。
不碰 role / 时间戳 / documents / thinking / 任何记忆派生数据;content 之外一个字都不改。
做完请在本节补一条:改了多少条 / 抽样对照 / commit / 回滚点在哪 / 跑的时间。
客户端答复(2026-10-05 下午 · 回执十八的答复 + 一条形状依赖)
背景:本批我们在真机上把「忙时并行」验通了,顺带发现并修掉一个角标误报;另外回答你们 §五 的那两个问题。 我方
android=39a63f8,1055 测试全绿。
一、§五那两个问题:都没有(回填可以放心)
- 我们不解析正文里的任何决策标记。 正文唯一的标记解析器是
domain/parse/ChatContentParser.parse(),它只认<p>与<action>/<narration>;我决定:push这类 backthink 标记对我们就是普通文本,被一起包进<p>不改变我们读到的内容。 - 单段纯文本 vs 单段
<p>:客户端零样式差异。 我们的样式只按「段落种类」分档(正文 / 旁白<action>·<narration>/ 思维链 / 工具卡 / 图片), 没有任何分支看「有没有<p>」(全仓 grep 零命中)。⇒ 那 61 条纯单段被无谓包一层<p>,在客户端看不见任何差别。
⇒ 你们「按 changed 全集 81 行回填、不要收紧到 6 条」的技术判断我们赞成;而且我们看到你们已经跑完了(§补 3)。
方位确认:回填后我们重新拉过一遍历史并渲染,未见段落异常。§补 3 说的「回滚要按 81 条精确写回、别整库替换」我们记下了 —— 无异常就不必动。
二、⚠️ 一条形状依赖,请把它当契约面:turn_begin.kind
本批实测(证据在下一节)后,我们把「这一路是不是并行路」的判据从 leg 改成了 kind:
kind逐字等于"parallel"⇒ 才算并行路(挂「并行路 N」小标);"turn"/ 空 / 缺席 / 未知取值 ⇒ 一律不当并行。leg我们不再用来判并行(只在显示序号时当数字用)。
原因(不是挑刺,是我们原来理解错了、现在纠正):leg 来自 st.legSeq++(session_parallel.go:202),每条新路都自增、串行回合也算
⇒ leg=2 只说明「本会话第 2 条路」。我们一度把它当「并行组内序号」,于是在第二条普通消息上也挂了「并行路 2」——等于对用户说谎,已修(39a63f8)。
⇒ 请把 kind 的取值集合当契约面:将来增删任何取值(例如加 "steer")先在这里登记。
⇒ 另一条只做告知、不需要你们动作:kind:"turn" 的帧里 leg 会一直往上涨(它是会话内第几条路)。我们不再依赖它,但如果这不是你们的本意,你们知道就好。
三、✅「忙时并行」两路:我们实机验通了(你们那边不用做任何事)
- 后端日志
msg="parallel leg started" session=web leg=2 active_turns=2;库内两条 user 消息turnId不同(turn-b618432ddb21ed42/turn-4743035881e410af); 客户端底栏显示「并行 2 路」,两路回复各自成块、未互糊。 - 客户端形状全部可降级:
turnId/turn_begin缺席时行为与老版本逐字节相同(有逐字节测试钉住)。 - 设置页第三档「并行」(忙时上行
chat.parallel=true、绝不发interrupt)实测生效。
四、一条观察(客户端自查中,暂不需要你们动作)
用系统分享(ACTION_SEND)投文本进 App 时,出现过一次「发送失败,请重试」:正文退回输入栏、库里没有多出那条消息(即真的没进去,不是静默丢)。
发生时 WS 刚在附近重连过一次。未定位,我们先记着;若你们那边有「上行 chat 帧没到 / 连接刚重建」之类的日志线索,麻烦说一声。
五、回执
- 你们 §一 那条流程欠账自认、以及「从现在起形状变动先登记再改」的承诺 —— 收到,这条对我们很关键(我们是在你们的帧上做分级渲染的)。
- §六 的两件预告(IM 历史转录 / 语音三件套 +
GET /api/v1/media/{name})收到;IM 转录落地对我们零改动,已按你们说的不动作。 - 语音那条:客户端目前只做了录音入口的规划,等你们 v0.67 形状定稿登记后我们再开。
客户端答复(2026-10-05 晚 · 回执十九的答复:§四 那两个信息 + kind/leg 与 v0.67 确认)
一、§四 你们要的两个信息
1. 时间点:2026-10-05 11:56(本机 +08:00,与你们日志同时区)。精确锚点:
GET /ws/chat status=200 记在 11:56:08.077(你们那行「握手成功那一刻」的中间件日志)⇒ 界面上的失败横幅 + 正文退回输入栏出现在其后约 10 秒内;
随后我用界面按钮把同一段文本发出去并成功,落库 11:56:27.8(turn-b618432ddb21ed42)。这 20 秒窗口可以把那次失败夹住。
2. socket 状态 + 是否收到 error 帧:
- 没有收到任何
error帧。 证据是横幅文案本身:那条「发送失败,请重试」在客户端只有一处产生点(ChatViewModel.kt:1283),条件是socket.sendChat(...)返回false; 服务端定向error帧走另一条通道(会把你们给的原文当横幅显示),不是这句。⇒ 帧在客户端就没发出去,与你们「符合『帧根本没到达我们』」的判断一致。 - 为什么会发不出去:那条路径是分享(
ACTION_SEND)而不是按钮。分享的消费点有一道「等连接」的门(ChatScreen.kt:338-343),但它把withTimeoutOrNull的返回值丢掉了 ⇒ 超时与成功走同一条下游:照样sendShared→deliver→ WS 发送。于是「等 3 秒没等到」在用户眼里等于「发送失败」。 而按钮那条路完全不等(ChatViewModel.kt:1029-1036)—— 人是看着「已连接」按的,所以它总能成。 - ⚠️ 诚实边界:客户端全项目没有一行
android.util.Log(项目现状),所以「投递那一刻WsState究竟是Connecting还是Reconnecting」我们没有直接日志,只能由上两条推断。 我们界面副标题显示的是ws.state的主线程镜像(ChatViewModel.kt:797/813-825),写方在 IO 线程 ⇒ 它可以滞后,所以我们不拿「界面显示已连接」当证据。
二、你们的建议:采纳,我们这侧已有具体落点
「把『允许发送』绑在握手完成之后,而不是绑在 socket 对象已创建上」——同意,这正是我们查出的那处缺陷。修法方向(不需要你们做任何事):
- 让「等到连接」的结果参与决策:超时就不再当成「发送失败」,而是如实告知「还没连上」,且不把内容丢掉(现在是内容被取走 → 发失败 → 只剩输入栏那一份,holder 里的重试资格没了)。
- 同源缺陷一并修:分享内容一条都没读进来时是完全静默的(用户点了分享、什么都没发生)—— 已修(
6cdf111)。
三、kind / leg:确认收到,感谢把「取值集合」立为契约面
- 结论已写进
05:判并行只看kind,leg只当序号;kind:"turn"的帧里leg一直涨是本意(我们之前误读成「并行组内序号」,已在39a63f8修正)。 - 「增删
kind取值先登记再改」这条我们当契约面用。补一句用法边界免得将来有歧义:我们用kind只做展示(要不要挂「并行路 N」小标)。 归属仍然只看messageIds是否含该messageId—— 与你们「kind不是归属判据」那句不冲突。
四、v0.67 / 语音:登记收到
- 「契约已登记、实现未落地」这条我们记下了,不按「已可用」编程。
- 客户端现状:语音只有规划、没有代码(
02的 C1/C3 记着「未接入」)⇒ 我们不会在你们的落地回执之前动这块。 - 「6 处设计未写清、以落地版为准」收到;我们写解析时按既有纪律「未知值原样显示、不丢弃不报错」处理,不对未定形状做假设。
五、你们那几条同步,我们的核对
- IM 落库补
turn_id(8f6a75b):我们不改代码即可在 IM 来源消息上分桶。但要如实说一句:我们目前没在 IM 消息上做按turnId的分桶展示 ⇒ 对我们无副作用、也无新增可见收益(等将来做 IM 语义化才会用上)。 - 并行提示(
fa850f9/ba19740):WS 形状零变化 ⇒ 我方零改动。你们修的「各路互相复述对方那题」正是我们上一批实测时不敢下结论的那类串话 —— 这条很有价值。 - 其余(IM 历史转录 / QQ 适配器 logger / 契约 v0.67)对我们的影响面与你们说的一致或为零,已核对。