尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Onyx 移动端 Chat 移植的高层架构设计:Hybrid Seams 方案下的 NDJSON 流式聊天实现

Onyx 移动端 Chat 移植的高层架构设计:Hybrid Seams 方案下的 NDJSON 流式聊天实现 Onyx 移动端 Chat 移植的高层架构设计Hybrid Seams 方案下的 NDJSON 流式聊天实现【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer导读本文深入解析 Onyxdanswer移动端原生应用如何以「Hybrid Seams混合接缝」方案将 Web 端的聊天体验完整移植到 React Native 客户端应用通过expo/fetch直连未做任何改动的后端以换行分隔 JSONNDJSON流式协议驱动逐 token 渲染同时保持 Web 端代码零改动。读完本文你将掌握移动端聊天的端到端调用链、useChatController与 zustand 双层状态设计、纯 TS 移动端原生数据层NDJSON 解析器、消息树、历史重建的实现细节以及expo/fetch与apiFetch的分工依据可直接对照仓库源码复现整套架构。说明本文对应设计文档 docs/mobile-chat/02-high-level-design.md属于mobile-chat系列00-index / 01-research / 02-high-level-design / 03-detailed-design / 04-implementation-plan / 05-pr-roadmap / 06-unified-chat-surface中的高层设计章节。一、设计目标把聊天体验搬到原生移动端后端与 Web 零改动1.1 这个设计要解决什么移动端 Chat 移植要达成的核心体验是用户打开 App → 选择 Agent或使用默认→ 围绕企业知识库进行流式对话消息可以可选地限定在某个项目project内也可以携带文档/照片附件。它完整镜像 Web 产品的行为但关键约束是只新增客户端后端保持不变——移动端与 Web 端共享的是同一套后端 API、同一套流式协议而不是共享代码。从源码结构看这一目标被严格贯彻移动端聊天相关逻辑全部收敛在 mobile/src/chat/ 目录解析器、消息树、历史重建、文件描述符等而 Web 端保留自己的副本mobile/src/chat/ndjson.ts的注释明确写着「(webs handleSSEStream internals.)」——即移植自 Web 的handleSSEStream内部逻辑但二者是各自独立的实现。1.2 方案代号Approach C — Hybrid Seams文档为本次移植定下的方案代号是Approach CHybrid Seams混合接缝。其含义是移动端通过标准 HTTP API 与后端对接接缝在客户端与后端的协议边界上客户端内部则采用「共享查询层 移动端原生聊天层」的混合结构。经过 2026-06-26 的PR 2 Decision调整后最终方案进一步收敛为「不共享任何聊天代码」Web 保持原样、零改动移动端在mobile/src/chat/自建纯 TS 层。二、端到端工作流从发消息到流式渲染2.1 前置基础AuthGate 与 apiFetch移动应用启动时已经通过AuthGate进入已认证的 Shell 结构具备可用的侧边栏以及一个名为apiFetch的 HTTP 层——它负责注入用户的 bearer token 并解析服务器 URL。新的聊天功能是在现有(auth)分组旁新增一个已认证的聊天路由组(app)包含 new-chat、chat/[id]、history、projects 四个页面。2.2 发送消息的三步编排当用户点击发送移动端的编排 HookuseChatController做三件事会话不存在则创建调用POST /api/chat/create-chat-session携带所选 Agent 的persona_id与当前激活的project_id乐观更新消息树在内存中的 message tree 里立刻放下一条用户气泡和一条空的助手气泡打开流式请求向/api/chat/send-chat-message发起流式 POST。后端对应实现位于 backend/onyx/server/query_and_chat/chat_backend.pyPOST /create-chat-sessionchat_backend.py#L462-L489接收ChatSessionCreationRequest依赖require_permission(Permission.WRITE_CHAT, allow_anonymousTrue)权限校验返回CreateChatSessionID含chat_session_id与incognito标志persona 或项目越权时抛 403非法 persona 抛 400。POST /send-chat-messagechat_backend.py#L771-L867默认streamTrue返回text/event-stream的StreamingResponsestreamfalse时返回完整ChatFullResponseJSON还支持llm_overrides多模型并行流式2-3 个 LLM 并行仅流式模式可用。2.3 流的核心expo/fetch NDJSON流式请求是全 App唯一不走apiFetch的 HTTP 调用改用expo/fetch因为只有它暴露可读的字节流。后端以**换行分隔 JSONNDJSON**回复——每行一个数据包。移动端用纯 TS 的解析器镜像 Web 的 line-buffering 逻辑把字节块转成类型化数据包。这个解析器的真实实现是 mobile/src/chat/ndjson.ts 中的createNdjsonBufferpushChunk(text)追加文本按\n切分尾部的半行保留到下一次调用跨 chunk 的包完整还原每行JSON.parse解析失败时尝试用/\{[^{}]*\}/g抢救扁平 JSON嵌套对象不可恢复flush()流结束时解析残留的半行无花括号恢复畸形尾部直接丢弃与 Web 行为一致。数据包类型定义在 mobile/src/chat/streamingModels.tsPacketType枚举覆盖核心消息包message_start / message_delta / message_end、stop、error以及搜索工具search_tool_*、图片生成、Python 工具、open_url_*Web 端叫FETCH_TOOL_*、工具调用参数、推理reasoning_start/delta/done等全套包类型。2.4 移动端 Hook 的处理策略移动端 Hook 读取数据包后忽略心跳包heartbeat核心体验只关心文本包MESSAGE_START/MESSAGE_DELTA/MESSAGE_END与STOP/ERROR把流式文本追加到助手气泡上约每 50ms 批量 flush 一次 UI避免屏幕频繁重绘抖动。收到STOP包后对话回到空闲态助手气泡获得真实的服务器消息 IDmessage-id。2.5 渲染层FlashList v2 与 StreamingMarkdown聊天屏用FlashList v2渲染消息树采用**非倒置non-inverted**模式流式期间自动吸附到底部。流式助手气泡通过React Native markdown 组件渲染不断累积的文本。2.6 历史重开与会话恢复重开历史会话时从后端拉取历史记录用移动端原生实现的processRawChatHistory镜像 Web 逻辑重建消息树。实现位于 mobile/src/chat/chatHistory.tspackets按序数与助手消息一一对应每个助手回合一个包列表以message_id复用为nodeId错误回合用error字段渲染为 error 类型消息Web parity并依据parent_message重建父子关系、按message_id排序子节点重开回合没有起始时间因此直接用processing_duration_seconds填充耗时显示。会话、Agent、项目列表均通过TanStack Query获取已接入并持久化到 MMKV以服务器 URL 为键——切换后端时不会串出脏数据。三、组件交互架构文档给出如下组件交互图本节按原文结构转写并标注实现文件┌─────────────────────────────────────────────┐ │ mobile/src/app/(app)/ (expo-router group) │ │ new-chat · chat/[id] · history · projects │ └───────────────┬───────────────────────────────┘ │ renders ┌───────────────▼───────────────┐ ┌──────────────────────┐ │ RN UI (mobile-only) │ │ TanStack Query │ │ MessageList (FlashList v2) │◄────┤ sessions · agents · │ │ StreamingMarkdown · InputBar │ │ projects · files │ └───────────────┬───────────────┘ │ (apiFetch MMKV) │ subscribes│ └──────────┬─────────────┘ ┌───────────────▼───────────────┐ │ apiFetch (JSON) │ chatSessionStore (zustand) │ ▼ │ per-session messageTree, │ ┌─────────────┐ │ chatState, AbortController │ │ Onyx │ └───────────────┬───────────────┘ │ backend │ drives ▲ │ updates │ (unchanged) │ ┌──────────┴─────▼───────────────┐ └──────▲──────┘ │ useChatController (mobile) │ │ expo/fetch │ onSubmit · drain stream · flush│─────────────────┘ (NDJSON stream) └───────────────┬─────────────────┘ │ calls ┌─────────────────────▼──────────────────────┐ │ mobile/src/chat/ (pure TS, mobile-native) │ │ ndjson parser · contracts (chat/streaming/ │ │ files/agents/proj) · messageTree · │ │ processRawChatHistory · fileDescriptors │ └─────────────────────────────────────────────┘ (web keeps its own copies — nothing shared)从源码看mobile/src/chat/目录与上图完全对应ndjson.tsNDJSON 解析器、messageTree.ts消息树数学、chatHistory.ts历史重建、messageProcessor.ts增量包→状态归约、streamingModels.ts包类型契约、contracts/documents、projects 契约、timeline/推理状态、工具展示、分组、以及agents.ts / fileDescriptors.ts / citations.ts / sources.ts / tools.ts等模块并配套 mobile/src/chat/tests/ 下的ndjson.test.ts、messageTree.test.ts、chatHistory.test.ts、messageProcessor.test.ts、fileDescriptors.test.ts等单测。3.1 消息树纯函数式的增量更新消息树是按 nodeId 键控的 Map根节点是合成的系统节点SYSTEM_NODE_ID -3通过latestChildNodeId实现分支。见 mobile/src/chat/messageTree.tsupdateParentInMap负责把子节点挂到父节点并依据「强制最新 / 唯一子节点 / 新添加」三条规则更新latestChildNodeIdgetMessageByMessageId提供按服务器消息 ID 反查。该文件头注释说明其「几乎逐字移植自 Web 的 messageTree.ts唯一差异是裁剪后的最小 Message 类型」。3.2 增量包处理带游标的归约器mobile/src/chat/messageProcessor.ts 是 Web 端packetProcessor的忠实移植维护nextPacketIndex游标只处理游标之后的包将包按回合turn/标签页tab分组为时间线条目合成SECTION_END让一步骤完整闭合同时维护 9a 引文/文档/完成度跟踪citationMap、documentMap、isComplete、stopReason、工具处理时长等。这解释了文档「每个 ~50ms 批量 flush」背后的增量语义——每次只把新到包归约进对应助手节点而非整体重建。四、关键组件清单组件职责类型(app)路由组已认证聊天屏new-chat、chat/[id]、history、projects新增移动端useChatController/useChatSessionController移动端编排提交、驱动流、批量 flush、停止、加载/恢复历史新增移动端chatSessionStorezustand会话级瞬态状态消息树、chat state、AbortController。不持久化新增移动端expo/fetch 流包装器唯一的流式 HTTP 调用把字节喂给移动端原生解析器新增移动端TanStack Query hookssessions、agents、projects、files 列表持久化按服务器 URL 键控新增移动端RN UIMessageListFlashList v2、StreamingMarkdown、InputBar键盘吸附、Agent 选择器、项目屏、附件 chips新增移动端移动端原生聊天数据层mobile/src/chat/NDJSON 解析器 包/聊天/文件类型 消息树数学 历史重建 文件描述符辅助从 Web 移植零共享新增移动端五、端到端场景与关键操作序列5.1 完整用户场景用户打开 App →AuthGate将其送入(app)聊天首页侧边栏通过 TanStack Query 展示近期会话用户在选择器中点选一个 Agent → 选中态被保存空聊天屏展示 starter prompts用户输入「Summarize the Q3 board deck」并点发送useChatController创建会话persona_id 所选 Agent→ 拿到chat_session_id导航到chat/[id]在消息树中放下用户气泡 空助手气泡chatStateloading通过expo/fetchPOST 消息后端流式返回 NDJSON 包移动端原生解析器产出数据包Hook 把MESSAGE_DELTA文本追加到助手气泡每 ~50ms flushchatStatestreamingFlashList 保持视图吸附底部助手气泡边增长边渲染 markdownSTOP到达 →chatStateinput助手气泡获得服务器 message-id会话列表 refetch 使历史反映新回合用户在回答中途把 App 切到后台再返回 → 重开会话时回放缓冲的数据包并重新挂接到进行中的运行。5.2 关键操作序列解析认证 服务器 URL现有AuthGate/sessionManager若无会话则创建persona_id、project_id→chat_session_id乐观播种消息树用户 空助手节点打开带 bearer JSON body 的expo/fetchPOST 流解码字节 → 移动端原生 NDJSON 解析器 → 类型化数据包丢弃心跳将MESSAGE_*包归约进助手节点批量 flush 到 zustand~50ms收到STOP/ERROR/ abort结算聊天状态、释放 reader、捕获 message-id、refetch 会话重开/恢复拉取历史 → 移动端原生processRawChatHistory→ 消息树若运行仍在进行尾随resume-stream。第 8 步对应的后端断点续流端点真实存在GET /chat-session/{session_id}/resume-streamchat_backend.py#L1280返回StreamingResponse这正是「重新挂接到进行中的运行」的服务端支撑。六、关键设计决策与理由6.1 流式用expo/fetch其余用apiFetchRN 的 legacy fetch没有可读响应体expo/fetchSDK 56 起为默认暴露response.body.getReader()从而可以复用 Web 完全一致的 NDJSON 解析逻辑。而所有 JSON 列表请求仍走既有apiFetch关口bearer 注入 错误归一化保持单一收口。6.2 聊天纯逻辑层全部移动端原生零共享解析器、消息树、历史重建、包→展示映射全部写在mobile/src/chat/Web 保留自己的副本。理由共享包机制onyx-ai/shared工具 Web 重指向 jest/dist 耦合带来的活动部件比它消除的约 200 行重复代码更多且生产前后端协议稳定漂移风险低、成本小若日后真被咬到再抽取也不迟。Web 保持不被触碰PR 2 Decision2026-06-26。6.3 双层状态列表走 TanStack Query直播流走 zustand列表数据受益于既有 MMKV 持久化 refetch而流式消息树持有AbortController绝不能持久化因此放在独立的瞬态 zustand storechatSessionStore中。6.4 FlashList v2 非倒置 maintainVisibleContentPosition这是现代 v2 聊天的标准模式流式时吸附底部同时不打扰已经上滑阅读的用户。七、对既有行为的影响移动端全新功能不删除任何既有内容Web行为不变且零改动——移动聊天移植不与 Web 共享代码没有 Web 文件被修改或重指向后端无任何改动。八、延伸阅读设计系列docs/mobile-chat/00-index.md、docs/mobile-chat/01-research.md、docs/mobile-chat/03-detailed-design.md、docs/mobile-chat/04-implementation-plan.md、docs/mobile-chat/05-pr-roadmap.md、docs/mobile-chat/06-unified-chat-surface.md移动端聊天纯 TS 层mobile/src/chat/ndjson.ts、mobile/src/chat/messageTree.ts、mobile/src/chat/chatHistory.ts、mobile/src/chat/messageProcessor.ts、mobile/src/chat/streamingModels.ts后端流式 APIbackend/onyx/server/query_and_chat/chat_backend.py/create-chat-session、/send-chat-message、/chat-session/{session_id}/resume-stream测试佐证mobile/src/chat/tests/ndjson.test.ts、mobile/src/chat/tests/messageTree.test.ts、mobile/src/chat/tests/chatHistory.test.ts【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表