Onyx 移动端 Agentic Reasoning Timeline 移植设计:从 Web 到 React Native 的对等实现研究
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
本篇技术指南基于 Onyx(danswer)仓库中docs/mobile-chat/9b-timeline/01-research.md的设计研究文档,系统讲解如何将 Web 端聊天界面中的Agent 推理时间线(AgentTimeline,即 reasoning / search / tool 子步骤的容器化展示)1:1 移植到 Onyx React Native + Expo 移动应用(mobile/)中。读者将掌握:Web 端时间线的数据流与渲染契约、移动端现有的可复用接缝、三种实现方案的取舍,以及最终选定的"完整外壳优先"(Full-Parity)方案在源码中的具体落地形态,并理解react-hooks/refs等平台约束如何驱动实现层面的必要分叉。
需求背景:为什么要把 Web 推理时间线搬进移动端
Web 端的 AI 助手回答中,Agent 在给出最终答案前会经历一段"推理过程"——包括内部推理(reasoning)、搜索、工具调用等多个子步骤。Web 端已经通过AgentTimeline组件将这些子步骤以步骤容器(step container)+ 头部(header)+ 图标(icon)+ 连接线(connector)+ 折叠/展开(collapse/expand)的形式呈现给用户。
9b 阶段的目标非常明确(需求原文):
- 将这套时间线移植到 Onyx 的移动端(React Native + Expo,即
mobile/目录),达到Web 对等(web parity)——不仅要"看起来一样",结构也要一样; - 在 9a(引用来源
CitedSources)的基础上扩展,并注册进 PR-3 渲染器分发(renderers/registry.ts)体系中; - 后端不做任何改动——推理数据包(reasoning packets)后端早已发出;
- 9b 将构建
AgentTimeline组合层(composition layer),后续所有阶段(search/fetch/tool 渲染器、9c–9e)都将接入这一层。
范围澄清:9b 只做"最细的行走骨架"
按 2026-07-16 所有者的范围锁定,9b 采用FOUNDATION-ONLY(仅基础)策略,只交付三件事:
- turn/tab 分组引擎(grouping engine);
AgentTimeline组合层(真实步骤 + 折叠/展开);- 唯一的 reasoning 步骤渲染器(处理
reasoning_start / reasoning_delta / reasoning_done三种包)。
其余一切均为独立后续阶段:
| 后续阶段 | 内容 |
|---|---|
| 内部/Web 搜索、fetch/open_url | 独立渲染器阶段 |
| python/code、coding-agent + bash | 独立渲染器阶段 |
| custom-tool(自定义工具) | 独立渲染器阶段 |
deep-research + 嵌套 Agent(sub_turn_index) | 独立渲染器阶段 |
| memory(记忆) | 独立渲染器阶段 |
| 图片生成 | 独立阶段(9e) |
多模型(model_index) | 移动端单模型,明确不做,但分组必须容忍非零model_index |
并行工具标签页(tab_index并行) | 后续阶段,但分组键与数据模型不得排除其可能性 |
同时有三个硬性约束:Web 对等是硬要求(不发明新的移动端时间线);聊天层保持原生(位于mobile/src/chat/,而非@onyx-ai/shared,这是 PR-2 的既定决策);后端不变。
现有代码基础:9b 的接缝早已预留
研究文档通过代码库扫描(并逐一实际读取验证)确认,9b 需要依赖的所有"接缝"(seam)在移动端已经存在且被显式预留:
1. 扁平化包处理器 ——mobile/src/chat/messageProcessor.ts
这是一个扁平、游标增量式(nextPacketIndex)的 packet→state 归约器:
ProcessedMessageState = { nodeId: number; nextPacketIndex: number; // 游标:只处理游标之后的包 citationMap: CitationMap; citations: StreamingCitation[]; seenCitationDocIds: Set<string>; documentMap: Map<string, SearchDoc>; isComplete: boolean; stopReason: StopReason | undefined; // ... }源码头部注释明确写道:"9b extends it with turn/tab grouping + timeline steps, so the shape here stays deliberately flat (grouping-free)."—— 即 9b 将在其上扩展 turn/tab 分组与时间线步骤,因此当前形态刻意保持扁平(无分组)。
从当前仓库源码看,messageProcessor.ts已实现了getGroupKey(${turn_index}-${tab_index ?? 0})、injectSectionEnd(合成SECTION_END让步骤读起来完整)、handleTurnTransition(新的turn_index关闭所有先前打开的分组)等核心机制——这正是文档中"分组引擎"的雏形。
2. 时间线组件桩 ——mobile/src/components/chat/AgentTimeline.tsx
研究文档描述它当时是一个stub:36px 导轨(w-36)、24pxAgentAvatar、reanimated 微光ThinkingLabel、TimelineStep列表原语(TimelineStepData = {key, label, status: "running"|"done"|"error", icon?}),连接线h-8 w-[1px] bg-border-01,首尾opacity-0;stepsprop 恒为[],无人填充它。它被渲染在MessageRow.AssistantMessage中、答案上方。
而从当前仓库看,AgentTimeline.tsx已经是完整的 Web 移植版本:接收turnGroups: TurnGroup[]、chatState、stopReason等,内部调用useTimelineHeader、useTimelineMetrics、useTimelineExpansion、useTimelineUIState,并根据 7 种 UI 状态分发StreamingHeader / ParallelStreamingHeader / StoppedHeader / CompletedHeader——即文档中"Approach C"的设计已经在仓库中落地,这也印证了研究结论的可行性。
3. 渲染器注册表 ——mobile/src/components/chat/renderers/registry.ts
移动端渲染器契约比 Web 更简单:
MessageRenderer = { matches(packets), Component } MessageRendererProps = { packets, processed }采用first-match-wins的RENDERERS数组 +findRenderer,目前只注册了MessageTextRenderer。相比 Web 的 render-prop 形式MessageRenderer<T,S>,这是文档明确点出的简化。
4. 每 flush 全量重算 ——mobile/src/hooks/usePacketDisplay.ts
processed = useMemo(() => processPackets(createInitialState(nodeId), packets), [nodeId, packets])每次 flush 全量重算(数组身份每次变化)。文档特别强调:这刻意不是渲染期可变的 ref——移动端的react-hooks/refslint禁止在渲染期间访问ref.current,因此 Web 端用 ref 持有的usePacketProcessor模式在移动端非法。这一点成为后续所有方案共同的硬约束。
5. 消息行组装 ——mobile/src/components/chat/MessageRow.tsx
AssistantMessage的组装顺序为:usePacketDisplay(node)→<Renderer packets processed/>(位于<View className="px-12">内)→AgentTimeline在其上 → 9a 的CitedSources在其下。hasContent = Renderer != null && packets.length > 0。
6. 流式模型 ——mobile/src/chat/streamingModels.ts
PacketType当前包含:MESSAGE_START/DELTA/END、STOP、SECTION_END、ERROR、CITATION_INFO、SEARCH_TOOL_DOCUMENTS_DELTA、OPEN_URL_DOCUMENTS。Placement已携带全部 4 个字段(turn_index、tab_index、sub_turn_index、model_index)。文档要求新增REASONING_START/DELTA/DONE(以及用于分组容忍的TOP_LEVEL_BRANCHING)——当前仓库中这些枚举值已存在(streamingModels.ts第 16–17 行、第 46–48 行)。
7. 9a 复用资产
mobile/src/chat/contracts/documents.ts提供完整的SearchDoc/StreamingCitation/CitationMap(9a 产物),供延后的 search/fetch 渲染器复用;9a 的源 UI(SourceRow/SourceIcon/openSource/CitedSources)同样可被延后阶段直接复用。
后端现状:推理包已存在,无需改动
后端已在backend/onyx/chat/llm_step.py中发出推理数据包(源码证据):
| 包类型 | 字段 |
|---|---|
ReasoningStart(type="reasoning_start") | 无字段 |
ReasoningDelta(type="reasoning_delta") | {reasoning: str}(增量文本) |
ReasoningDone(type="reasoning_done") | 无字段 |
数据包携带Placement.turn_index/tab_index。文档强调两个关键事实:
- 线上没有
message_end——整个 turn 只能通过OverallStop(type="stop")完成; SECTION_END通常是客户端合成的——Web 端在新的turn_index出现时把它注入先前的分组、在 STOP 时注入所有仍打开的分组——这正是步骤被标记为"完成"的方式。
当前移动端messageProcessor.ts的injectSectionEnd与此完全对应:它幂等地向分组追加合成包,并在 STOP 时关闭所有仍打开的分组(见handleStopPacket,源码注释:"the backend never sends one")。
此外TopLevelBranching {num_parallel_branches}是并行前的元数据(预并行信息),移动端用它维护expectedBranches映射(handleTopLevelBranching)。
Web 端真相源:对等的目标架构
Web 端实现全部位于web/src/app/app/message/messageComponents/下,是 9b 的对等目标,分四层:
1. 分组层
timeline/hooks/packetProcessor.ts:按${turn_index}-${tab_index ?? 0}分组、合成SECTION_END、归类 tool/display 分组;随后timeline/transformers.ts完成GroupedPacket[]→TransformedStep[]→TurnGroup[]的变换,其中isParallel = 同一 turn_index 下有多个步骤。
移动端的对应实现位于mobile/src/chat/timeline/transformers.ts:transformPacketGroups将GroupedPacket映射为TransformedStep(含key/turnIndex/tabIndex/packets),groupStepsByTurn按turnIndex聚合、按tabIndex排序并计算isParallel。
2. 组合层
AgentMessage.tsx运行usePacketProcessor+usePacedTurnGroups(200ms 错峰揭晓),上方渲染<AgentTimeline>,下方渲染最终答案的RendererComponent。
移动端对应:mobile/src/hooks/timeline/usePacketProcessor.ts将processPackets以useMemo全量重算方式托管(注释明确说明这是从 Web 的渲染期 ref 变更结构而来);mobile/src/hooks/timeline/usePacedTurnGroups.ts实现了 200ms 错峰(PACING_DELAY_MS = 200),并处理了历史回放旁路(shouldBypassPacing)、STOP 时立即冲刷全部待揭晓步骤、tool-after-message 隐藏答案等细节。
3. 外壳层
AgentTimeline.tsx+useTimelineUIState(7 种状态:EMPTY / DISPLAY_CONTENT_ONLY / STREAMING_SEQUENTIAL / STREAMING_PARALLEL / STOPPED / COMPLETED_COLLAPSED / COMPLETED_EXPANDED)+useTimelineExpansion(默认折叠;用户未手动切换时,停止/答案开始后自动折叠)+useTimelineHeader(微光头部文字)+useTimelineMetrics。
移动端对应文件为mobile/src/hooks/timeline/下的useTimelineUIState.ts、useTimelineExpansion.ts、useTimelineHeader.ts、useTimelineMetrics.ts、useStreamingDuration.ts、useTimelineStepState.ts,以及mobile/src/components/chat/timeline/headers/下的四个头部组件(StreamingHeader、ParallelStreamingHeader、StoppedHeader、CompletedHeader)。
4. 单步层
TimelineRendererComponent(每步独立的isExpanded,renderType = override ?? (isExpanded ? FULL : COMPACT))+StepContainer(TimelineIconColumn导轨 +TimelineSurface色调 +TimelineStepContent头/折叠/主体);结尾追加 Done/Stopped 终止步骤。
移动端对应mobile/src/components/chat/timeline/下的StepContainer.tsx、TimelineRendererComponent.tsx、TimelineStep.tsx、CollapsedStreamingContent.tsx、ExpandedTimelineContent.tsx,以及primitives/中的TimelineRoot、TimelineHeaderRow、TimelineIconColumn、TimelineSurface、TimelineStepContent、TimelineRow、TimelineTopSpacer、timelineTokens。
渲染器契约(interfaces.ts)
Web 采用render-prop形式:MessageRenderer<T,S>计算RendererResult[]并调用children(results):
RendererResult = { icon, status, content, expandedText?, // 展开态文本 supportsCollapsible?, // 是否支持折叠 alwaysCollapsible?, timelineLayout?, noPaddingRight?, surfaceBackground?, // 表面背景色 }外层StepContainer拥有包裹层,渲染器自身从不绘制自己的外壳。
Reasoning 渲染器
timeline/renderers/reasoning/ReasoningRenderer.tsx:SvgCircle图标、状态文案"Thinking"(或提取的 markdown 标题)、流式推理 markdown 的ExpandableTextDisplay、500ms 最小思考门控。
移动端将推理文本解析逻辑独立为纯模块mobile/src/chat/timeline/reasoningState.ts(保持 reanimated-free 以便单测):constructCurrentReasoningState判断hasStart/hasEnd(SECTION_END/ERROR/REASONING_DONE任一即结束)、拼接REASONING_DELTA文本;extractFirstParagraph在推理以短 markdown 标题开头时提取title(超过 60 字符视为散文而非标题,MAX_TITLE_LENGTH = 60)。UI 侧对应ReasoningTextSheet.tsx/ReasoningTextWindow.tsx。
移动端原语与平台约束(关键注意事项)
研究文档从渲染基础设施扫描中总结了移动端的可用原语与"坑":
- 可用原语:
View、Text(font+color 枚举)、Icon、Separator、Button、Card、Content/ContentAction、Spinner(RNAnimated,jest 安全)、StreamingMarkdown(富 markdown,但颜色必须通过varsLight/varsDark+textPresets从@onyx-ai/shared/native解析为具体 hex——markdown 库内部NativeWind 类不生效)、reanimated 4.3.1(当前微光效果;ChatSurface使用LinearTransition/FadeIn/FadeOut); - 没有
Collapsible/Accordion原语——折叠/展开是全新工作(需向所有者确认:移植 Web UX 还是用 reanimated 组合); - 没有 chevron-up(需旋转
chevron-down);没有 brain/推理图标、没有纯圆圈字形(Web 用SvgCircle)——加图标是小规模的所有者决策; - NativeWind 间距是像素值(类名数字 == px);
- RN 没有 hover——删除 Web 的
isHover分支,桌面端 hover 展示必须变成常显或显式点击的触达(且要真实的命中目标); - reanimated jest 注意事项:把纯逻辑(分组/步骤推导/状态机)放在无 reanimated 的模块中,叶子组件直接导入;
- 流式重渲染性能:行组件 memoize、合并更新;自动折叠的高度动画在流式过程中可能卡顿(优先固定高度摘要 + 点击展开);
- 无障碍:触发器
accessibilityRole="button"+accessibilityState={{expanded}};折叠后子元素必须从无障碍/焦点树中移除,而非仅视觉隐藏。
行业最佳实践
研究文档汇总了六条业界共识(外部资料仅作背景,不构成仓库事实):
- 渐进式披露是主流——默认折叠;流式中显示紧凑的 "Thinking… (Ns)" 标签(带动画字形 + 耗时计时);完成后自动折叠为一行摘要;不要默认倾倒完整思维链(DeepSeek 式的流水输出被点名批评为令人淹没);
- Agent-UX "活动时间线"——可折叠的冗余度、固定的"当前步骤"、分层透明度;滚动的聊天线程不是好的工作流追踪器,结构化的时间线(而非内联文本)才是正确容器;
- 触屏披露无障碍——触发器必须暴露展开/折叠状态;折叠面板的子元素必须从 AX/焦点树移除;
- RN 流式重渲染陷阱——
React.memo行组件 +useCallback的renderItem;不要每个 tick 都给每行新的对象/函数身份;合并/节流流式setState(不要每个 token 都 setState); - Hover→触屏陷阱——每个桌面 hover 揭示都必须变成常显或显式点击的触达,且要有真实命中目标(RN 完全没有 hover);
- RN 时间线/步骤指示器库又薄又没人维护——预期从原语自己构建导轨/圆点/可折叠节点;现有库是线性表单向导式的 stepper,不是流式/嵌套时间线。
三种实现方案
方案 A —— 极简优先:"Lean Steps"(分组进处理器 + reasoning 叶子接入现有导轨)
扩展恰好两个现有接缝、新增一个叶子:
- 分组逻辑内置于已经是纯函数的
messageProcessor(在ProcessedMessageState上新增steps: TimelineStep[]),因此usePacketDisplay的每次 flush 全量重算路径完全不动、jest 安全; - 给现有
AgentTimelinestub 喂真实 reasoning 步骤,新增一个ReasoningStep叶子(流式StreamingMarkdown主体 + 点击切换折叠 + 叶子本地计时); - 扁平
{packets, processed}渲染器契约原样保留——reasoning 是时间线驻留的,不是注册表渲染器,因此findRenderer/RENDERERS和 9a 引用路径都不受影响; - 不移植任何 Web 机制(无 render-prop 契约、无 7 状态机、无错峰)。
改动文件:streamingModels.ts(+4 枚举、+4 接口)、messageProcessor.ts(分组 →steps/stepByKey)、AgentTimeline.tsx(渲染TimelineStep[],switch(kind))、MessageRow.tsx(传 steps、细化isLoading);新增ReasoningStep.tsx及分组单元测试。
工作量:约 360–420 LOC,1 个 PR。代价:最小改动面、对 9a 零风险、纯分组可完全单测;但没有错峰、没有 7 状态头部、没有逐步 render-prop 契约;第二个渲染器要接入需新增kind+case(而非findRenderer),即当真正的工具需要色调表面/逐步折叠时,AgentTimeline要做一次有界的重构。后续契合度:每个延后阶段 = 加kind+ reducer 分支 + 叶子 + 一个case;9a 的 SourceRow 可落入 search/fetch 叶子;更重的机制(错峰、render-propRendererResult)等真正有工具需要时再作为该接缝的有界演进引入。
方案 B —— 灵活优先:"可扩展步骤接缝"
一个纯 turn/tab分组模块(chat/timeline/grouping.ts)与现有扁平processPackets并列运行(都通过useMemo托管,遵守 refs-lint 禁令),产出TurnGroup[]的TransformedStep[]。每个步骤由优先级排序的步骤注册表(镜像 Web 的findRenderer)解析到一个步骤渲染器,渲染器返回 WebRendererResult数据契约的移动端模拟(普通对象返回,非 JSX,比 Web 的 render-prop 简单但同样可扩展)。单个StepContainer(导轨 + 色调表面 + 头部 + 折叠主体)拥有全部 chrome;渲染器从不绘制自己的包裹层。Reasoning 是 1 号渲染器(也是链条最后的兜底)。messageProcessor保持扁平(分组是兄弟模块,遵循其头部注释)。新增精简版useTimelineExpansion+useTimelineUIState,以及全新Collapsible原语 + 推理图标(均为所有者确认项)。
改动文件:新增chat/timeline/{grouping,stepContract,stepRegistry}.ts、chat/timeline/renderers/reasoning/ReasoningRenderer.tsx、components/chat/timeline/StepContainer.tsx、components/chat/timeline/{useTimelineExpansion,useTimelineUIState}.ts、components/ui/collapsible.tsx(待确认)、icons/reasoning-circle.tsx(待确认);修改streamingModels.ts、usePacketDisplay.ts(+turnGroups)、AgentTimeline.tsx、MessageRow.tsx。
工作量:约 1,810 LOC(生产 + 测试),2 个 PR——9b-1 纯引擎(分组 + 契约 + 注册表 + reasoning 推导 + 测试,无 UI)、9b-2 UI 与接线(Collapsible、StepContainer、expansion/uiState、视图层、AgentTimeline 重写)。代价:每个延后阶段 = 一个新文件(谓词 + 返回RendererResult的渲染器),分组/容器/折叠/MessageRow零改动,改造代价摊薄到约 0;但 LOC 约为 A 的 2 倍,且引入了 reasoning 几乎用不到的契约字段(alwaysCollapsible、timelineLayout,部分要到阶段 2 才非投机),外加 2 个所有者把关的产物带来决策延迟。后续契合度:近乎完美——这就是接缝本身;search/fetch 直接复用 9a 的 SourceRow;并行标签页已在键中区分;model_index被容忍;只有嵌套是可能扩展groupStepsByTurn的阶段。
方案 C —— 全量对等:"忠实外壳优先"
现在就把 Web 的整个时间线外壳 1:1 移植:分组 +transformers+usePacedTurnGroups(200ms 错峰)+ 完整useTimelineUIState(7 状态)+useTimelineExpansion+useTimelineHeader+useStreamingDuration(实时 "Ns"/"Thought for {duration}")+useTimelineMetrics+ render-prop 渲染器契约 +StepContainer+TimelineRendererComponent+ Streaming/Stopped/Completed 头部 + Done/Stopped 终止步骤——只接线 reasoning 渲染器主体。保持移动端每 flush 全量重算模型(Web 的 ref 持有usePacketProcessor在移动端非法)的方式是:把分组做成纯函数;usePacedTurnGroups必须重构掉 Web 的渲染期 ref 读取以符合 refs lint。该外壳在只有 reasoning 内容时与 Web行为上无法区分,因此延后阶段只需添加渲染器主体。
改动文件:约 22 个新增 + 6 个修改(分组、transformers、接口、错峰 hook、4 个状态/头部/指标 hook、时长 hook、3 个导轨/表面/内容原语、StepContainer + TimelineRendererComponent、3 个头部组件、ReasoningRenderer、Collapsible/ExpandableTextDisplay(待确认)、圆形图标、AgentTimeline 重写;修改streamingModels、usePacketDisplay、MessageRow、registry)。
工作量:约 3,700–4,300 LOC,坦诚地说 5–6 个 PR(接线 → 错峰 → 状态 → 原语 → 渲染器 → 头部 + 组合)。代价:外壳第一天即与 Web 忠实一致(微光头部文字、实时计时、错峰揭示、答案开始自动折叠、Done/Stopped、色调表面);每个延后渲染器都是纯增量、外壳零返工、零漂移;但这是 reasoning-only 阶段最大的 LOC/审查面,7 状态机的大片区域在并行存在前以死路径形式上线,错峰 + 自动折叠高度动画带来真实的流式性能/卡顿风险,usePacedTurnGroups(重构掉渲染期 ref)是最容易出 bug、且存在行为分歧风险的文件。后续契合度:同类最佳——并行标签页数据模型 +sub_turn_index/model_index容忍内置且休眠;但这种最大扩展性正是提前支付外壳成本的唯一理由。
方案横评
| 维度 | A(Lean Steps) | B(可扩展接缝) | C(忠实外壳) |
|---|---|---|---|
| 规模 | ≈400 LOC,1 PR | ≈1,800 LOC,2 PR | ≈4,000 LOC,5–6 PR |
| 共享基础 | 三者都新增同样的streamingModels包类型和同样的分组键(${turn_index}-${tab_index ?? 0},model_index忽略),分歧仅在"现在建多少外壳/契约" | ||
| refs-lint 约束 | 三者都被约束:都不能移植 Web 渲染期可变 ref 的usePacketProcessor;分组是纯useMemo;C 在此风险最大(错峰 hook 重构) | ||
| 扩展性 vs YAGNI | 推迟 render-prop/错峰/状态机直到第二个渲染器证明形状(风险:之后AgentTimeline有界重构) | 前置数据契约 + 注册表(真正难改造的部分),但不错峰/不全状态 | 前置一切(风险:死代码 + 卡顿 + 移植移动靶) |
| 即时对等 | 只有 C 让外壳第一天 Web 忠实(微光头部、实时计时、错峰揭示、答案自动折叠、Done/Stopped)。A、B 正确渲染 reasoning 但外壳更简单;路线图的对等要求是"外观 AND 结构"——仅 reasoning 时,A/B 仍可匹配步骤/导轨/折叠形状,同时推迟头部状态机 | ||
| 所有者确认项 | A 一个都不需要(无新原语/图标) | B、C 在 UI 落地前都需要Collapsible原语 + 推理图标决策 |
最终选型:方案 C —— "忠实外壳优先"(Full Parity)
方案 C 在 GATE 1(所有者,2026-07-16)被选定。所有者指令(原文意图):
"I want something that matches web. I am going to build the renderers immediately — current PR goes in [first], then the renderers. I don't want any refactor later. I'm fine with any number of lines — I want everything, whatever way we need it. Just don't drift away from web."
("我要和 Web 一致的东西。我会立刻构建渲染器——当前 PR 先合入,然后渲染器。我之后不想做任何重构。行数多少我都无所谓——我要全部,用任何我们需要的方式。只是别偏离 Web。")
对 9b 的具体含义
- 1:1 移植 Web 的整个时间线外壳:render-prop
MessageRenderer<T,S>契约 +RendererResult/RenderType(HIGHLIGHT/FULL/COMPACT/INLINE)、纯分组(packetProcessor→transformers)、usePacedTurnGroups(200ms 错峰)、完整useTimelineUIState(7 状态)+useTimelineExpansion+useTimelineHeader+useStreamingDuration+useTimelineMetrics、StepContainer+TimelineRendererComponent+ 导轨/表面/内容原语、Streaming/Stopped/Completed 头部、Done/Stopped 终止步骤。原样采用 Web 的 render-prop 渲染器契约(而非方案 B 的简化数据对象),使未来每个 Web 渲染器都能近乎机械化地移植。 - 9b 只接 reasoning 渲染器主体。休眠的并行标签页状态(
isParallel、STREAMING_PARALLEL、showParallelTabs)和sub_turn_index/model_index容忍随外壳一起上线(与 Web 一致),使并行/嵌套/多模型成为日后仅 UI 层的后续工作、接缝零改动。 - 9b 功能流范围(已确认):本规范只覆盖完整外壳 + reasoning 渲染器。每个工具渲染器(内部/Web 搜索、fetch/open_url、python/code、coding-agent+bash、custom-tool、deep-research + 嵌套 Agent、memory)都是该零重构接缝上的独立后续 PR,按阶段逐一规范(先 grill 后设计),由所有者构建时确定——不在本设计内。但设计必须完整规定 render-prop 契约 + 每种工具的渲染器清单/分发(inventory/dispatch),使这些后续工作机械化。
唯一被接受的分叉(平台强制、行为保持)
Web 的usePacketProcessor和usePacedTurnGroups在渲染期间读取stateRef.current(重置检测/旁路),而移动端react-hooks/refslint禁止这一点。这两者被重构为useMemo重算 + 仅 effect 写 ref的模型,保持相同的分组输出和相同的 200ms 揭晓时序。这是实现层面的必然,不是外观/结构漂移——渲染出的时间线与 Web 忠实一致。细节记录在 03-detailed-design.md。
这一决策的落地可以从当前仓库源码中直接验证:mobile/src/hooks/timeline/usePacedTurnGroups.ts的头部注释明确写道,Web 版本在渲染期读取错峰 ref 被 lint 禁止,因此渲染相关字段(revealedStepKeys、toolPacingComplete)改为由 effect/callback发布的useState,内部记账(待办队列、定时器、标志)保留在仅在 effect/callback 中触碰的 ref 中,且明确"dropped web's prevPacedRef stabilization"。
对后续阶段的指导意义
9b 选型的分层价值在于:数据契约和分组键先行固化,UI 机制按需演进。任何延后阶段(9c–9e)在接入时只需做三件事:
- 在
streamingModels.ts注册对应的PacketType; - 按 render-prop 契约实现一个返回
RendererResult的渲染器(复用 9a 的 SourceRow 等资产); - 在步骤注册表/分发中登记匹配谓词——外壳(StepContainer、头部状态机、错峰、折叠、Done/Stopped 终止步骤)零改动。
而移动端开发者需要始终牢记两条平台红线:渲染期间不得触碰ref.current(分组与推导一律纯useMemo);纯逻辑与 reanimated 分离(分组/步骤推导/状态机放在无 reanimated 模块中,保证 jest 可测)。这两条红线既是方案 C 相比 Web 的唯一实现分叉来源,也是移动端时间线长期可维护、可单测的根本保证。
结语
从研究文档到当前仓库源码,docs/mobile-chat/9b-timeline/系列文档(00-index.md、01-research.md、02-high-level-design.md、03-detailed-design.md、04-implementation-plan.md)与mobile/中的时间线实现(mobile/src/chat/timeline/、mobile/src/components/chat/timeline/、mobile/src/hooks/timeline/)构成了"设计先行、源码印证"的完整闭环。这套方法论的核心可复用于仓库内其他平台移植类任务:先扫描并确认既有接缝,再以"对等但合法"的方式重构 Web 机制,最后让延后功能在零返工的接缝上机械接入。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考