接入契约 v0.63 的 documents[].reason:把「读不到的原因」原样送到附件预览弹层
后端 902b6ee / 契约 v0.63 给 historyMessage.documents[] 的元素加了可选字段 reason: 只在 status == "unreadable" 时下发(Go omitempty,extracted 时整个键缺席),内容与后端 注入模型清单里的那 10 条是同一份字符串,最长的一条 40+ 字。 改了什么 - DocumentRefDto 加 reason: String? = null。documents[] 在 WS 握手 history、 message_persisted、GET /messages 三处共用这一个 DTO,所以一处解析三条路全覆盖。 KDoc 写明三条口径:可选、只在 unreadable 时下发、是后端原话所以客户端原样显示, 且绝不做整句相等判断(需要分支只允许用稳定子串)。 - DocumentChipModel 加 reason,由 DocumentChips.of(ref) 取 ref.reason 去掉首尾空白后 非空才留,否则归一成 null——一句空话会顶掉弹层里的通用说明,比没有更坏。 芯片外观一个字没改:detail / statusLabel 保持现状(unreadable 仍显示「读不到」)。 理由:那句话太长,塞进一行芯片只会被 Ellipsis 切掉半句,比不显示更糟。 - DocumentPreviewGate.plan 增加 reason 参数:非空白时直接作为正文区那句说明原样返回 (NotText(reason)),压过本端按 mime / 扩展名的裁决;DocumentTarget.previewPlan 顺着 chips.reason 把它带进裁决。预览弹层本身一行代码都没改——它渲染的就是 plan.message 那句话,所以 reason 从 DTO 到用户看见的那句话整条链路上没有任何改写或截断。 - 配套单测 17 条。 为什么这么改 - 服务端那句原因已经断言「无法抽取正文」,它比本端按类型编的通用说明(只知道类型、 不知道到底为什么)准确;替它改写、归纳或截断就是丢掉服务端唯一一次说清原因的机会。 - 它也不该与正文并存:服务端说正文抽不出来,本端再把字节当 UTF-8 渲染,产出的正是 正文闸要消灭的「看着像文件坏了」。 - reason 缺席时(契约 v0.63 之前的历史消息)产出与改之前逐字相同:兜底那句话、判据、 文案全都不动。 测试(新增 17 条) - DocumentDtoTest +5:unreadable 带 reason 逐字解出;extracted 元素没有该键 -> null; v0.62 风格的缺键载荷 -> null(不解码失败、不替服务端编一句话);清单外的未知分类原样 保留;显式 null -> null。 - DocumentChipTest +5:reason 带进模型;句中标点与换行原样、只去首尾空白;空白串 -> null;绝不进芯片副行(detail 逐字不变);待发附件恒为 null。 - DocumentPreviewTest +7:reason 逐字成为正文区那句话(含未知分类,不丢不改不换兜底); 只 trim 一次再原样显示;缺席时与改前逐字相同(拿 refusalMessage 对比); 空白 reason 不改任何行为;text mime + reason 不再假装有正文。 - assembleDebug 通过;testDebugUnitTest 848 个测试 0 失败 0 错误 0 跳过。 已知代价(已写进 KDoc,供人拍板) 服务端判为读不到、但字节其实下得动的附件(例如超限的纯文本)会失去 App 内预览, 「用其它应用打开」与「保存到下载目录」两条出口照旧完整可用——与「未知 mime 判为非文本」 是同一笔不对称账。
This commit is contained in:
1 parent
a84d473e2f
commit
e5fa1b871f
7 files changed
+269
-8
No files matched your search
@@ -25,6 +25,8 @@ import kotlinx.serialization.Serializable
|
||||
* 3. 下行/历史元素只回**引用与统计,绝不回正文**(吸取 `G3`「base64 原样落库、每页历史重传」
|
||||
* 的教训)。`status:"unreadable"` 才是「读不到这个文件」的可观测信号,**不是** `truncated`
|
||||
* (`truncated` 只说明注入正文被预算截断,正文照样进得了上下文)。
|
||||
* **契约 v0.63(后端 `902b6ee`)在此之上加了可选 [DocumentRefDto.reason]**:读不到时随
|
||||
* `status:"unreadable"` 一起下发的**服务端原话**,客户端原样显示(详见该字段的 KDoc)。
|
||||
*
|
||||
* ## 客户端必须两种都吃的一件事
|
||||
* 「0 个附件」这一形状在后端**尚未定死**(回执五原文:期望「不出现或空数组」,S3.5 回报里定死)。
|
||||
@@ -58,8 +60,8 @@ data class DocumentUploadResponse(
|
||||
* 下行/历史则带上全部统计字段。
|
||||
*
|
||||
* 字段可出现性(后端 `history.DocumentRef` 的 `omitempty` 口径,逐字段核对过):
|
||||
* `name` / `mime` / `size` **恒在**;`chars` / `truncated` / `status` 为 Go 的 `omitempty`
|
||||
* —— 零值(0 / false / "")时**整键省略**,所以取值一律给默认值,解码不会因缺键失败。
|
||||
* `name` / `mime` / `size` **恒在**;`chars` / `truncated` / `status` / `reason` 为 Go 的 `omitempty`
|
||||
* —— 零值(0 / false / "" / nil)时**整键省略**,所以取值一律给默认值,解码不会因缺键失败。
|
||||
*/
|
||||
@Serializable
|
||||
data class DocumentRefDto(
|
||||
@@ -80,4 +82,18 @@ data class DocumentRefDto(
|
||||
* 未知值**原样保留**(不丢弃、不报错、不占位符),由客户端决定如何显示。
|
||||
*/
|
||||
val status: String = "",
|
||||
/**
|
||||
* **读不到的原因**(契约 v0.63 新增,后端 `902b6ee`):`status == "unreadable"` 时下发的
|
||||
* 那句**服务端原话**;`status == "extracted"` 时**整键缺席**,因此是可选字段。
|
||||
*
|
||||
* 两句纪律:
|
||||
* 1. **原样显示**。[reason] 与后端**注入模型清单里的那 10 条是同一份字符串**(逐字相同,
|
||||
* 最长的一条 40+ 字),客户端**不替服务端改写、不截断、不归纳**成自己的话——
|
||||
* 本端能编的只有像 [top.yeij.cyrene.domain.chat.DocumentPreviewGate.refusalMessage]
|
||||
* 那样「按类型泛泛说一句」,比这里的原因粗得多;
|
||||
* 2. **绝不做整句相等判断**。分类若真的需要分支,只允许用**稳定子串**(如「需要 OCR」)——
|
||||
* 整句比对会在后端改一个字时静默失效,且失败的样子是「显示成兜底那句话」,
|
||||
* 看起来完全正常。
|
||||
*/
|
||||
val reason: String? = null,
|
||||
)
|
||||
Reference in new issue
Block a user