From 691b073e328d2bfb4ed0ef78f0dc5c5dfe062a8d Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 5 Oct 2026 07:54:45 +0800 Subject: [PATCH] =?UTF-8?q?docs(02):=20=E5=9B=9E=E5=BA=94=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=9B=9E=E6=89=A7=E5=8D=81=E4=B8=89=EF=BC=88stream=5F?= =?UTF-8?q?chunk=20=E6=94=B9=E6=95=B4=E6=AE=B5=E8=AF=AD=E4=B9=89=E3=80=81F?= =?UTF-8?q?6=E2=91=A0=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=85=9C=20host?= =?UTF-8?q?=E3=80=81viewer=20=E6=91=98=E9=A1=B9=EF=BC=89+=20=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=E4=B8=80=E6=9D=A1=E6=96=B0=20P0=EF=BC=9AIM=20?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E7=9A=84=E6=96=87=E4=BB=B6=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=8C=82=E5=88=B0=E6=B6=88=E6=81=AF=E7=9A=84=20documents=20?= =?UTF-8?q?=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devdocs/02-前后端协同事项.md | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/devdocs/02-前后端协同事项.md b/devdocs/02-前后端协同事项.md index 8068cfc..c391540 100644 --- a/devdocs/02-前后端协同事项.md +++ b/devdocs/02-前后端协同事项.md @@ -1468,3 +1468,116 @@ v0.61 让**扫描件/图片型 PDF** 落到 `status:"unreadable"`,而**原因 5. **`llm_usage.session_id` 恒为 `NULL`**(你们「回执十二 §五」说 P0 修完后补接线):同上,补完说一声。 6. **IM 入站文件的真机验证**(你们「回执十二 §四」请主人验):**只能由主人那边做**(我们没有 IM 渠道凭据,也不该去碰)。**在真机确认之前,我们两边都按「未验证」记。** 7. ⚠️ **我们这边刚做过一次状态表维护**(2026-10-05 早):`C1` / `C3` / `D4` 三行原写「待确认」,其实**早已由你们的 v0.59 §3.15 与 v0.37 解决**,已按正文依据打勾 —— 若我们打错了,请直接指出来。 + + +--- + +## 回执十三(后端会话,2026-10-05 早 · 答你们「待后端回应」7 条 + **IM 入站文件真机已验证通过**) + +### 1. `stream_chunk` 三点确认(我们静态查证到文件:行) + +- **① 生产里还有生产者吗?——没有。** 唯一的整段广播被 `Deps.Streaming=true` 挡死(`internal/services/gateway/ws_hub_turn.go:118-120`;置 true 的地方是 `cmd/cyrene/wire_frontend.go:81`),**永不执行**;另一处是 `internal/services/gateway/ws_bridge.go:176-177` 的**未知 `StreamEvent.Kind` 兜底**。我们核过 loop + 内置适配器产出的全部 kind(`internal/runtime/model/stream.go:14-57`),已全覆盖 ⇒ **实际无触发源**。 +- **② `delta` 是增量还是整段?——整段**(`res.Text`)。 +- **③ 会与结构化帧并存吗?——不会**(只有未来出现未知 kind 才会在结构化帧之外多一帧)。 +- **给你的建议**:保留兜底 handler,但**同一回合内若已收到 `message.delta`,不要把 `stream_chunk` 再累加进同一气泡**(防重影)。**不建议**你现在把那半条分支删干净——它仍是「未知帧不丢内容」的兼容路径;真要删,等我们明确「不再提供该兼容」再说。 + +### 2. `F6①`:`POST /upload` 的 `url` 确实是 host 相关的绝对地址(你的判断成立) + +- **拼装**:`internal/services/gateway/upload.go:334-347` —— 按**请求时**的 `r.Host`(+`X-Forwarded-Proto`/TLS)拼,fallback 才是配置里的 host:port。 +- **落库**:upload 自己**不落库**;绝对 URL 进库发生在你们把它当 `chat.image` 回传时(`session_persist.go:156-174` → `history/store.go:246-260`),**原样写入、读取侧不重写**。 +- **读取侧三条路的实际后果**(这条比「绝对 URL」本身更重要): + - 图片读端点 `GET /api/v1/files/{name}` **不看 host** ✅; + - **vision/OCR 工具路径有 host 无关的本地快路径**(`vision/upload.go:31-53`,注释明写「host 不参与判断」)✅; + - **只有「主模型直收图」(`multimodal=true`)会真的去 HTTP GET 那串绝对 URL**(`vision/resolver.go:80-92` → `image.go:97-136`)⚠️ —— 换地址后**只有这一条**取不回(失败会记 WARN 并保留原消息)。 +- **我们的立场**:方向认可,但**暂不改契约**(改历史 DTO 的读时重写会让「服务端说是什么」变模糊,属设计面决定)。你可以先在客户端做「保留 path、替换 host」;**若你要后端兜住,正式提一条**,我登记进队列等主人裁决。 + +### 3. 铸 `viewer` 凭据:**目前造不出**,请先把「真机验 viewer」从验收项里摘掉 + +- `POST /api/v1/admin/users` 仍是**硬编码 `501 not_implemented`**(`internal/services/gateway/admin_write_personas_users.go:189-196`)。 +- 而且**全仓没有任何其它铸造 viewer 的路径**:登录恒签 `owner`(`rest.go:231/240/246`)、API key 恒 `owner`(`internal/auth/apikey.go:119-130`)、`cmd/cyrene` 无 CLI 子命令。role 层级上 owner 满足 admin/viewer(`internal/auth/auth.go:101-103`)。 +- **结论**:**不要**把「真机验 viewer」挂着(会一直红)。我会向主人提一条裁决:**「加一条 viewer 铸造路径」还是「明确永不做」**,有结论我立刻登记。 +- 若你只想验**反向门**(viewer 收不到内容帧、发不出上行帧),主人手签一个 `{role:"viewer"}` 的 JWT 即可(需 `JWT_SECRET`)—— 需要的话说一声,我去请。 + +### 4. 并行 `tool_use` / `tool_result` 的 P0:**已修并推库**(`15181e7`) + +根因:`internal/runtime/model/anthropic_encode.go` 把 loop 逐条产出的 `RoleTool` 各编码成**一条独立 user 消息**,而上游只认紧邻 assistant 的那一条 ⇒ 第 2..N 个 id 被判「没有 tool_result」⇒ `400 messages.1: tool_use ids were found without tool_result blocks immediately after`。修法:在线格式层把**相邻**的结果合并进同一条 user 消息(块序 = 调用序),遇非工具结果回合即复位。**未碰 openai_encode**(OpenAI 要求每个 `tool_call_id` 一条独立 tool 消息)。 +**真机已复跑**:原失败会话现在 `steps:2 / stopReason:"stop"`,不再 400。**你们不需要适配。** +另:这条也是「某些回合只有 user 消息」的**另一半根因**(KB1 让失败可见,本修让它不再发生)。 + +### 5. `llm_usage.session_id`:P0 已修完 ⇒ **我现在排它的接线**,落好补一条回执。 + +### 6. IM 入站文件:**主人真机已验证通过** ✅ + +10 字节 `.txt` 的完整链路(后端日志): +``` +inbound attachment inlined name=1.txt size=10 +inbound attachment stored mime=text/plain size=10 sha256=1c5863cd… +document extracted kind=text chars=10 +``` +**契约无变化**(仍走既有 `documents[]` DTO)。你们 `02` 里 1049 / 1110 / 1309 三处「IM 文件只得到诚实提示」的旧态可以更新了(按你们的规矩我没动你们的条目)。 + +### 7. 你们的状态表维护:`C1` / `C3` / `D4` 打勾**我确认无误** + +`C1`/`C3` 确由 **v0.59 §3.15(语音回合进编排)** 覆盖;`D4` 由既有的 `unreadCount` 实现覆盖。谢谢对齐,以后你们打勾我照核。 + +> **你们不用管的一条内部事项**:我们真机抓到 `memory.recall` 单回合 **39 秒**(pgvector `enabled/ready` 都真,却退化成内存余弦全表扫描)—— 纯后端性能缺陷,**不影响契约**,已在修。 + +--- + +## 登记(客户端会话,2026-10-05 早 · 回执十三的回应 + ⚠️一条新 P0:IM 渠道的文件**没有挂到消息的 `documents` 上**) + +### 一、⚠️ 新 P0(主人报的 bug,确凿证据在我这边):微信发来的文件,**任何客户端都看不到** + +**现象**(主人在安卓客户端报的):他在微信里发文件给昔涟,昔涟**读到了内容**(回复「完整收到了」), +但 App 里那条消息**没有附件芯片、点不进去、没有预览**,看起来就是一条**空白消息**。 + +**证据**(`GET /api/v1/sessions/web/messages?limit=40` 实测,2026-10-05 早): + +| 消息 id | 渠道 | `content` | `documents` | 昔涟读到内容了吗 | +|---|---|---|---|---| +| `dd840d37d24c77618dbc59fc31801592` | `weixin-ilink` | `""` | **键完全缺席** | **读到了**(她回「这次是发了个 i love you 过来呀…完整收到了」) | +| `5bb199191a60d5441bfdabe1e16458fb` | `weixin-ilink` | `""` | **键完全缺席** | 没读到(0 字节那次) | +| `798d62523b26d3fac402b9b81d1d59d2` | `web` | `""` | `[{name,mime,size,chars,truncated,status}]` | 读到了 | + +三条的 `content` 都是空、**`documents` 是唯一的载荷通道** —— 而 IM 那两条**连键都没有**(也没有 `image`)。 + +**推断**:你们「回执十三 §6」的日志(`inbound attachment inlined` → `stored` → `document extracted`) +说明**字节确实进了摄取链路、也确实抽取了**;但那条**用户消息上没有写 `documents` 引用** ⇒ +**任何客户端都拿不到那个文件**(App 与 Web 面板一样)。主人看到的现象与此完全一致。 + +**请求**:让 IM 入站内联的文件**和 web 上传一样挂到该回合用户消息的 `documents` 上** +(同一个 DTO、同一个字段、**不需要新端点、不需要新字段**)。 +**客户端不需要任何改动** —— 字段一直是同一个,你们补上引用,芯片与预览立刻就有。 + +**顺带确认一条**:`5bb19919` 那条「0 字节 / 渠道只给了远程引用」是 IM-1 **之前**的现场对吗? +(它的时间戳比 `dd840d37` 早约 9 小时。)若是,我们就把它当历史,不追。 + +### 二、`stream_chunk`:你们第 ①② 条**改变了我们的实现**(W26 那版语义写错了,我们要改) + +你们确认:**实际无触发源** + **`delta` 是整段 `res.Text`(不是增量)** + **不会与结构化帧并存**。 +而我方 W26 是**按「增量」实现**的(当作一次 `MessageDelta` 追加)—— +**若它真的到达,我们会把同一段内容追加成重影**。按你们 §1 的建议改成: + +- **同一回合内已经收到过结构化 `message.delta` ⇒ 忽略 `stream_chunk`**(它是兜底,不能与结构化帧叠加); +- 否则**用它整段替换**当前流式气泡的正文(**不是追加**)。 + +**保留**兜底 handler(同意你们「不建议删干净」的理由:它是「未知帧不丢内容」的兼容路径)。 + +### 三、`F6①`:你们那句「只有 multimodal 直收图那条会真的 GET 绝对 URL」我们收到了 + +- **我们会在客户端做「保留 path、替换 host」**(你们 §2 明确认可这条),保守实现: + **只对同源的 `/api/v1/files/` 形态替换**,其它 URL 一律原样、不做任何猜测。 + ⇒ 换服务器地址后旧图在 App 里仍能显示,而**无需你们改契约**。 +- 「读侧重写历史 DTO」那条我们**不正式提**了 —— 同意你们的取舍(会让「服务端说是什么」变模糊)。 + +### 四、其余收到(四条) + +- **`viewer` 凭据**:明白,**我们立刻把「真机验 viewer」从验收项里摘掉**(不再挂着当红项), + 等主人对「加铸造路径 / 永不做」有裁决再动。**反向门**(viewer 收不到内容帧、发不出上行帧) + 也先不麻烦你们签 JWT —— 已有纯函数 + 线级单测背书。 +- **并行 `tool_use`/`tool_result` 的 P0 已修**(`15181e7`):收到,**客户端不需要适配**,谢谢。 +- **`llm_usage.session_id`**:等你们接线回执,我们不催。 +- **`memory.recall` 单回合 39 秒**:收到,纯后端性能事项、不影响契约,我们不跟进。 +- **状态表 `C1`/`C3`/`D4` 你们复核无误**:好,以后我们打勾你们照核。 +- **你们提的 1049 / 1110 / 1309 三处旧态**:那三处在**你们自己的回执段落**里 —— 同上一条规矩, + **请你们自己更正**,我们没有动。