Onyx 移动端 Agentic Reasoning Timeline 高层设计:1:1 移植 Web 智能体时间线外壳
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
导读
本文是 Onyx(danswer)移动端富聊天改造子阶段9b — Agentic Reasoning Timeline的高层设计解读。Web 端在助手"思考"或调用工具时,会在答案上方展示一条垂直步骤时间线(推理 Thinking 步骤、搜索/工具步骤等,每个步骤含图标、状态头、连接线和可折叠正文),并在流式输出期间显示带微光的 "Thinking… (12s)" 头部,答案开始后自动折叠为 "Thought for 12s · 3 steps" 胶囊。9b 的目标是把这条时间线完整外壳 1:1 移植到 React Native + Expo 移动端,并只接线reasoning(推理)步骤渲染器,为后续 search/fetch/python/custom-tool/deep-research/memory 等工具渲染器铺好"零重构"的接入缝隙。读完本文你将掌握:移动端时间线的端到端数据流、分组引擎与section_end合成规则、200ms 节拍揭示、七状态机、render-prop 渲染契约,以及 Web/移动两端唯一被平台强制的实现差异。
背景:为什么移动端需要 9b
在 9b 之前,移动端聊天虽然已经能渲染流式 Markdown 答案和 9a 阶段引入的 Sources 引用条,但对助手的推理过程与工具调用过程完全没有可见性——AgentTimeline组件只是一个steps属性永远为空的桩(stub)。这可以从移动端源码得到印证:
mobile/src/components/chat/AgentTimeline.tsx已有 36px 的 rail、24px 的AgentAvatar、reanimated 微光ThinkingLabel和TimelineStep列表原语,但没有任何数据填充它;mobile/src/chat/messageProcessor.ts是一个扁平的、游标增量式(nextPacketIndex)的 packet→state reducer,其文件头注释明确写着"9b extends it with turn/tab grouping + timeline steps, so the shape here stays deliberately flat (grouping-free)"——即 9b 的缝隙是预先预留的。
而 Web 端早已拥有完整的 agent timeline 体系,全部位于web/src/app/app/message/messageComponents/下。9b 的使命就是把这套体系忠实移植到移动端。
总体方案:Approach C — Faithful Shell First(全量外壳先行)
9b 在需求研究阶段(01-research.md)评估过三个方案:
- A — Lean Steps(约 400 LOC,1 个 PR):分组逻辑放进已有的扁平 reducer,只加一个推理叶子,不移植任何 Web 机制;
- B — Extensible Step Seam(约 1,800 LOC,2 个 PR):纯分组模块 + 优先级步骤注册表 + 数据契约,但不含 pacing 与完整状态机;
- C — Faithful Shell First(约 3,700–4,300 LOC,5–6 个 PR):把 Web 的整个时间线外壳 1:1 移植,包括 render-prop 渲染契约、分组引擎、
usePacedTurnGroups的 200ms 节拍、完整的七状态机、StepContainer 与 Done/Stopped 终止步骤,只接线 reasoning 渲染器。
最终选定Approach C(GATE 1,2026-07-16 所有者决策)。所有者的原话意图是:"我要和 Web 一致的东西。我马上就会去写渲染器——当前 PR 先合入,然后渲染器跟上。我不想要任何后续重构。行数多少我都不在乎——我要全部的东西,用任何必要的方式。只是别偏离 Web。" 这意味着移动端要精确移植Web 的 render-propMessageRenderer<T,S>契约(而不是简化成数据对象),使后续每个 Web 渲染器都能近乎机械地移植过来。
端到端数据流:从原始 Packet 到屏幕上的时间线
移动端聊天流本质上是一个随模型流式输出不断增长的扁平Packet[]列表,每个 packet 形如{placement, obj}。9b 的核心做法是在"原始 packets"与"屏幕上的时间线"之间插入 Web 的分组(grouping)、节拍(pacing)与状态机(state-machine)三层,忠实镜像 Web 的AgentMessage。完整流程(源自高层设计)如下:
- 分组(Group):一个纯 reducer 遍历 packets,按分组键
"{turn_index}-{tab_index}"(取自每个 packet 的placement)把它们装入steps。每当出现新的turn_index,就把之前所有未结束的 step"完成"——方法是向其中合成(synthesize)一个section_end;最终的stoppacket 关闭剩余的一切。这正是 Web 判定一个 step 结束的方式——后端很少主动发送section_end。随后 steps 被组织成turn groups(共享同一turn_index的 steps 视为"并行";单独一个的视为"串行")。 - 节拍(Pace):一个有状态 hook 以200ms 交错逐步揭示 steps(第一步立即显示,其余每隔 200ms 显示一个;
stop一次性全部刷出)。它还扣住最终答案,直到工具步骤动画播完——保证答案永远不会先于时间线弹出。历史重载(已完成的)消息绕过节拍,瞬间显示全部内容。 - 推导 UI 状态(Derive UI state):一个纯状态机把"正在流式 / 已停止 / 已展开"推导为七种状态(EMPTY、DISPLAY_CONTENT_ONLY、STREAMING_SEQUENTIAL、STREAMING_PARALLEL、STOPPED、COMPLETED_COLLAPSED、COMPLETED_EXPANDED)外加一组"显示什么/圆角哪里"布尔量。若干独立小 hook 分别计算头部文本(取自当前 step 的第一个 packet:"Thinking"、"Searching the web"…)、实时计时器(每秒 1 次,一旦后端上报时长即冻结)、step 计数以及展开/折叠状态(默认折叠;答案开始后自动折叠,除非用户手动切换过)。
- 渲染(Render):时间线外壳在 36px rail 中绘制 agent 头像、头部(流式时是微光 "Thinking…",完成时是 "Thought for X · N steps" 折叠按钮),展开时显示完整 step 列表。每个 step 由
StepContainer绘制(图标 rail + 连接线 + 着色表面 + 头部 + 可折叠正文)。TimelineRendererComponent拥有每个 step 的展开/折叠状态,并通过findRenderer(沿用 Web 的优先级调度,reasoning 排最后)选择渲染器。渲染器是一个render-prop 组件:它计算一个小型RendererResult({icon, status, content, …})交给容器,由容器拥有视觉外框。9b 接线的唯一渲染器是ReasoningRenderer:它累积流式推理 Markdown,抽取标题作为 step 标题,强制 500ms 最短 "Thinking" 展示时长,并通过移动端的StreamingMarkdown渲染正文。 - 答案与引用(Answer + sources):时间线下方,最终答案通过同一渲染契约以
FULL级别渲染(移动端已有的MessageTextRenderer,迁移到 render-prop 契约),9a 的Sources条渲染在其下——两者行为均保持不变。
组件交互图
以下结构图(源自高层设计)展示了移动端 9b 完成后各组件的关系:
assistant node.packets[] (flat Packet[], grows each stream flush) │ ▼ ┌───────────────────────────────────────────────────────────────────────┐ │ MessageRow.AssistantMessage (mobile analog of web AgentMessage) │ │ │ │ usePacketProcessor(packets, nodeId) │ │ └─ messageProcessor.processPackets (PURE reducer: grouping + │ │ section_end injection + citations/docs + finalAnswerComing) │ │ → toolGroups, displayGroups, citations, stopPacketSeen, … │ │ └─ transformers.groupStepsByTurn → toolTurnGroups: TurnGroup[] │ │ │ │ usePacedTurnGroups(toolTurnGroups, displayGroups, stop, node, final) │ │ └─ 200ms staggered reveal (timer) │ │ → pacedTurnGroups, pacedDisplayGroups, pacedFinalAnswerComing │ │ │ │ ┌── <AgentTimeline turnGroups=pacedTurnGroups … /> ── (ABOVE) ──┐ │ │ │ useTimelineUIState (7 states) · useTimelineExpansion │ │ │ │ useTimelineHeader · useStreamingDuration · useMetrics │ │ │ │ header switch → StreamingHeader / CompletedHeader / Stopped│ │ │ │ ExpandedTimelineContent → per step: │ │ │ │ TimelineStep → TimelineRendererComponent (owns expand) │ │ │ │ → findRenderer(step.packets) → ReasoningRenderer │ │ │ │ → children([RendererResult]) → StepContainer wraps │ │ │ │ + Done / Stopped terminal step │ │ │ └─────────────────────────────────────────────────────────────── ┘ │ │ │ │ pacedDisplayGroups → <RendererComponent renderType=FULL/> (BELOW) │ │ → findRenderer → MessageTextRenderer → StreamingMarkdown answer │ │ │ │ <CitedSources/> (9a, unchanged) │ └───────────────────────────────────────────────────────────────────────┘分组引擎:section_end合成是正确性的核心
Web 端的分组引擎是web/src/app/app/message/messageComponents/timeline/hooks/packetProcessor.ts,9b 将其逐字移植为移动端的messageProcessor.ts扩展。仓库源码可以印证文档中的关键算法:
- 分组键:
getGroupKey从 packet 的placement中取turn_index与tab_index ?? 0,拼成`${turnIndex}-${tabIndex}`(见 packetProcessor.ts)。model_index被忽略(移动端单模型),但分组键与数据模型不排除并行/嵌套/多模型,为后续阶段预留; section_end合成:injectSectionEnd是幂等的(key 已结束时直接返回),解析 key 中的{turn, tab},向该组 push 一个合成 packet{placement: {turn_index, tab_index}, obj: {type: SECTION_END}}(注意不带sub_turn_index——这样 research/coding 父级完成判定才能工作),并把 key 标记为已结束(见 packetProcessor.ts)。触发时机共有三个:- 触发 1:真实的
SECTION_END/ERRORpacket; - 触发 2(turn 切换):新的
turn_index的第一个 packet 会向所有未结束的seenGroupKey注入section_end; - 触发 3(stop):第一个
stop向所有打开的组注入。 一个关键细节:在已见过的 turn 内出现新的tab_index不会触发触发 2——这正是并行 tab 语义得以保留的原因。
- 触发 1:真实的
- 内容判定:
CONTENT_PACKET_TYPES_SET与FINAL_ANSWER_PACKET_TYPES_SET两个集合决定一个组是否有可显示内容、最终答案是否即将到来(见 packetProcessor.ts)。例如REASONING_START在内容集合中,而MESSAGE_START/MESSAGE_DELTA/IMAGE_GENERATION_*在最终答案集合中。
为什么这一步如此重要?因为在 Web 与移动端,一个 step 是否"完成"几乎完全靠客户端合成的section_end判定——后端很少主动发送。把它逐字移植,是保证推理/工具步骤正确闭合的承重性正确性规则(Web 端代码位于web/.../timeline/hooks/packetProcessor.ts)。
节拍层:200ms 交错揭示与答案闸门
usePacedTurnGroups是移动端新引入的 hook(对应 Web 同名实现),负责:
- 第一步立即显示,其余 steps 每隔
PACING_DELAY_MS = 200毫秒逐一显示; stop到来时一次性刷出所有步骤;- 扣住最终答案(
pacedDisplayGroups/pacedFinalAnswerComing),直到工具步骤动画完成,避免答案先于时间线弹出; - 历史重载(已完成的)消息绕过节拍,全部瞬间呈现;
- Web 的
prevPacedRef引用稳定性逻辑在移植时被舍弃。
七状态机与派生态 hook
移动端按 1:1 移植了 Web 的派生 hook 族(全部位于mobile/src/hooks/timeline/):
| Hook | 职责 |
|---|---|
useTimelineUIState | 纯状态机:六分支优先级 EMPTY → DISPLAY_CONTENT_ONLY → STREAMING_PARALLEL/SEQUENTIAL → STOPPED → COMPLETED_EXPANDED → COMPLETED_COLLAPSED,外加showTintedBackground、showRoundedBottom、showDoneStep、showStoppedStep、showParallelTabs等布尔量 |
useTimelineExpansion | 折叠状态:默认折叠、userHasToggled锁存、在stopPacketSeen \|\| hasDisplayContent时自动折叠、并行 tab 同步 |
useTimelineHeader | packet 类型 → 头部文本映射:reasoning → "Thinking",stop_reason === USER_CANCELLED→ StoppedHeader,默认 → "Thinking…" |
useStreamingDuration | 实时计时器:每秒刷新一次(整数秒变化时才更新),后端时长(message_start.pre_answer_processing_seconds)存在时冻结 |
useTimelineMetrics | step 计数与 last-step 标志 |
useTimelineStepState | memory 专属提取(休眠,后续阶段启用) |
这些 hook 需要的streamingStartedAt时间戳由 PR-3 流控制器在流启动(发送与恢复)时打在每个助手节点上——没有它,实时计时标签永远不会显示。
Render-prop 渲染契约:零重构的未来渲染器缝隙
9b 最关键的架构决策是原样采用 Web 的 render-prop 契约(移植自 Webinterfaces.ts,见详细设计):
RenderType:HIGHLIGHT / FULL / COMPACT / INLINE;RendererResult:{icon, status, content, supportsCollapsible?, alwaysCollapsible?, timelineLayout?, noPaddingRight?, surfaceBackground?}(Web 中无人读取的expandedText?死字段在移植时被丢弃);MessageRenderer<T,S>:render-prop 类型,渲染器接收{packets, state, renderType, animate, stopPacketSeen, …}并调用children(results)把RendererResult交给容器——渲染器绝不返回自己的 View 树,否则StepContainer无法包裹它;findRenderer:完整的 13 槽优先级链(chat → deep-research → research-agent → coding-agent → web-search → internal-search → image → python → file-reader → custom-tool → fetch → memory →reasoning 最后)。9b 只接线 chat→MessageTextRenderer与 reasoning→ReasoningRenderer,其余 11 个谓词存在但返回null,每个都标注// PR 9x: <tool>——后续阶段只需一行接线上。
这样做的回报是:每个未来工具渲染器(搜索/抓取/Python/自定义工具/深度研究/记忆)都只是"一个 obj 接口 + 一个findRenderer谓词接线 + 一个返回RendererResult的 render-prop 渲染器",零引擎/枚举/外壳改动。这也要求把移动端 PR-3 已有的简单{matches, Component}注册表(mobile/src/components/chat/renderers/registry.ts)现在就迁移到该契约(包括最终答案的MessageTextRenderer)——推迟迁移本身就是所有者明令避免的"日后重构"。
ReasoningRenderer:唯一接线的步骤渲染器
Web 端实现位于web/src/app/app/message/messageComponents/timeline/renderers/reasoning/ReasoningRenderer.tsx,移动端逐字移植。从源码可看到三个核心点:
- 500ms 最短 "Thinking" 展示:
const THINKING_MIN_DURATION_MS = 500;(见 ReasoningRenderer.tsx)——推理流结束后至少保持 500ms 的 "Thinking" 状态,避免闪烁; - 标题抽取:
extractFirstParagraph只把真正的 Markdown 标题(以#开头)且长度不超过 60 字符的第一段抽取为 step 标题,否则回退为 "Thinking"(见 ReasoningRenderer.tsx); - 状态累积:
constructCurrentReasoningState判定hasStart(REASONING_START)与hasEnd(SECTION_END/ERROR/REASONING_DONE),并把所有REASONING_DELTA的reasoning字段 join 成正文。
正文渲染用移动端的ReasoningTextWindow(192px 高、8×24 的ScrollView包裹StreamingMarkdown,流式时自动滚到最新行,关闭后可手动滚动,溢出时显示 "View full text" 按钮),以及ReasoningTextSheet模态(完整文本 + 字节数 + 行数 +expo-clipboard复制;Web 的 Download 因无移动端对应物而被移除)。
端到端场景:用户向推理模型提问
(源自高层设计的完整场景)
- 用户发送问题,流开始;
AgentTimeline显示头像 + 微光"Thinking…"(状态 EMPTY)。 reasoning_start随后reasoning_deltapackets 到达(turn 0)。分组引擎打开 step"0-0";头部切到STREAMING_SEQUENTIAL并显示微光"Thinking";折叠态流式预览显示推理 Markdown 的最新几行滚动进来;计时器逐秒跳动"3s… 4s…"。- 模型思考结束(
reasoning_done,或新 turn /message_start)。推理 step 被标记完成(合成section_end);message_start置位finalAnswerComing;时间线自动折叠为"Thought for 6s · 1 step"。 message_deltapackets 通过MessageTextRenderer在(已折叠的)时间线下方流式渲染答案;若答案引用了文档,9aSources条出现。stop结束本轮。点击 "Thought for 6s" 胶囊展开时间线,显示完整推理 step(图标 rail + "Thinking" 头部 + 推理 Markdown)与终止的"Done"step。- 稍后重新打开聊天会水合已保存的 packets、绕过节拍,并瞬间渲染折叠时间线 + 答案。
关键决策与原因
- 精确采用 Web 的 render-prop
MessageRenderer<T,S>契约(而非简化的数据对象):所有者会立即以后续 PR 构建工具渲染器,要求零重构——每个 Web 渲染器的移植必须近乎机械,这要求完全相同的{packets, state, renderType, children(results)}形状与完全相同的RendererResult字段。 - 忠实的
section_end合成 +"{turn}-{tab}"分组:step 是否完成几乎全部由客户端合成判定,这是承重规则,必须逐字移植。 - 唯一被接受的、平台强制的分歧:两个 render 期间读 ref 的 hook 被重构:Web 的
usePacketProcessor在render 期间修改状态 ref,usePacedTurnGroups在 render 期间读取 pacing refs——两者在移动端的react-hooks/refslint 下都非法。它们被重构为useMemo全量重算(分组)+ effect 驱动状态(pacing 定时器),行为保持等价(同样的分组输出、同样的 200ms 节奏)。这是实现层面的必要,不是外观/结构漂移。顺带一提,这个重构反而更符合 React 官方 purity 规则(react.dev 明确禁止 render 期间读写ref.current)。 - 现在交付完整外壳,只接线 reasoning:并行 tab(
ParallelTimelineTabs)与嵌套/记忆路径以休眠状态随外壳发布(与 Web 一致),使并行性、深度研究嵌套、多模型都成为后续纯 UI跟进,无需缝隙变更——这既匹配 Web "外壳已支持"的设计,也兑现所有者"要全部、不要重构"的要求。 - 渐进式披露与行业默认一致:默认折叠、流式 "Thinking… (Ns)" 摘要、答案到来时自动折叠、点击(而非悬停)展开——这是主流模式,也正是 Web 已有的行为,所以对齐与最佳实践在这里重合。
移动端与 Web 的既定分歧清单
除上述两个 hook 重构外,详细设计还记录了以下有意的、不影响 reasoning 路径外观/结构的分歧:
- 微光 = reanimated不透明度脉冲(复用 9a 的
ThinkingLabel),而非 Web 的background-clip:text渐变(RN 无对应物); - 推理正文窗口 = 固定高度的自动跟随 ScrollView,而非 Web 的像素级
translateY自动滚动(RN 的 native measure 叶子在 YogaAtMost约束下无法溢出,裁剪 View 方案不可行); - 无悬停:所有
isHover分支删除,用静止色(bg-border-01、stroke-text-02、bg-background-tint-00)与显式Pressable触达; - 搜索头部子标签("Reading" vs "Searching the web")在搜索阶段前退化为通用标签;
expandedText死字段丢弃;memory 提示/模态在记忆阶段前不实现;并行 tab 在 9b.7 实际按所有者决策提前交付为 live(含 Opal pillTabs的 RN 移植,见下文)。
后端与数据契约:零改动
9b 是纯前端阶段:无后端、DB、API 变更。推理数据包已经存在于后端:
ReasoningStart type="reasoning_start"(无字段)、ReasoningDelta type="reasoning_delta" {reasoning: str}、ReasoningDone type="reasoning_done",由backend/onyx/chat/llm_step.py发出,携带Placement.turn_index/tab_index;- 线上没有
message_end——整个 turn 只能通过OverallStop (type="stop")完成; TopLevelBranching {num_parallel_branches}是并行前的元数据。
移动端mobile/src/chat/streamingModels.ts需要补充的是完整的 WebPacketType枚举值(当前文件已含REASONING_START/DELTA/DONE、TOP_LEVEL_BRANCHING、SEARCH_TOOL_*、FETCH_TOOL_*、TOOL_CALL_ARGUMENT_DELTA等),以及引擎/外壳需要解引用的 obj 接口(ReasoningStart/Delta/Done、TopLevelBranching、ToolCallArgumentDelta、MessageStart.pre_answer_processing_seconds等)。策略是枚举现在就补全、各工具的 obj 接口按阶段后补——引擎/助手/集合一次性编译通过,未来永不再改枚举。
交付路线:7 个分层 PR
由于全量外壳移植规模可观(约 3.7–4.3k LOC),PR 路线图将其切成 7 个可独立合并的 PR,前 6 个以 "dark"(已编译 + 单元测试、未上屏)方式合入,直到 9b.7 组合 PR 点亮时间线:
| PR | 关键交付物 | 状态 |
|---|---|---|
| 9b.1 | 分组引擎 + 完整 PacketType 枚举 + 纯步骤助手(单元测试) | dark |
| 9b.2 | usePacketProcessor+usePacedTurnGroups(两个重构 hook) | dark |
| 9b.3 | 六个状态/派生 hook(7 状态机、展开、头部、计时、指标) | dark |
| 9b.4 | render-prop 契约 +findRenderer+RendererComponent,最终答案迁移 | live(答案路径) |
| 9b.5 | rail/surface/content 原语 +StepContainer+TimelineRendererComponent+ 新图标 +StreamingMarkdownmuted 变体 | dark |
| 9b.6 | ReasoningRenderer+reasoningState+ReasoningTextWindow | dark |
| 9b.7 | AgentTimeline外壳 + 头部 + Expanded/Collapsed 内容 +MessageRow接线 +streamingStartedAt | 点亮 + 设备门禁 |
值得注意:9b.7 在正式编码前经历了所有者的四个AskUserQuestion门禁,其中最大的一个决策是并行 tab 提前交付为 live(而非休眠),这需要一个新原语components/ui/tabs.tsx(Opal pillTabs的 RN 移植:Root context +List+Trigger)以及 6 个 1:1 的 Opal 图标移植(stop-circle、branch、user、code、book-open、slow-time)。9b.7 还修复了三个对抗性评审发现的高危问题,其中最重要的是Stopped 路径在线上不可达——用户停止会先于后端stoppacket 中止读取器,导致stopPacketSeen永不置位、turn 停留在 STREAMING 状态;修复方式是runChatStream的finally在 abort 时合成一个USER_CANCELLED停止 packet(buildUserCancelledStopPacket)。另一处修复是FlashList回收 React key 导致滚动后无关消息继承了上一 turn 的展开状态,修复为在renderItem内以nodeId作为MessageRow的 key。同时,水合 turn 此前完全没有时长,任何重开的聊天头部都会显示 "Thought for some time"——后端响应中已有的processing_duration_seconds(backend/onyx/chat/llm_step.py模型侧)被映射进chatHistory解决。
测试策略:纯逻辑承载风险
9b 的全部风险集中在纯核心与 hooks(后端零改动),因此采用RN Testing Library + Jest 单元测试为主,覆盖(详见实施计划):
- 分组/引擎:分组键
"{turn}-{tab}";三个section_end触发(真实 packet、turn 切换关闭先前组、stop 关闭全部打开组);工具 vs 显示分类;finalAnswerComing与 tool-after-message 重置;hasContentPackets;model_index容忍;历史重载重置(数组收缩); - Transformers:
groupStepsByTurn并行检测 + turn/tab 排序; - 节拍(fake timers):第一步立即、后续 200ms 间隔、
stop全刷、历史绕过瞬间显示、答案在节拍完成前被扣住; - 状态 hooks:7 状态全部 + 每个派生布尔量;自动折叠 +
userHasToggled抑制;计时器 tick + 后端时长冻结; - Reasoning:标题抽取(Markdown 标题规则、60 字符上限)+ delta 累积;
ReasoningRenderer500ms 门禁(fake timers)+ 空/未开始分支; - 组件冒烟测试:模拟推理 packet 流渲染 "Thinking" step、流式 Markdown、标记 done、折叠为 "Thought for Ns · 1 step";点击展开;
USER_CANCELLED停止显示 Stopped step; - 硬性设备门禁(所有者执行,不可自动化):开发构建上驱动推理模型,确认流式微光 + 实时计时、答案开始时自动折叠、点击展开、Done 终止步骤、水合(重开历史)渲染显示真实 "Thought for Xs",以及迁移后的答案路径与 9a Sources 完好、流式重渲染保持流畅。
现有行为的变化
- 涉及思考/工具的助手消息现在会在答案上方显示时间线(此前只有静态 "Thinking…" 微光、没有任何步骤);普通无工具答案外观不变(EMPTY → 直接出答案);
- 渲染路径被重构为 Web 的分组/节拍/调度三层:9a 的citations/Sources行为与流式 Markdown 答案保持不变(最终答案迁移到同一契约但渲染效果一致);
- 无后端、DB 或 API 变更;非聊天界面零改动;
- 新增两处小的时序状态依赖:每条消息一个
streamingStartedAt时间戳(实时计时用,runChatStream启动时打点),以及折叠/展开 + reasoning 的 muted Markdown 变体(新 UI)。
参考文档
本阶段的完整设计文档链(从索引进入):
- 01-research.md — 需求、锁定范围、代码库/后端/Web/行业调研、三方案对比与选定(C);
- 02-high-level-design.md — 端到端流程、组件交互图、关键决策(本文主体);
- 03-detailed-design.md — 精确契约、新文件清单、文件树、逐文件职责、集成点、两个 ref 重构、既定分歧;
- 04-implementation-plan.md — CLAUDE.md 格式实施计划 + 六项计划挑战结果(全部通过);
- 05-pr-roadmap.md — 7-PR 交付序列与 9b.7 的 as-built 记录。
对应的仓库源码锚点:Web 端权威实现位于web/src/app/app/message/messageComponents/timeline/(hooks/packetProcessor.ts、AgentTimeline.tsx、StepContainer.tsx、TimelineRendererComponent.tsx、renderers/reasoning/ReasoningRenderer.tsx),组合根在web/src/app/app/message/messageComponents/AgentMessage.tsx;移动端接入点包括mobile/src/chat/streamingModels.ts、mobile/src/chat/messageProcessor.ts、mobile/src/components/chat/AgentTimeline.tsx、mobile/src/components/chat/MessageRow.tsx、mobile/src/components/chat/renderers/registry.ts、mobile/src/hooks/usePacketDisplay.ts;后端推理数据包来自backend/onyx/chat/llm_step.py。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考