ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Onyx 移动端聊天移植详细设计:客户端数据模型、NDJSON 流式解析与消息渲染器注册表架构

Onyx 移动端聊天移植详细设计:客户端数据模型、NDJSON 流式解析与消息渲染器注册表架构 Onyx 移动端聊天移植详细设计客户端数据模型、NDJSON 流式解析与消息渲染器注册表架构【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer本文是 Onyx 移动端聊天移植Mobile Chat Port系列的详细设计文档承接 高层设计 的端到端流程聚焦于客户端数据模型不涉及任何后端改动、纯 TypeScript 聊天数据层NDJSON 解析器、消息树、历史重建以及可扩展的消息渲染器注册表。读者将掌握为什么聊天状态必须拆成临时 zustand 持久化 TanStack Query两层、createNdjsonBuffer与expo/fetch流式传输如何协作、渲染器注册表如何让后续富聊天功能以增量注册而非核心重写的方式落地以及整个移植对 web 端和onyx-ai/shared共享包的零触碰边界。设计定位与一条关键决策覆盖设计文档的地图定位按项目所有者要求本文档刻意保持高层级high-level——它是移植工作的地图map而非逐行的实现规格line-by-line spec。每个 PR 阶段见 05-pr-roadmap.md在编码前都会针对该阶段进行独立的详细分析会话与所有者逐项确认。因此文中列出的文件清单与形态是预期结构会在各阶段实施时被确认或细化。⚠️ 决策覆盖2026-06-29聊天逻辑不共享本设计最重要的前提是 2026-06-29 的决策覆盖DECISION OVERRIDE项目所有者推翻了 Approach C 中共享聊天代码接缝的规划——任何与聊天相关的内容都不会被抽取到onyx-ai/shared共享包。具体来说本文档中凡是出现web/lib/shared/src/contracts/*或web/lib/shared/src/utils/*如ndjson.ts、messageTree.ts、chatHistory.ts、streaming.ts、chat.ts、files.ts、agents.ts、projects.ts、fileDescriptors.ts之处均应读作mobile/src/chat/*——移动端自有web 端不重指向、无 shimShared message-tree fns、SharedcreateNdjsonBuffer() 以及共享dist/watch.mjs集成说明均被取代移动端拥有这些副本web 端保持原样onyx-ai/shared继续只接收跨平台的设计原语design primitives。这一决策已在 PR 2 中落地实现文件位于mobile/src/chat/下streamingModels.ts、interfaces.ts、ndjson.ts、messageTree.ts、__tests__/等。其动机与完整推理记录在 05-pr-roadmap.md 的 PR 2 章节共享包机制工具函数 web 重指向 jest mapper dist 构建耦合比它消除的约 200 行重复代码更复杂且产品尚未投产pre-production后端协议稳定漂移风险低且后续重新抽取成本低。数据库设计零后端改动本移植不涉及任何后端或数据库变更N/A —— 移动端客户端与现有 Onyx 后端及其现有 schema 通信。所有数据模型工作都在客户端完成内存中的 zustand TanStack Query 缓存外加已配置好的 MMKV 持久化。相关后端表chat_session.project_id、user_file、persona、project、Project__UserFile均已存在其结构说明见 01-research.md。这意味着本移植是一个纯客户端项目可以独立验证、独立合并不触碰服务端任何代码。客户端数据模型三层状态架构客户端状态被刻意拆分为三个各司其职的部分这也是 高层设计 中两层状态决策的具体化。临时聊天状态 ——chatSessionStorezustand绝不持久化chatSessionStore镜像 web 端useChatSessionStore的形态但裁剪到锁定核心范围无多模型、无重新生成、无排队消息、无文档侧栏字段。其字段设计如下字段形态设计理由currentSessionIdstring \| null指明当前打开屏幕渲染的是哪个会话sessionsMapsessionId, SessionData按会话隔离保证一个流不会写进另一个会话SessionData.messageTreeMapnodeId, Message移动端原生Message类型会话的对话内容只能通过移动端原生的upsertMessages变更SessionData.chatStateinput \| loading \| streaming \| uploading驱动输入栏、loading 指示器与发送按钮的门控SessionData.abortControllerAbortController停止/卸载时取消流不可序列化 → store 绝不能持久化SessionData.submittedMessagestring用户节点落地前的乐观回显端口并裁剪后的 actionssetCurrentSession、createSession、updateSessionAndMessageTree、updateChatState、setAbortController、abortSession。在仓库中该 store 已实现于 mobile/src/state/chatSessionStore.tsSessionData接口与文档定义一致messageTree、chatState、abortController、submittedMessage并通过ensureSession/hydrateSession/updateSessionTree/patchNode等 action 操作。文件头注释明确写着Ephemeral per-session chat state. NEVER persisted: holds live AbortControllers. The Map-per-session is what stops one stream writing into another.——与本文档的绝不持久化约束完全对应。服务器态列表 —— TanStack Query持久化到 MMKVPII 排除列表类服务器状态走 TanStack Query复用移动端已有的 MMKV 持久化与失效机制。查询键在 mobile/src/api/query-keys.ts 中扩展全部以serverUrl为键前缀切换后端实例时不会串数据chatSessions(serverUrl)chatSession(serverUrl, id)agents(serverUrl)projects(serverUrl)projectFiles(serverUrl, projectId)仓库实现与此一致例如chatSessions: (serverUrl) [chat-sessions, serverUrl]、chatSession: (serverUrl, sessionId) [chat-session, serverUrl, sessionId]。附件上传进度 ——uploadStorezustand临时态独立附件上传进度单独存放在一个独立的临时 zustand store中键为客户端临时文件 id值为{ uri, name, mimeType, size, status, bytesSent, totalBytes }。它被输入栏与项目文件页共同消费与聊天状态解耦避免上传进度污染消息树的重渲染。接口设计函数 类型 Hooks无类整个新面是函数 类型 Hooks不引入任何 class。关键接缝seams如下。移动端原生createNdjsonBufferT()定义于 mobile/src/chat/ndjson.ts是流式解析的核心纯函数接口为pushChunk(text: string): T[]与flush(): T[]泛型参数为调用方的包类型内部持有跨 chunk 的部分行partial-line字符串按\n切分保留行尾残缺部分留待下次拼接逐行对完整行执行JSON.parse内置 web 端的花括号恢复brace-recovery回退单行解析失败时尝试用/\{[^{}]*\}/g提取扁平非嵌套JSON 对象以挽救可恢复数据见parseLine实现不含任何fetch/TextDecoder——传输层职责留在外部transport 负责提供已解码文本镜像 web 端handleSSEStream的解析核心去掉了 reader不共享PR 2 Decision2026-06-26。export function createNdjsonBufferT unknown(): NdjsonBufferT { let buffer ; return { pushChunk(text: string): T[] { buffer text; const lines buffer.split(\n); buffer lines.pop() ?? ; // 保留行尾残缺部分 const out: T[] []; for (const line of lines) { if (line.trim() ) continue; parseLine(line, out); } return out; }, flush(): T[] { /* 解析流末尾遗留的残缺行 */ }, }; }配套单元测试位于 mobile/src/chat/tests/ndjson.test.ts。移动端streamChatMessage(body, signal): AsyncGeneratorPacket定义于 mobile/src/api/chat/stream.ts负责流式传输使用expo/fetch发起 POST整个应用中唯一不走apiFetch的 HTTP 调用因为它需要可读的字节流response.body持有getReader()TextDecoder解码循环逐 chunk 喂给移动端原生的 NDJSON buffer过滤chat_heartbeat心跳包在 abort /finally中调用reader.cancel()释放连接防止泄漏见readNdjson的 finally 块Release the reader on early-return/abort, or the connection leaks.。同一文件还实现了resumeChatMessage(sessionId, cursor, signal)用于恢复进行中的 run先从cursor位置重放缓冲再尾随tail实时事件resume 场景下保留心跳作为静默期 liveness 信号keepHeartbeats true。另外StreamHttpError携带 HTTP status让 resume 调用方在预期的无内容可恢复404场景下保持静默。SendMessageBody的字段在源码中也有详细注释其中值得注意的约定parent_message_id: number | null——null表示首条消息否则为最后一条助手消息 idallowed_tool_ids/forced_tool_id/internal_search_filters省略或为 null 时使用后端默认允许所有工具、不强制任何工具、无来源过滤模型覆盖字段是单数名llm_override——llm_overrides多模型对比用是另一个字段移动端不使用。移动端包渲染器注册表renderer registry这是本文档强调的可扩展性基石在 PR 3 中构建即使当时只随核心发布一个渲染器。它镜像 web 端的renderMessageComponent.tsxMessageRendererTPacket,TState契约MessageRendererTPacket, TState移动端契约——{ matches(packetType): boolean; reduce(state, packet): TState; render(state): ReactNode }。因为返回 RN 节点React Native 耦合所以移动端自有、不共享其中的*包分组packet-grouping*步骤未来若证明有复用价值可以再共享findRenderer(packetType): MessageRenderer | null——分发函数web 端findRenderer的移动端等价物核心PR 3只注册一个渲染器MessageTextRendererMESSAGE_START/DELTA/END→ markdown 字符串 isComplete/error。它的代码量约等于一个扁平拼接器不会扩大核心范围usePacketDisplay(node)——对某个节点的包分组后遍历注册表产出渲染输出。延后的富聊天 PR9a–9e只需向注册表添加渲染器agentic 步骤则再加一个时间线组合层——无需重写核心。web 端 React 耦合的usePacketProcessor保持 web-only。移动端原生消息树函数从 web 端messageTree.ts移植针对最小化移动端Message编写依 PR 2 最小共享决策不共享web 保留自己的副本upsertMessages、getLatestMessageChain、getMessageByMessageId、buildImmediateMessages、buildEmptyMessage常量SYSTEM_NODE_ID与类型MessageTreeState在仓库中mobile/src/chat/messageTree.ts 的upsertMessages完整实现了树为以合成 system 节点为根、通过latestChildNodeId分支的MapnodeId, Message这一结构首次插入时若不存在根节点会创建一个SYSTEM_MESSAGE_ID -3的 dummy system 节点见 mobile/src/chat/messageTree.ts并通过updateParentInMap维护父节点的childrenNodeIds与latestChildNodeId语义makeLatest强制、唯一子节点或新添加子节点时成为 latest。processRawChatHistory则位于 mobile/src/chat/chatHistory.ts将后端的messages[]packets[][]按助手消息序数对齐结构性地重建为消息树——只做结构转换不涉及渲染。值得注意的细节packets按助手消息序数对齐每个助手轮次一个列表出错轮次的文本在error字段中渲染为消息文本web 对齐消息type置为errornodeId直接复用message_id只要求唯一性processing_duration_seconds从后端回填作为Thought for Xs头部展示而流式进行中的轮次则用streamingStartedAtepoch ms驱动时间线计时器。配套单元测试位于 mobile/src/chat/tests/chatHistory.test.ts 与 mobile/src/chat/tests/messageTree.test.ts。新文件清单共享包web/lib/shared/src/无新增依 PR 2 决策聊天纯层为移动端原生任何聊天相关内容都不会进入onyx-ai/shared。移动端原生聊天层mobile/src/chat/整个聊天纯层都在移动端不共享文件职责ndjson.tscreateNdjsonBufferT()——纯行缓冲 NDJSON 解析器镜像 webhandleSSEStream解析核心contracts/streaming.ts最小化Packet包装、Placement、PacketType核心子集、MessageStart/Delta/End、Stop/StopReason、PacketError、ChatHeartbeat、MessageResponseIDInfo。MessageStart.final_documents类型为unknown[] \| null不带OnyxDocument。富包类型按各自阶段后续添加contracts/chat.tsMessage最小化、ChatState、ChatSession/BackendChatSession/BackendMessage、发送消息请求体类型、创建会话请求/响应contracts/files.tsFileDescriptor、ChatFileType、UserFileStatuscontracts/agents.tsMinimalAgent选择子集、AgentStarterMessagecontracts/projects.tsProject、ProjectFile、CategorizedFiles、RejectedFilemessageTree.ts树 upsert/遍历/构建器——从 web 移植并针对最小化MessagechatHistory.tsprocessRawChatHistory——后端 messagespackets → 树仅结构fileDescriptors.tsprojectFilesToFileDescriptors 类型检测PR 8仓库实际落地时契约层位于 mobile/src/chat/contracts/含documents.ts、projects.ts另有streamingModels.ts、interfaces.ts、agents.ts、fileDescriptors.ts、constants.ts等文件以及__tests__/目录下的ndjson.test.ts、messageTree.test.ts、chatHistory.test.ts、fileDescriptors.test.ts、agents.test.ts等单元测试。移动端应用层mobile/src/文件职责app/(app)/_layout.tsxAuthGate下的 Authed Stack承载聊天组app/(app)/index.tsx新聊天 / 聊天首页空态、starter promptsapp/(app)/chat/[id].tsx聊天会话页消息列表 输入栏app/(app)/history.tsx会话/历史列表app/(app)/projects/index.tsx、projects/[id].tsx项目列表 项目详情聊天 文件state/chatSessionStore.ts临时逐会话 zustand store见上state/uploadStore.ts附件上传进度见上api/chat/stream.tsexpo/fetch流式生成器见上api/chat/sessions.tsTanStack Query hooks创建/获取/列表/重命名会话api/chat/agents.tsGET /api/personahookapi/chat/projects.ts项目列表/详情/文件 关联/取消关联 hooksapi/files/upload.tsexpo-file-systemcreateUploadTaskmultipart 上传器hooks/useChatController.tsonSubmit、驱动流、~50ms 批量 flush、停止hooks/useChatSessionController.ts加载会话 → 水合消息树恢复进行中的 runhooks/usePacketDisplay.ts分组节点包 遍历渲染器注册表产出渲染输出components/chat/renderers/registry.tsMessageRenderer契约 findRenderer分发镜像 webrenderMessageComponent。核心只注册MessageTextRenderercomponents/chat/renderers/MessageTextRenderer.tsx唯一核心渲染器MESSAGE_*→StreamingMarkdown。富渲染器9a–9e稍后在其旁注册components/chat/MessageList.tsxFlashList v2 非反转、maintainVisibleContentPosition、onStartReached分页、memoized 行components/chat/MessageRow.tsx用户 vs 助手气泡按(nodeId, packetCount)memoize经usePacketDisplay渲染components/chat/AgentTimeline.tsx延后——在 PR 9b 首次构建agentic 时间线渲染器的组合层镜像 webAgentTimeline/TimelineRendererComponentcomponents/chat/StreamingMarkdown.tsxRN markdown接口后面是 streamdownmarked 作为回退块级 memoizecomponents/chat/InputBar.tsxKeyboardStickyView增长式输入 发送/停止后续附件 chipscomponents/chat/AgentPicker.tsxBottom-sheet 智能体列表avatar/name/description/starterscomponents/chat/AttachmentChips.tsx已选文件 chips 状态 移除icons/*任何新图标回形针、停止等仓库中编排 hook 已实现于 mobile/src/hooks/useChatController.ts其开头注释说明了一个关键工程细节runChatStream是模块级作用域的因此当页面跳转进入/chat/[id]导致首页卸载后流仍能按sessionId继续写入。FLUSH_INTERVAL_MS约 50ms 批量 flush 间隔来自 mobile/src/chat/constants.ts。文件结构目录树web/lib/shared/src/ 无聊天改动——聊天相关内容绝不进入 shared web/src/ 原样不动——streamingUtils.ts / messageTree.ts / fileUtils.ts 保持 web 自有 mobile/src/ ├── app/ │ ├── _layout.tsx 修改挂载 (app) 组 │ └── (app)/ 新增 _layout · index · chat/[id] · history · projects/* ├── chat/ 新增移动端原生纯层 │ ├── ndjson.ts (新增) contracts/ (新增) streaming · chat · files · agents · projects │ ├── messageTree.ts (新增) chatHistory.ts (新增) fileDescriptors.ts (新增) │ └── *.test.ts 新增 ndjson tree history 单元测试 ├── state/ 新增 chatSessionStore.ts · uploadStore.ts ├── api/ │ ├── query-keys.ts 修改新增 chat/agents/projects 键 │ ├── chat/ 新增 stream.ts · sessions.ts · agents.ts · projects.ts │ └── files/ 新增 upload.ts ├── hooks/ 新增 useChatController · useChatSessionController · usePacketDisplay └── components/chat/ 新增 MessageList · MessageRow · StreamingMarkdown · InputBar · AgentPicker · AttachmentChips集成点Auth / HTTP列表类调用复用 mobile/src/api/client.ts 的apiFetch注入 bearer ApiError归一化流式调用api/chat/stream.ts复用getBaseUrl()mobile/src/api/config.tstokenStore.ts的 token但直接使用expo/fetch——这是唯一例外路径因为apiFetch的 JSON 语义无法承载可读字节流。导航(app)组挂载在 mobile/src/app/_layout.tsx 现有的AuthGate之下侧边栏mobile/src/components/sidebar呈现会话/项目入口。查询缓存PII 排除必做而非可选扩展 mobile/src/api/query-keys.ts 并复用 mobile/src/query/client.ts 的持久化客户端。由于聊天内容天然敏感聊天会话列表与会话详情/消息的查询键必须在 PR 1 中加入dehydrateOptions的 PII 排除列表与现有me排除项并列在任何聊天历史被持久化到 MMKV 之前完成。后果是聊天历史不会缓存到磁盘启动时重新拉取——这是正确的 PII 安全默认值镜像me排除模式。仓库中该机制已存在mobile/src/query/client.ts 的dehydrateOptions.shouldDehydrateQuery首先检查isNonPersistedKey(query.queryKey)mobile/src/query/tests/client.test.ts 中的测试断言了chat-session相关键与me、项目查询、最近文件查询一样绝不持久化到未加密磁盘缓存而auth-type等非 PII 成功查询可以持久化。无 web 改动移动端聊天移植不触碰任何 web 文件。web/src/lib/search/streamingUtils.ts、web/src/app/app/services/messageTree.ts、web/src/app/app/services/fileUtils.ts保持 web 自有移动端在mobile/src/chat/中原生重实现了解析器、消息树与文件描述符逻辑。无共享构建耦合聊天相关内容不进入onyx-ai/shared因此本移植不依赖共享包的 dist 重建 /file:重新链接。onyx-ai/shared继续保留其现有的 design-token、interactive/typography、numbers/format面。实施前的重要注意事项先做降险 spikePhase 0 / 早期expo/fetchspike确认在设备 dev buildRN 0.85 / SDK 56上response.body.getReader()可用——回退方案是 XHR 进度事件喂入同一个NDJSON buffer该 spike 已通过getReader()在 iOS 模拟器上工作正常记录于 05-pr-roadmap.mdreact-native-streamdownspike确认能在 RN 0.85 / Reanimated v4 上构建——回退方案为react-native-marked两者都保留块级 memoize。两者都需要dev client已有expo-dev-client不能用 Expo Go。绝不持久化chatSessionStore它持有AbortController与实时流必须与持久化的 TanStack 缓存严格分离重启后通过GET get-chat-session 移动端原生processRawChatHistory重新水合会话。流式性能杠杆~50ms 批量 flush行按packetCount而非数组身份memoize——这是最大的流式性能杠杆移植 web 的stillCurrent/abort 守卫保证后台化的流不会写入错误的会话源码层面由chatSessionStore的按会话Map与模块级runChatStream协同实现。发送门控阻塞发送直到附件文件完成索引token_count ! null对FAILED/卡住状态给出可见提示而不是无限阻塞3s 状态轮询镜像 webProjectsContext的模式。保持移动端contracts/streaming.ts最小化现在只放核心包类型富类型留在各自的延后阶段添加。在引文citations落地之前避免把 web 端完整的OnyxDocument形态拖进来。渲染器基础在 PR 3渲染器作为后续在 PR 3 构建MessageRenderer契约 findRenderer分发只注册MessageTextRenderer使富聊天功能成为增量注册而非核心重写。Agentic 时间线组合层AgentTimeline本身也延后——第一个时间线渲染器 PR9b才构建它它插入的分发接缝已在 PR 3 存在。不要在核心中构建富渲染器只构建接缝。智能体/项目选择是隐式的由会话创建时的persona_id/project_id携带没有逐消息的 agent 参数。后端不支持为已有会话选择智能体与 web 一致——需要新开会话。错误处理ERROR/PacketError包将助手节点置为错误状态OnyxError风格的消息只在后端抛出客户端呈现ApiError消息。这是纯客户端工程不涉及HTTPException考量。expires/ Celery不适用本移植未引入任何后端任务。小结一张可以照着实施的地图本详细设计文档的价值在于给出了无后端改动前提下的完整客户端蓝图三层状态架构临时 zustand / 持久化 Query / 独立上传进度、移动端原生的纯聊天数据层NDJSON 解析器 消息树 历史重建、以及一个让未来 9a–9e 富聊天功能全部以加一个渲染器方式落地的渲染器注册表。配合仓库中已落地的 ndjson.ts、stream.ts、messageTree.ts、chatHistory.ts、chatSessionStore.ts 与 PII 排除测试 client.test.ts无论是继续阅读 实施计划 与 PR 路线图还是对照 统一聊天面设计本设计都能为每个阶段提供稳定的接缝与验收基准。【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表