首页
/ Onyx 移动端聊天移植方案研究:将 Web 聊天体验搬到 React Native 的技术选型与架构决策

Onyx 移动端聊天移植方案研究:将 Web 聊天体验搬到 React Native 的技术选型与架构决策

2026-09-09 21:41:44作者:伍希望

本文基于 docs/mobile-chat/01-research.md 展开,系统梳理 Onyx 将 Web 聊天体验移植到移动 App(React Native + Expo + NativeWind) 的完整研究结论:需求边界、Web 端可复用资产盘点、NDJSON 流式协议与消息树等纯 TS 逻辑的源码级分析、业界最佳实践调研,以及三种共享边界方案的对比与最终选型。读完本文,你可以掌握移动端聊天移植的分阶段路线、流式解析器与消息树的移植要点,以及"共享代码边界"如何影响长期维护成本。

需求与范围:什么要移植,什么明确延后

本次移植的目标是把 Onyx Web 端聊天体验完整搬到移动 App 上,技术栈为 React Native 0.85 + Expo SDK 56 + NativeWind。移植范围在 GATE 1 阶段就被锁定为四条明确的能力线:

  • 核心聊天:发送消息 → 流式接收回答 → 渲染 Markdown → 会话/历史列表;
  • Agent 选择:仅"选择某个 Agent 进行对话",不做 Agent 的创建与编辑;
  • Projects(项目):支持选择项目、在项目内聊天、项目管理项目文件(增删文件),不做项目 CRUD;
  • 输入栏文件附件:支持文档(expo-document-picker)与相册图片(expo-image-picker),不做相机拍摄。

纯 TypeScript 逻辑通过 @onyx-ai/shared 共享。整个工程以**可独立合并的阶段(phases)**交付,逐个合并;由于产品尚未进入生产环境,无需考虑向后兼容

需求澄清清单(GATE 1 锁定)

# 问题 答案
1 核心聊天的功能下限 最小健壮核心:发送 → 流式回答 → 渲染 Markdown → 会话/历史列表。引用/来源、Agent 化子步骤(reasoning/search/tool 时间线)、重新生成/编辑/反馈、追问建议、图片生成全部延后到各自独立的后期阶段。
2 Projects 范围 选择 + 项目内聊天 + 项目文件管理(增删文件)。项目创建/重命名/删除延后
3 附件来源 文档(expo-document-picker)+ 相册(expo-image-picker)。相机延后
Agents 仅选择对话,不创建/编辑。
共享抽取策略(长期约定) @onyx-ai/shared 增量成长,仅在复用被证明后抽取(extract-on-proven-reuse),不做一次性的大规模前置抽取。跨平台契约放中性路径(/contracts/types);/native 仅限 React Native 使用。

被移植对象:Web 端"refresh"架构的资产盘点

研究文档通过一次精确的代码扫描,定位了 Web 端聊天功能的完整实现链,这些正是移植的素材:

  • 编排层web/src/refresh-pages/AppPage.tsx 组装三个核心 Hook——web/src/hooks/useChatController.ts(端到端提交)、web/src/hooks/useChatSessionController.ts(会话生命周期 + 恢复)、web/src/lib/agents/hooks.ts(Agent 选择)。
  • 状态层:Zustand 仓库 web/src/app/app/stores/useChatSessionStore.ts,持有按会话区分的 messageTree: Map<nodeId, Message>chatState'input'|'streaming'|'loading'|'uploading'|'toolBuilding')以及每个会话独立的 AbortController
  • 流式发送web/src/app/app/services/lib.tsx 中的 sendMessage()POST /api/chat/send-chat-message 发请求(文档标注确认于 lib.tsx:189)。响应是 NDJSON(换行分隔的 JSON),而非 SSE 帧格式。
  • 解析器web/src/lib/search/streamingUtils.tshandleSSEStream(),用 fetchresponse.body.getReader() + TextDecoder + 缓冲区 split('\n') 逐行解析,并过滤 chat_heartbeat
  • 报文协议:外层包装为 { placement:{turn_index, tab_index?, sub_turn_index?, model_index?}, obj:{type,...} },共 40+ 种报文类型(纯 TS,定义在 web/src/app/app/services/streamingModels.ts)。最小核心只需 MESSAGE_START/DELTA/ENDSTOPERRORMessageResponseIDInfoCreateChatSessionID
  • 纯 TS、与 React 解耦web/src/app/app/services/messageTree.tsupsertMessagesgetLatestMessageChainbuildImmediateMessages 等);核心类型在 web/src/app/app/interfaces.ts
  • 与 React 强耦合(必须在 RN 端重写):报文→展示的变换 …/timeline/hooks/usePacketProcessor.ts;Markdown 渲染 web/src/components/chat/MinimalMarkdown.tsx(react-markdown + rehype);滚动/工具栏/编辑 UI。
  • 后端backend/onyx/server/query_and_chat/chat_backend.py(含 models.pystreaming_models.py),以及 backend/onyx/server/features/projects/api.pybackend/onyx/server/features/persona/api.py

流式协议深挖:NDJSON 报文、解析器与 Web 端实现

报文格式:包装报文 + 根级控制对象

Web 与移动端共用的线上协议不是 SSE,而是 NDJSON:每一行一个 JSON 对象。协议中混有两种形态的对象:

  1. 包装报文(Packet){ placement: { turn_index, tab_index?, sub_turn_index?, model_index? }, obj: { type, ... } }
  2. 根级控制对象:如 MessageResponseIDInfo(带 user_message_id/reserved_assistant_message_id 字段)和流式错误 StreamingError(带顶层 error 字段)。

因此移动端的判别逻辑明确为按字段判别(discriminate by field),而不是看 obj.type。在 mobile/src/chat/streamingModels.ts 中,PacketType 枚举定义了完整的报文类型族,覆盖 message_start/message_delta/message_endstopsection_endtop_level_branchingerror,以及 search/image-generation/python/fetch/custom-tool/file-reader/memory/reasoning/citation/deep-research/coding-agent/bash 等工具报文。核心聊天阶段真正消费的只有前三类加 STOP/ERROR

Web 端解析器:handleSSEStream 的逐行缓冲逻辑

web/src/lib/search/streamingUtils.ts 是移植的参考实现,其核心循环为:

const reader = streamingResponse.body?.getReader();
const decoder = new TextDecoder();
let buffer = "";
// ...信号 abort 时 reader.cancel()
while (true) {
  const { done, value } = await reader?.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop() || "";          // 保留跨块的不完整行
  for (const line of lines) {
    if (line.trim() === "") continue;
    const data = JSON.parse(line);     // 逐行解析
    yield data;
  }
}
// 收尾 flush:解析残留缓冲

要点是:split("\n") 后把最后一段重新放回 buffer,从而跨网络块(chunk)拼接不完整的行;解析失败时还会用正则 line.match(/\{[^{}]*\}/g) 尝试"打捞"扁平 JSON 对象;finallyreader.cancel() 确保提前退出(abort/早退)时释放连接而非只是停止读取。

移动端实现:纯 TS 引擎的本地移植

在最终方案(见下文选型)下,聊天相关代码全部以移动端本地原生实现落地于 mobile/src/chat/ 目录,Web 端保持不动。以下是该目录中的核心模块与源码级解读。

1. NDJSON 行缓冲器:ndjson.ts

mobile/src/chat/ndjson.ts 把 Web 端 handleSSEStream 内部的行缓冲逻辑抽成无平台耦合的纯 TS 工具:

export interface NdjsonBuffer<T> {
  pushChunk(text: string): T[];  // 追加文本,返回本块完成的报文;尾部不完整行留给下次
  flush(): T[];                  // 流结束时解析残留行;不做括号恢复,畸形尾部直接丢弃(与 web 一致)
}

createNdjsonBuffer<T>() 内部持有 buffer 字符串,pushChunk 执行"拼接 → split → 弹出末段回存"的流程,parseLineJSON.parse 失败时同样用 \{(?:[^{}]*)\} 正则打捞扁平 JSON。它的注释明确写道:"No platform coupling — the transport feeds it decoded text and owns the reader/decoder",即传输层(负责 reader/decoder)与解析层(纯行缓冲)职责分离——这正是跨平台复用意图的体现。

2. 传输层:expo/fetch + 流式消费

mobile/src/api/chat/stream.ts 是移动端唯一绕过 apiFetch 的地方——因为只有 expo/fetch 在 RN 上暴露可读的 response.body

import { fetch as expoFetch } from "expo/fetch";
// ...
export async function* streamChatMessage(body: SendMessageBody, signal: AbortSignal) {
  const response = await expoFetch(`${getBaseUrl()}/chat/send-chat-message`, {
    method: "POST",
    headers: { ...(await authHeaders()), "Content-Type": "application/json" },
    body: JSON.stringify(body),
    signal,
  });
  if (!response.ok) await raiseForStatus(response);
  yield* readNdjson(response);
}

readNdjsonresponse.body.getReader() + TextDecoder.decode(value, { stream: true }) 读取,把解码文本喂给 createNdjsonBuffer,按需过滤心跳(isHeartbeat 同时识别包装报文内 obj.type === "chat_heartbeat" 与根级 type === "chat_heartbeat" 两种形态);finallyreader.cancel() 防连接泄漏。resumeChatMessage 则对 GET /chat/chat-session/{id}/resume-stream?cursor=n 做同样的消费(keepHeartbeats=true,把心跳当作静默期活性信号)。SendMessageBody 的字段(messagechat_session_idparent_message_idfile_descriptorsdeep_researchorigin、可选的 allowed_tool_ids/forced_tool_id/internal_search_filters/llm_override)注释清晰地标注了 null 语义与"缺省即用后端默认值"的约定。

3. 消息树:messageTree.ts

mobile/src/chat/messageTree.ts 标注"Ported ~verbatim from web's messageTree.ts;only divergence is the trimmed minimal Message",保留了以下核心函数:

  • upsertMessages(currentMessages, messagesToAdd, makeLatestChildMessage):批量 upsert 节点,并在树为空时自动创建合成系统根节点(SYSTEM_NODE_ID = -3),维护每个父节点的 childrenNodeIdslatestChildNodeId
  • getLatestMessageChain(messages):从根沿 latestChildNodeId 走到当前分支,产出可见消息链;
  • getLastSuccessfulMessageId:从链尾向前找第一个非 error 且有 messageId 的消息,用于计算下一条消息的 parent_message_id(合成根返回 -3,调用方映射为 null);
  • buildImmediateMessages(parentNodeId, userInput, files):用负的临时 nodeId(-1 * Date.now() - offset,避免与后端 messageId 冲突)构造"用户消息 → 空助手消息"两个初始节点,用户节点直接指向助手节点——注释特别说明"每次发送都用新 nodeId,以便编辑时 fork 出兄弟分支"。

消息节点的核心类型定义在 mobile/src/chat/interfaces.tsMessagemessageId?nodeIdparentNodeIdchildrenNodeIds?latestChildNodeId?typeuser|assistant|system|error)、messagefilespackets,以及移动端追加的 streamingStartedAt(流式计时锚点)与 processingDurationSeconds(会话恢复后显示"Thought for Xs")。ChatState'input'|'loading'|'streaming'|'uploading'

4. 会话历史重建:chatHistory.ts

mobile/src/chat/chatHistory.tsprocessRawChatHistory(rawMessages, packets)GET 会话详情返回的 messages[]packets[][](按助手消息序号对齐,一个列表对应一次助手轮次)重建为消息树:复用 message_idnodeId,出错轮次用 error 文本渲染并标记为 type: "error",用 parent_message/latest_child_message 重建父子关系并对子节点排序,同时把 processing_duration_seconds 写入节点。

5. 编排:useChatController 与流式刷新节奏

移动端有自己的编排 Hook mobile/src/hooks/useChatController.ts,结构上对齐 Web 版但完全本地实现:

  • runChatStream 为模块级函数(避免登录页卸载后中断流),消费 streamChatMessage 的异步生成器,用 50ms 批量 flushFLUSH_INTERVAL_MS,定义于 mobile/src/chat/constants.ts,注释说明 send 与 resume 共用同一节奏)累积 Packet[] 后一次性 upsertMessages
  • 首个 packet 到来时把 chatState 置为 streamingMessageResponseIDInfo 回填用户/助手消息的真实 id;流式错误把助手轮次直接转为 error 节点,避免占位符"…"卡死;
  • 用户停止走 AbortController + 调用 POST /api/chat/stop-chat-session/{id}(客户端 abort 只断流,后端仍需显式停止),abort 且无错误时用 buildUserCancelledStopPacket 补一个 stop_reason: "user_cancelled" 的 STOP 报文(否则该轮次在重载前会一直显示"正在流式");会话 id 为空时先 createChatSession(personaId, projectId),成功后才清空草稿,失败则草稿(文本+文件引用)得以保留。

会话 / Agent / 项目 / 附件:移动端要对接的 API 面

研究文档给出了移植所需的最小 API 契约清单,与 mobile/src/chat/contracts/ 下的 documents.tsprojects.tsmobile/src/api/ 中的实现相呼应:

  • 会话POST /api/chat/create-chat-session,body 为 {persona_id(=0 默认), description, project_id},返回 {chat_session_id};详情接口返回 messages[] + packets[][](供重放)+ current_run;恢复进行中的流:GET /api/chat/chat-session/{id}/resume-stream?cursor=n;停止:POST /api/chat/stop-chat-session/{id}
  • AgentsGET /api/personaMinimalAgent[]id,name,description,tools[],starter_messages,icon_name,uploaded_image_id,builtin_persona,is_public,is_featured,display_priority)。选择是隐式的——会话携带 persona_idsendMessage 没有 agent 参数;默认 persona id=0。类型对应 web/src/lib/agents/types.ts,移动端有 mobile/src/chat/agents.ts(含 DEFAULT_AGENT_ID)。
  • ProjectsGET /api/user/projectsProject[] {id,name,instructions,chat_sessions[]}ChatSession 携带 project_id;文件为多对多关系。上传 POST /api/user/projects/file/upload(multipart,字段 files、可选 project_id/temp_id_map)→ {user_files: ProjectFile[], rejected_files[]}ProjectFile {id,file_id,name,status,chat_file_type,token_count,…}UserFileStatus 枚举;关联/解绑为 POST/DELETE /api/user/projects/{pid}/files/{fid}。Web 端服务层为 web/src/app/app/projects/projectsService.ts,状态层为 web/src/providers/ProjectsContext.tsx(乐观 temp_id + 3 秒状态轮询)。
  • 附件FileDescriptor {id, type: ChatFileType, name?, user_file_id?}ChatFileType 枚举(image/document/plain_text/tabular/user_knowledge);纯辅助函数 projectFilesToFileDescriptors() 位于 web/src/app/app/services/fileUtils.ts。发送报文携带 file_descriptors[]发送被门控——直到上传的文件完成索引(token_count != null)才允许发送;图片预览走 GET /api/chat/file/{file_id}

移动端已有的复用基础

移植不是从零开始,移动工程已具备以下地基(研究文档逐一给出精确路径):

  • 导航mobile/src/app/_layout.tsx 组合了 PersistQueryClientProvider + SidebarProvider + AuthGate + expo-router Stack;鉴权路由组在 mobile/src/app/(auth);新的鉴权聊天路由组可并排插入。
  • HTTPmobile/src/api/client.tsapiFetch<T> 自动注入 Bearer token、把错误归一为 ApiError、base URL 从 session.serverUrl 惰性读取(mobile/src/api/config.ts);查询键集中在 mobile/src/api/query-keys.ts(按 serverUrl 区分)。
  • 服务端状态:TanStack Query 持久化到 MMKV,通过 dehydrateOptions 剔除 PII(mobile/src/query/client.ts);鉴权/会话:mobile/src/api/auth/sessionManager.tsmobile/src/api/auth/tokenStore.ts(secure-store 支撑的 getToken/setToken)、mobile/src/hooks/useCurrentUser.tsmobile/src/state/session.ts(zustand)。
  • UI 原语mobile/src/components/ui(text/button/text-input/icon/separator)、侧栏 mobile/src/components/sidebar、图标 mobile/src/iconszustand@shopify/flash-list@2.0.2react-native-reanimated@4react-native-workletsexpo-dev-client 均已就位。
  • 共享包@onyx-ai/shared 源码在 web/lib/shared/src,目前较薄(types/dto.tstypes/enums.tscontracts/utils/),子路径导出 ./types ./contracts ./utils;移动端通过 file: 依赖消费,共享代码变更后必须重建 dist

业界最佳实践(2025–2026 调研结论)

研究文档结合 2025–2026 年的公开资料,沉淀了五条可直接落地的工程结论:

  1. 流式传输:用 expo/fetch(SDK 56 的默认全局 fetch),不要用 RN 旧版基于 XHR 的 fetch(没有 response.body)。POST + Bearer + JSON,消费 response.body.getReader() + TextDecoder.decode(value,{stream:true}) + 缓冲 split('\n'),保留尾部不完整行,结束时 flush。这与 Web 解析器几乎逐字一致,因此 NDJSON 行解析层可以做成可共享的纯 TS。状态更新按约 50ms 批量提交,行做 memoize,卸载/abort 时 reader.cancel()不要用 react-native-sse——协议是 NDJSON 而非 SSE 帧。
  2. 消息列表@shopify/flash-list v2(已在依赖中),非反转(non-inverted)maintainVisibleContentPosition={{ startRenderingFromBottom:true, autoscrollToBottomThreshold:0.2 }};通过 onStartReached 分页加载更早消息;注意短列表场景的边界(#2050)。注意:startRenderingFromBottom / autoscrollToBottomThresholdFlashList v2 自己扩展的 maintainVisibleContentPosition(与 RN 核心 ScrollView 只支持 minIndexForVisible / autoscrollToTopThreshold 不同)。
  3. Markdownreact-native-markdown-display 已无人维护。可选方案:react-native-streamdown(Software Mansion 出品,通过 worklets 处理流式中的部分/未闭合 Markdown,需要 dev build 并在 RN 0.85 上做兼容性 spike)或 react-native-marked(纯 JS,安全回退)。无论选哪个库,块级 memoization 都是流式性能的关键
  4. 键盘/输入react-native-keyboard-controller(用 KeyboardStickyView 实现吸底且随内容增长的输入栏)——需要 dev build(已有 expo-dev-client)+ Reanimated(已在依赖中)。
  5. 文件expo-document-pickercopyToCacheDirectory:true)+ expo-image-pickermediaTypes:['images'])。上传用新版 expo-file-systemFile(uri).createUploadTask(url,{uploadType:MULTIPART, fieldName:'files', headers:{Authorization}, onProgress})——直接从磁盘流式读取,规避 iOS FormData 2–3 倍内存导致 OOM 的问题;注意非 2xx 状态会 resolve(需要自己检查 status)。把每个资源归一化为 {uri,name,mimeType,size} 并提供回退;用 useMutation + 小型 zustand 进度仓库驱动。

三种共享边界方案:对比与权衡

三条路线共享同一个纵向切片阶段路线图(地基 → 共享解析器/契约 → 核心聊天 → 会话/历史 → Agents → Projects → 项目文件 → 输入附件 → 延后的富聊天),它们的差异只在于共享抽取边界:Web 聊天纯 TS 核心有多少进入 @onyx-ai/shared,Web 是否被重构去消费它。

方案 A —— 简单优先:"薄契约,胖移动端"

只共享今天就能证明完全一致的部分:报文类型契约(裁剪后的 streamingModels.ts)与纯 NDJSON 行解析器(handleSSEStream 的 buffer/split 核心,不含 reader)。DTO 类型(FileDescriptor/枚举、MinimalAgent、Project/ProjectFile)随各阶段按需增量进入 shared。移动端复制其实际用到的约 4 个消息树函数(Web 的 messageTree.ts 携带多模型/Agent 化冗余),并在本地重写 packet→display reducer(核心只处理 MESSAGE_*/STOP/ERROR)。Web 仅被改动为导入共享的报文类型 + 解析器(低风险,Web 测试覆盖)。

  • 收益:共享面最小、最贴合策略、单 PR 风险最低;移动端可自由使用 RN 原生惯用法。
  • 代价:两套解析器 + 两套 reducer + 两份消息树副本 → 后端协议变化时存在静默漂移风险;移动端可能重踩 Web 已修复的 bug。

方案 B —— 健壮/复用优先:"胖共享聊天引擎"

把 Web 的整个纯 TS 核心抽进新的 @onyx-ai/shared/chat 子路径:全部报文类型、NDJSON 解析器(置于 ChatTransport 接口之后)、messageTree.tspacketProcessor.ts 的 reducer、buildSendMessageBodyprocessRawChatHistory重构 Web 去消费它(用薄 re-export 垫片保持 Web 导入路径不变)。移动端只需实现一个平台原语——基于 expo/fetch 的 ChatTransport——外加 RN 渲染层与薄 store/query 胶水。

  • 收益:真正的单一事实来源——后端报文变更只改一个文件,两端同时跟进;移动端各阶段几乎纯渲染工作;引擎用假 transport 即可轻松做单元测试。
  • 代价违反 extract-on-proven-reuse 既定策略(在复用被证明之前就抽取);在移植 PR 里改动正在运行的 Web 流式代码(给线上 Web 增加回归面);Web + 移动端耦合到共享 dist 的重建;为延后功能前置了抽象。

方案 C —— 灵活优先:"混合接缝"(推荐)

共享跨平台契约(chat/streaming/files/agents/projects 类型)以及真正零依赖、漂移代价最高且迁移成本低的纯辅助函数:NDJSON 行缓冲解析器、messageTree 的 upsert/遍历、processRawChatHistoryprojectFilesToFileDescriptors抽取与 React 耦合的 usePacketProcessor移动端自持编排 Hook + zustand store + 渲染层 + 薄 packet→display 映射(核心 = "拼接 MESSAGE_* 的内容")。移植 PR 中不改动 Web——Web 导入仅在文件已经是纯 TS 的时机按阶段机会主义地重指到 shared(机械性 import 替换)。

  • 收益:恰好共享高价值/低成本/高漂移风险的片段(两端说同一协议,这些片段按构造就相同 = 已被证明的复用,而非投机);保留 RN 原生渲染与编排;不做强制的大爆炸 Web 重构;符合既定策略。
  • 代价:packet→display 逻辑存在两处(Web Hook + 移动端映射)——这个取舍被接受,因为该片段确实与 React 耦合且核心映射微不足道;存在一定编排控制流的重复。

横向对比结论

维度 A B C
漂移风险(后端协议变更触及的代码库数) 2(解析器、reducer、树均有副本) 1 ~1.5(解析器/树/契约共享,薄展示映射重复)
对线上 Web 的风险 ≈ 无(仅 import 替换或不动) 真实存在(重构流式路径,靠 re-export 垫片 + Web e2e 缓解) ≈ 无
单 PR 规模 / 可合并性 各阶段 ≤700 LOC 干净可合 早期共享引擎 + Web 重构阶段最大 各阶段 ≤700 LOC 干净可合
策略契合度(extract-on-proven-reuse) 契合 明确违背 契合
阶段路线图 三方案一致 三方案一致 三方案一致

值得强调的一点:阶段路线图在三个方案下完全相同——选择的差异不会重塑路线图,主要影响的是 Phase 2 的内容Web 被改动的程度。此外,所有方案共担的共享 spike 风险:在真机 build(RN 0.85 / SDK 56)上确认 expo/fetch 暴露 response.body.getReader();确认 react-native-streamdown 能在 RN 0.85 上构建(回退方案 react-native-marked);两者都需要 dev build(已有 expo-dev-client)。

最终选型:方案 C,以及 PR 2 的关键修订

选定方案 C —— Hybrid Seams(GATE 1 通过)。

但研究文档随后记录了一次重要修订(2026-06-26,PR 2):聊天相关的共享边界被整体放弃。NDJSON 解析器、契约/类型、messageTreeprocessRawChatHistoryprojectFilesToFileDescriptors 全部原生写在移动端,Web 保留自己的副本。理由写得很直白:共享包机制(util + Web 重指 + jest mapper + dist 耦合)产生的活动部件,比它省下的约 200 行重复代码还要多;且产品未上线、协议稳定,未来修漂移成本很低。Web 完全不被改动。(详细决策记录见 docs/mobile-chat/05-pr-roadmap.md 中的 PR 2 Decision。)

修订后的最终边界划分:

  • 进入 @onyx-ai/shared聊天相关零内容(修订后)。(原计划:NDJSON 解析器 + 契约 + messageTree + processRawChatHistory + projectFilesToFileDescriptors 增量进入——现全部移动端本地实现,Web 不动。)
  • 仅存于移动端:expo/fetch 传输层、zustand 聊天会话 store、移动端编排 Hook(useChatController/useChatSessionController)、chat/streaming/file 契约 + messageTree + processRawChatHistory + fileDescriptors(现为移动端原生,位于 mobile/src/chat/)、薄 packet→display 映射(核心 = 拼接 MESSAGE_* 内容)、全部 RN UI、expo pickers + expo-file-system 上传。
  • 仅存于 WebusePacketProcessor + transformers、MinimalMarkdown、完整工具/引用报文动物园、SWR hooks。Web 保留现有副本;导入仅在文件已是纯 TS 时按阶段机会主义地重指 shared——不做大爆炸式 Web 重构
  • 延后的富聊天功能(引用、Agent 化时间线、重新生成/编辑/反馈、追问、图片生成)各自作为独立的后期阶段落地,届时再把相关报文类型加入 shared + 扩展移动端映射 + 补充 RN UI。

落地启示:这份研究如何指导后续阶段

从源码证据看,最终落地形态与研究报告完全吻合:mobile/src/chat/ndjson.tsmessageTree.tschatHistory.tsstreamingModels.tsfileDescriptors.ts 等均为移动端本地纯 TS 实现,配套 mobile/src/chat/__tests__/ndjson.test.tsmessageTree.test.tschatHistory.test.tsfileDescriptors.test.ts 等测试覆盖;mobile/src/hooks/useChatController.tsmobile/src/api/chat/stream.ts 完成了编排与传输。这份研究报告的价值在于:它把"移植"从"照搬 UI"重新定义为"界定协议级契约 + 最小健壮核心 + 分阶段扩展"的工程决策——先锁死需求边界(哪些延后),再盘点可复用资产(哪些纯 TS、哪些 React 耦合),再用共享边界的三方案对比回答"哪些代码该共享、什么时候共享",最终以"移动端原生、Web 不动"的务实修订收尾,为后续每个可独立合并的阶段(docs/mobile-chat/04-implementation-plan.md)提供了清晰的决策依据与风险清单。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
899
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525