Onyx 移动端聊天移植方案研究:将 Web 聊天体验搬到 React Native 的技术选型与架构决策
本文基于 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.ts的handleSSEStream(),用fetch的response.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/END、STOP、ERROR、MessageResponseIDInfo、CreateChatSessionID。 - 纯 TS、与 React 解耦:
web/src/app/app/services/messageTree.ts(upsertMessages、getLatestMessageChain、buildImmediateMessages等);核心类型在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.py、streaming_models.py),以及backend/onyx/server/features/projects/api.py、backend/onyx/server/features/persona/api.py。
流式协议深挖:NDJSON 报文、解析器与 Web 端实现
报文格式:包装报文 + 根级控制对象
Web 与移动端共用的线上协议不是 SSE,而是 NDJSON:每一行一个 JSON 对象。协议中混有两种形态的对象:
- 包装报文(Packet):
{ placement: { turn_index, tab_index?, sub_turn_index?, model_index? }, obj: { type, ... } }; - 根级控制对象:如
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_end、stop、section_end、top_level_branching、error,以及 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 对象;finally 中 reader.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 → 弹出末段回存"的流程,parseLine 在 JSON.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);
}
readNdjson 用 response.body.getReader() + TextDecoder.decode(value, { stream: true }) 读取,把解码文本喂给 createNdjsonBuffer,按需过滤心跳(isHeartbeat 同时识别包装报文内 obj.type === "chat_heartbeat" 与根级 type === "chat_heartbeat" 两种形态);finally 中 reader.cancel() 防连接泄漏。resumeChatMessage 则对 GET /chat/chat-session/{id}/resume-stream?cursor=n 做同样的消费(keepHeartbeats=true,把心跳当作静默期活性信号)。SendMessageBody 的字段(message、chat_session_id、parent_message_id、file_descriptors、deep_research、origin、可选的 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),维护每个父节点的childrenNodeIds与latestChildNodeId;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.ts:Message 含 messageId?、nodeId、parentNodeId、childrenNodeIds?、latestChildNodeId?、type(user|assistant|system|error)、message、files、packets,以及移动端追加的 streamingStartedAt(流式计时锚点)与 processingDurationSeconds(会话恢复后显示"Thought for Xs")。ChatState 为 'input'|'loading'|'streaming'|'uploading'。
4. 会话历史重建:chatHistory.ts
mobile/src/chat/chatHistory.ts 的 processRawChatHistory(rawMessages, packets) 把 GET 会话详情返回的 messages[] 与 packets[][](按助手消息序号对齐,一个列表对应一次助手轮次)重建为消息树:复用 message_id 作 nodeId,出错轮次用 error 文本渲染并标记为 type: "error",用 parent_message/latest_child_message 重建父子关系并对子节点排序,同时把 processing_duration_seconds 写入节点。
5. 编排:useChatController 与流式刷新节奏
移动端有自己的编排 Hook mobile/src/hooks/useChatController.ts,结构上对齐 Web 版但完全本地实现:
runChatStream为模块级函数(避免登录页卸载后中断流),消费streamChatMessage的异步生成器,用 50ms 批量 flush(FLUSH_INTERVAL_MS,定义于mobile/src/chat/constants.ts,注释说明 send 与 resume 共用同一节奏)累积Packet[]后一次性upsertMessages;- 首个 packet 到来时把
chatState置为streaming;MessageResponseIDInfo回填用户/助手消息的真实 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.ts、projects.ts 及 mobile/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}。 - Agents:
GET /api/persona→MinimalAgent[](id,name,description,tools[],starter_messages,icon_name,uploaded_image_id,builtin_persona,is_public,is_featured,display_priority)。选择是隐式的——会话携带persona_id,sendMessage没有 agent 参数;默认 persona id=0。类型对应web/src/lib/agents/types.ts,移动端有mobile/src/chat/agents.ts(含DEFAULT_AGENT_ID)。 - Projects:
GET /api/user/projects→Project[] {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-routerStack;鉴权路由组在mobile/src/app/(auth);新的鉴权聊天路由组可并排插入。 - HTTP:
mobile/src/api/client.ts的apiFetch<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.ts、mobile/src/api/auth/tokenStore.ts(secure-store 支撑的getToken/setToken)、mobile/src/hooks/useCurrentUser.ts、mobile/src/state/session.ts(zustand)。 - UI 原语:
mobile/src/components/ui(text/button/text-input/icon/separator)、侧栏mobile/src/components/sidebar、图标mobile/src/icons。zustand、@shopify/flash-list@2.0.2、react-native-reanimated@4、react-native-worklets、expo-dev-client均已就位。 - 共享包:
@onyx-ai/shared源码在web/lib/shared/src,目前较薄(types/dto.ts、types/enums.ts、contracts/、utils/),子路径导出./types ./contracts ./utils;移动端通过file:依赖消费,共享代码变更后必须重建 dist。
业界最佳实践(2025–2026 调研结论)
研究文档结合 2025–2026 年的公开资料,沉淀了五条可直接落地的工程结论:
- 流式传输:用
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 帧。 - 消息列表:
@shopify/flash-listv2(已在依赖中),非反转(non-inverted),maintainVisibleContentPosition={{ startRenderingFromBottom:true, autoscrollToBottomThreshold:0.2 }};通过onStartReached分页加载更早消息;注意短列表场景的边界(#2050)。注意:startRenderingFromBottom/autoscrollToBottomThreshold是 FlashList v2 自己扩展的maintainVisibleContentPosition键(与 RN 核心 ScrollView 只支持minIndexForVisible/autoscrollToTopThreshold不同)。 - Markdown:
react-native-markdown-display已无人维护。可选方案:react-native-streamdown(Software Mansion 出品,通过 worklets 处理流式中的部分/未闭合 Markdown,需要 dev build 并在 RN 0.85 上做兼容性 spike)或react-native-marked(纯 JS,安全回退)。无论选哪个库,块级 memoization 都是流式性能的关键。 - 键盘/输入:
react-native-keyboard-controller(用KeyboardStickyView实现吸底且随内容增长的输入栏)——需要 dev build(已有expo-dev-client)+ Reanimated(已在依赖中)。 - 文件:
expo-document-picker(copyToCacheDirectory:true)+expo-image-picker(mediaTypes:['images'])。上传用新版expo-file-system的File(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.ts、packetProcessor.ts 的 reducer、buildSendMessageBody、processRawChatHistory。重构 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/遍历、processRawChatHistory、projectFilesToFileDescriptors。不抽取与 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 解析器、契约/类型、messageTree、processRawChatHistory、projectFilesToFileDescriptors 全部原生写在移动端,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上传。 - 仅存于 Web:
usePacketProcessor+ transformers、MinimalMarkdown、完整工具/引用报文动物园、SWR hooks。Web 保留现有副本;导入仅在文件已是纯 TS 时按阶段机会主义地重指 shared——不做大爆炸式 Web 重构。 - 延后的富聊天功能(引用、Agent 化时间线、重新生成/编辑/反馈、追问、图片生成)各自作为独立的后期阶段落地,届时再把相关报文类型加入 shared + 扩展移动端映射 + 补充 RN UI。
落地启示:这份研究如何指导后续阶段
从源码证据看,最终落地形态与研究报告完全吻合:mobile/src/chat/ 下 ndjson.ts、messageTree.ts、chatHistory.ts、streamingModels.ts、fileDescriptors.ts 等均为移动端本地纯 TS 实现,配套 mobile/src/chat/__tests__/ 中 ndjson.test.ts、messageTree.test.ts、chatHistory.test.ts、fileDescriptors.test.ts 等测试覆盖;mobile/src/hooks/useChatController.ts 与 mobile/src/api/chat/stream.ts 完成了编排与传输。这份研究报告的价值在于:它把"移植"从"照搬 UI"重新定义为"界定协议级契约 + 最小健壮核心 + 分阶段扩展"的工程决策——先锁死需求边界(哪些延后),再盘点可复用资产(哪些纯 TS、哪些 React 耦合),再用共享边界的三方案对比回答"哪些代码该共享、什么时候共享",最终以"移动端原生、Web 不动"的务实修订收尾,为后续每个可独立合并的阶段(docs/mobile-chat/04-implementation-plan.md)提供了清晰的决策依据与风险清单。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00