首页
/ Onyx 移动端 Chat 引用(Citations & Cited Sources)体验移植:数据包规范、渲染约束与方案选型

Onyx 移动端 Chat 引用(Citations & Cited Sources)体验移植:数据包规范、渲染约束与方案选型

2026-09-09 21:48:10作者:姚月梅Lane

导读

本文整理自 docs/mobile-chat/9a-citations/01-research.md(Onyx 移动端 Chat 子阶段 9a — Citations & Cited Sources 的研究文档),并对照当前仓库中已落地的实现与测试进行佐证。文章聚焦三个问题:后端以怎样的 NDJSON 数据包把"引用编号 → 文档"关系送达客户端;在 React Native 原生 markdown 渲染器没有自定义节点 hook 的约束下,inline [N] 标记如何被样式化、可点按;以及移动端"Sources"按钮 + 底部弹层(bottom sheet)这一引用来源界面该如何设计与选型。读完本文,你将掌握 Onyx 移动端引用的完整数据流、后端权威包规范、三个实现方案(A/B/C)的取舍,以及最终为何选择"可复用基础 + inline 简化"的组合。

需求与背景:把 Web 的引用体验搬到移动端

Onyx 的 Web 端在回答中会把检索到的文档以 [N] 引用标记嵌入答案,并提供可点击的引用来源面板。子阶段 9a 的目标,是把这套体验完整移植到移动端 React Native 应用(mobile/):消费并处理引用/文档的流式数据包,在流式答案中渲染可点按的 [N] 标记,以及一个 "Sources" 来源列表界面——行为与 Web 对齐,结构上尽可能对齐。这是被延期的"丰富聊天(rich-chat)"工作(docs/mobile-chat/05-pr-roadmap.md 中的 PR 9a–9e)的第一个子阶段,Web 端是设计的唯一事实来源。

一个贯穿全文的背景事实:移动端聊天层采用原生方案(非 WebView),因此 Web 端 react-markdown 那种"把任意节点替换为自定义组件"的做法在移动端行不通,这直接决定了 9a 几乎所有关键决策。

三个关键澄清:子阶段、流程与最核心的渲染约束

研究文档以 Q&A 形式锁定了三个前提:

  • Q1 — 属于 PR 9 的哪个子阶段? 9a(citations/sources)。路线图推荐它最先做;而 9b(agent 时间线,即 agent-timeline)是延期的下一阶段。
  • Q2 — 启动流程? 完整的特性流程(研究 → 方案 → 高层设计 → 详细设计 → 实施计划),在 docs/mobile-chat/ 下以门禁文档(gated docs)形式推进,写任何代码之前先被评审。
  • Q3(承重问题)— 移动端 markdown 渲染器没有自定义节点 hook,inline [N] 标记怎么处理? 结论:做成带样式、可点按的 [N] 链接(点按通过 onLinkPress 打开来源);忠实于 Web 的富 UI 放在 Sources 按钮 + 来源弹层里(那里没有渲染器约束)。上标样式可选。而 Web 那种 inline SourceTag 芯片形态需要重度文本分段,会与流式 markdown 渲染器打架——被否决;完全隐藏标记也被否决(会丢掉"论断 → 来源"的绑定关系)。

约束为什么存在(记录下来以免被重新争议)

  • Web:用 react-markdown 渲染答案(JS 元素树),Onyx 重写 a 节点(MemoizedAnchor),把每个 [[N]](url) 链接替换成自定义 SourceTag 芯片 + 悬停卡片。因为每个节点都是可替换的 React 元素。
  • 移动端:用 react-native-streamdownreact-native-enriched-markdown,这是原生渲染器(md4c + 原生文本原语),选择它是为了避免 WebView 开销和流式卡顿。它全部的公开定制面就是 markdownStyle(每个节点的颜色/字重/下划线)+ onLinkPress/onLinkLongPress 点按回调,没有自定义节点渲染器 / 渲染覆盖。所以 inline 位置无法用自定义组件替换 [N] 链接,只能样式化 + 可点按。悬停卡片无论如何也无法移植(触屏没有 hover,点按打开就是等价物)。

这一约束在 mobile/src/components/chat/StreamingMarkdown.tsx 的实现中得到印证:组件只暴露 markdownStyle(通过 buildMarkdownStyle 基于 Onyx 设计 token 解析具体颜色/字重)与 onLinkPress(第 141 行把原生回调的 event.url 转发出去),没有任何自定义节点接口。

现状盘点:移动端 Chat 渲染路径与分发接缝

研究文档对代码库做了逐文件扫描,明确了 9a 要"嫁接"在哪些既有机制上:

渲染路径与分发接缝

  • mobile/src/components/chat/MessageList.tsx:每条消息渲染一个 MessageRow,本身没有按包(packet)分发的逻辑。
  • mobile/src/components/chat/MessageRow.tsxAssistantMessage 从会话状态取得 processed 状态(文档研究时点为 usePacketDisplay(node),其定位是"单组 stub",注释写明 "Core = one group; PR 9 adds real grouping"),先渲染 <AgentTimeline>(外壳),再在 px-12 缩进内渲染匹配到的 Renderer。
  • mobile/src/components/chat/renderers/:分发接缝。MessageRenderer { matches(packets); Component }findRenderer 按"首个匹配"分发,最初只注册了 MessageTextRenderer(研究文档注明 "PR 9 adds rich renderers")。
  • mobile/src/components/chat/renderers/MessageTextRenderer.tsxmatches 判定"存在 chat 类包";accumulateContent(packets)message_start/message_deltacontent 拼接起来 → 交给 useTypewriter<StreamingMarkdown>。当前实现中,它已经给 StreamingMarkdown 传入了 onLinkPress={openUrl}(见 MessageTextRenderer.tsx),即 9a 的 inline 点按路径已经落地。

Markdown 渲染器与打字机揭示

  • mobile/src/components/chat/StreamingMarkdown.tsx:包装 <StreamdownText markdown markdownStyle flavor="github" selectable>StreamdownText 继承 EnrichedMarkdownText 的全部 props(含 onLinkPress/onLinkLongPress),另有 remendConfigmarkdownStyle.link--action-selection-05 + 下划线(全局样式,无法按链接单独定制)。
  • mobile/src/hooks/useTypewriter.ts:按字符前缀切片逐步揭示 target(流中约 180 字符/秒)。这带来一个重要的实现约束:标记文本会作为字符逐个通过,中途可能短暂出现残缺的 [1——所以引用解析必须跑在完整累积内容上,而不是"已显示内容"上。当前实现还包含一个细节:流结束时进入自适应追赶(CATCHUP_FRAMES)模式,以及回到前台时直接跳到全文。

数据包契约与摄取通道

  • mobile/src/chat/streamingModels.tsPacketType 枚举(message_start/delta/endstopsection_enderror 等)、Packet {placement, obj}Placement {turn_index, tab_index?, sub_turn_index?, model_index?}ObjTypes 联合类型。这是新增包类型的扩展点。文档还强调了两级判别约定:外层包用 "obj" in e && "placement" in e 判别(见 mobile/src/api/chat/stream.ts),根级对象(如 message_id_info、流式错误)按字段存在性判别,不要看 obj.type
  • mobile/src/chat/contracts/projects.ts:项目读模型,是 SearchDoc/文档契约(mobile/src/chat/contracts/)的候选归属地。
  • mobile/src/hooks/useChatController.ts:通过防抖 flush 把每一个包裹包追加到 node.packets{...node, packets:[...node.packets, ...pending]}),不检查 obj.type——因此新增的引用/文档包类型到达渲染器时控制器零改动
  • mobile/src/chat/chatHistory.tsprocessRawChatHistory 按助手轮次对齐 packets[agentIdx],历史会话的引用通过这里加载。

可复用的 UI 原语

9a 的来源界面可以直接复用仓库里已有的组件原语:

已确认的缺口与冲突

研究扫描确认了三个必须处理的问题:

  • 来源图标缺口:移动端没有任何 SourceIcon / WebResultIcon / favicon 组件(grep 确认)。Web 用 SourceIcon(source_type → 图标)+ WebResultIcon(URL → favicon)。9a 必须补一个最小版本,完整的约 40 个连接器 logo 集不在范围内。
  • 没有文档预览面:移动端没有文档预览(Web 在 PreviewModal 中打开 File/UserFile 文档)。
  • 路由 /sources/[id] 已被占用:它是 PROJECT-FILES 屏幕(mobile/src/app/(app)/sources/[id].tsx,把 id 当 projectId,复用 useProjectFiles)。deriveFocusmobile/src/chat/chatFocus.ts)只匹配 //chat/…/projects/…。所以聊天引用面必须是底部弹层(或一条全新的独立路由),不能是 /sources
  • 测试缺失:原先没有 renderer / usePacketDisplay 测试,mobile/src/chat/tests/fixtures.ts 只有 makeProjectFile,事实上的包 fixture 内联在 ndjson.test.ts 里。需要补 makePacket / makeCitationPacket / makeSearchDoc(Packet) 辅助函数——这一点在后续实现中已经兑现(见下文"测试覆盖")。

后端数据包规范(权威 wire 形状)

这是 9a 的"接缝"所在,也是移动端必须完全对齐的事实层。格式为 NDJSON,每行一个 Packet

{ placement: { turn_index, tab_index?, sub_turn_index?, model_index? }, obj: { type, ... } }

没有 data: SSE 前缀。移动端的 isPacket = "obj" in e && "placement" in e 正是据此判别。

CitationInfo:唯一的引用包

  • CitationInfo { type: "citation_info", citation_number: int, document_id: str },定义于 backend/onyx/server/query_and_chat/streaming_models.py。注释明确写道:citation_number 是 LLM 提供的编号,document_id 是连接器侧的真实文档 ID(非数据库 int 主键)。
  • Web 的枚举还声明了 citation_start / citation_end——后端从不发送它们,忽略即可。移动端 mobile/src/chat/streamingModels.ts 也以注释形式记录了这一点:"Only citation_info is emitted; citation_start/end are declared for parity but never sent."

文档包(每个 { type, documents: SearchDoc[] }

  • SearchToolDocumentsDeltatype: "search_tool_documents_delta"(内部检索 + Web 检索)。
  • OpenUrlDocumentstype: "open_url_documents"(URL 抓取工具;Web 端叫 FetchToolDocuments,wire 字符串相同)。

message_start.final_documents:权威被引集合

message_start 携带 final_documents: SearchDoc[] | null(本轮被引用文档的权威集合)和 pre_answer_processing_seconds。注意后端 AgentResponseStart 不发送 Web 端那个 id/content 字段(移动端 MessageStart 也只建模了 contentpre_answer_processing_secondsfinal_documents)。

SearchDoc 全字段

后端定义于 backend/onyx/context/search/models.py

字段 类型 说明
document_id str 连接器侧文档 ID
chunk_ind int 块索引
semantic_identifier str 语义标题(列表展示用)
link str | None 文档外链;无链接文档为 null
blurb str 摘要
source_type DocumentSource webgoogle_driveconfluence
boost int 检索加权
hidden bool 是否隐藏文档(仅 admin 检索才可能为 True)
metadata dict[str, str | list[str]] 元数据键值
score float | None 相关分
is_relevant bool | None 相关标记
relevance_explanation str | None 相关性解释
match_highlights list[str] Vespa 高亮片段(如 <hi>the</hi> <hi>answer</hi> is 42
updated_at datetime | None 最后更新
primary_owners / secondary_owners list[str] | None 属主
is_internet bool 是否互联网文档
file_id str | None 文件型文档的文件 ID(populate_file_ids_on_sections 之后才有)

注意:线上没有 db_doc_id(它是 SearchDoc,不是 SavedSearchDoc)。移动端在 mobile/src/chat/contracts/documents.ts 里做了全字段移植(注释明确说明"Full field set — 9b's search/fetch sub-renderers consume the extras, so model it once here")。

终止包是 stop 而非 message_end

终止包是 stop{type:"stop", stop_reason}),没有 message_end。移动端 isComplete 已经以 STOP 为准。

关键事实:inline 标记 [[n]](url) 的链接已烘焙

[backend/onyx/chat/citation_processor.py](https://gitcode.com/GitHub_Trending/da/danswer/blob/2a5439d86a849cdbb543d4034533edc5166806d1/backend/onyx/chat/citation_processor.py?utm_source=gitcode_repo_files#L496-L508) 是 9a 整个 inline 路径的支点:处理器解析出引用编号后,取 link = search_doc.link or "",然后在 HYPERLINK 模式下把标记格式化为 [[{num}]]({link})。由此推出两条结论:

  • Web/有链接文档 → 标记的 URL 就是文档链接 → onLinkPress(event.url) 可以直接打开,点按不需要任何引用状态解析
  • 文件/内部文档(无链接) → 标记是 [[n]]()(空括号)→ 可能根本渲染不成可点按链接(这才是真正的边缘 case)。

同文件还展示了去重逻辑:同一 document_id 只有在首次被引时才会生成 CitationInforecent_cited_documents / cited_document_ids 两个集合共同把关),cited_documents_in_order 维护首次引用顺序——这正是移动端 citations[] 有序数组的后端来源。

单轮回答的包序与去重

研究文档给出的每轮标准顺序:

  1. 文档包先到(搜索工具的轮次内):search_tool_documents_delta / open_url_documents
  2. message_start(携带 final_documents);
  3. 反复出现:citation_info 紧随其后就是携带对应 [[n]](url) 文本的 message_delta
  4. section_end
  5. stop

去重规则:一个 document_id 只在首次被引时发 citation_info。这对移动端处理器意味着:citations[] 天然按首次引用顺序去重排列,citationMap 是"编号 → 文档 ID"的覆盖映射。

Web 端结构对齐目标(parity target)

Web 的 packetProcessor.ts 通过 handleCitationPacket/handleDocumentPacket 就地、增量地维护三份状态(nextPacketIndex 游标保证只处理新增,数组收缩时重置):citationMap{[n]: document_id})、citations[](经 seen-set 去重、按首次引用顺序)、documentMapMap<document_id, doc>)。三个表面都从这份状态取数:

  • (A) inline 标记:答案 markdown 中的 [N]MemoizedAnchor 解析 → citationMap → 文档 → SourceTag 芯片 + 悬停卡片。(移动端:改为带样式的可点按链接。)
  • (B) "Sources" 工具栏按钮MessageToolbarSourceTag variant="button"):最多 3 个来源图标的 IconStack + "Sources" 标签,当 citations.length>0 || documentMap.size>0 时显示,点按打开 (C)。
  • (C) 引用来源列表DocumentsSidebar):移动 Web 上已经是底部 Modal,标题就是 "Sources"。分节:Cited Sources(按引用顺序)/ More(或 "Found Sources")/ User Files。每行 = ChatDocumentDisplay:[来源类型图标或 favicon + 标题(semantic_identifier)截断] / [元数据:更新时间徽标 + ≤3 个元数据 chip] / [来自 match_highlights || blurb 的片段]。行点按 → openDocument
  • openDocumentweb/src/lib/search/utils.ts):有 link → 浏览器打开;File/UserFile(无链接)→ 预览弹窗;否则 no-op。

移动端 9a 的三种状态名与三个表面,正是对这一结构的逐一移植。

移动端引用 UX 行业最佳实践

研究文档调研并沉淀了以下移动端引用体验的最佳实践(这也是 9a 设计的 UX 依据):

  • 保留数字 [N] 标记(RAG 级模式;Perplexity、Claude 都在用)——工作是样式 + 点按目标,而不是改成内联超链接。
  • 标记必须是真正可聚焦/可点按的元素,具备"点按即开"行为(触屏没有 hover 兜底)。
  • 点按目标 ≥ 44×44 pt(Apple HIG)/ WCAG 2.5.8 24px 下限——用 hitSlop/padding 扩大命中区,而不是只给一个小小的字形。
  • 来源 = 可折叠的 "Sources (N)" / 底部弹层 / 横向卡片滚动,而不是桌面侧栏;卡片展示 favicon + 标题 + 域名 + ≤200 字符片段。
  • 点按默认用应用内浏览器打开expo-web-browser openBrowserAsync → SFSafariViewController / Chrome Custom Tabs),而不是外部 Safari/Chrome;无 URL 的内部文档用预览弹层。
  • 流式:从累积答案文本解析标记([12] 可能跨 delta 被切开);容忍前向引用(标记先于其来源到达),未解析前渲染为惰性。
  • 无障碍:每个标记要有描述性 accessibilityLabel("Source N: {title}"),而不是裸数字;accessibilityRole="link"
  • NN/g 研究:用户很少点击引用,但引用的存在会驱动(过度)信任——来源卡片要以标题 + 域名开头(有意义的标签),标记放在论断旁边。

三个实现方案与选型

方案 A — Simplicity-First:扩展 MessageTextRenderer,不引入新框架

引用数据沿用现有 node.packetsusePacketDisplay 已经把所有包交给唯一匹配的 MessageTextRenderer,所以 registry / usePacketDisplay / controller / history 全部零改动。只需扩展 MessageTextRenderer:(a) 把 onLinkPress 透传给 StreamingMarkdown;(b) 用一个纯函数 deriveCitations(packets)(每次 flush 用 useMemo 重跑,和 accumulateContent 一样)驱动 Sources 栏 + 底部弹层。inline 路由不需要引用状态——标记 URL 是预烘焙的,onLinkPress(event.url) 直接打开;空 URL 的文件标记路由到打开 Sources 弹层。新增约 4 个文件、4 处小改动。

  • 代价:活动部件最少、最快上线、风险最低;但引用逻辑被困在 MessageTextRenderer 里(9b 分组时要搬家),deriveCitations 每次 flush 全量重扫(聊天规模下可接受),且没有为 9b 建共享来源层。
  • 风险:空括号 [[n]]() 文件标记可能解析不成链接(缓解:一行归一化 ]]()]](#cited));来源图标缺口用公共 favicon expo-image + 通用兜底补齐。

方案 B — Streaming-Robustness / Performance-First:增量处理器 + 标记变换

核心是一个纯增量 citationProcessor.ts(ref 型 useCitationProcessornextPacketIndex 只处理新增、就地变更、用 citationCount/docCount/resolveVersion 做变更代理),让 2000 token / 30 引用的答案永不重解析;Sources 栏/弹层是 memoized 兄弟组件,只在计数变化时重渲染,绝不逐 token 渲染。纯函数 transformCitationMarkers(displayed, resolve) 把已解析的 [[n]](url) 重写为受控的 onyxcite://n href,裁剪残缺的尾部标记,把前向引用降级为惰性纯文本;点按路由解析 n → doc → openDocument

  • 代价:确定性的流式边界 + O(新增包) 处理 + memoized UI;但这套鲁棒性机器在很大程度上被"URL 预烘焙"这一事实削弱(文档包先于答案到达,且标记自带 URL,前向引用门控收益很小),onyxcite:// 重写还引入了脆弱的逐帧变换。
  • 风险:渲染器可能过滤自定义 onyxcite:// scheme;部分标记正则的边缘 case([not a cite]、真正的 [[x]](y) 链接);上标可能无法表达。

方案 C — Flexibility / Reusable-Foundation:一个处理器 + processed 通道 + 共享来源层

引入 9b–9e 都会复用的最小持久接缝:(a) 纯增量 messageProcessor.ts(Web 单个 packetProcessor 的移动移植:游标 + 收缩重置),9a 填充 citationMap/citations[]/documentMap,9b 在此基础上扩展分组/steps;(b) 扩展 usePacketDisplay 来宿主处理器,MessageRendererProps 携带 processed 状态(不只是原始包)——即 9b 的时间线/搜索/工具渲染器要读的通道;(c) 共享来源层——SearchDoc 契约、SourceIcon/WebResultIconSourceRowopenSource 路由——9a 的弹层和 9b 的搜索/抓取子渲染器共用。分组本身现在不建(那是 9b);9a 状态保持扁平/无分组,让 9b 的分组设计不受约束。inline 走同一条 onLinkPressresolveCitationHrefopenSource

  • 代价:9b 接入时渲染器管道零重写(Web 证明了单处理器路径),SourceRow/SourceIcon/SearchDoc/openSource 在 9a–9b 各被复用 3–4 次;前置成本是一个小的纯模块 + 一个 hook 改动 + 一个 prop 增加 + 几个组件。每个接缝都由具体、临近的 9b 消费者背书(不是臆测)。
  • 风险:对 9b 分组形态的猜测错误(已最小化——9a 不承诺任何 turn/tab 分组);SearchDoc 建模不足(缓解:现在就移植全字段);resolveCitationHref 是唯一脆弱映射(隔离 + 单测)。

跨方案比较

  • inline 点按路径:A 的洞察(预烘焙标记 URL + 文档包先于答案)让常见 Web 文档场景变得平凡,同时削弱了 B 的前向引用 / onyxcite:// 机器——B 真正有用的内核(增量游标)在 C 里本来就有(Web 里也有)。所以 B 的额外部分在聊天规模下大多不划算。
  • 与路线图的对齐:9b 紧接着要做,且明确需要一个可扩展的包处理器(分组)+ 渲染 SearchDoc 来源行的搜索/抓取渲染器——也就是 C 的共享层 + processed 通道。A 是把引用逻辑临时塞进 MessageTextRenderer,偏离 Web 的单处理器结构,9b 落地时被迫搬家。
  • 抽取策略张力:移动端策略是"证明可复用才抽取,而非提前抽取"。C 的复用对象都是具名且临近的(9b),不是臆测——所以 C 不算过度工程;但如果 9b 很远,A 才是诚实的选择。
  • 失败模式:三者共享空括号 [[n]]() 文件标记风险和来源图标缺口;C 额外带一个(已最小化的)分组形态猜测;B 额外带自定义 scheme + 正则风险。

选定方案:C + A 的 inline 简化(GATE 1 通过)

Approach C(可复用基础),采纳 A 的 inline 简化。 理由:C 匹配 Web 的真实结构(一个增量包处理器 + 给渲染器的 processed 状态通道),而 9b(agent 时间线)就是下一个阶段,它需要的正是这些:一个可扩展分组/步骤的处理器,以及 9b 搜索/抓取子渲染器复用的共享来源层(SearchDoc + SourceIcon/WebResultIcon/SourceRow/openSource)。A 会在 9b 落地时被迫把引用逻辑搬家;B 建的流式鲁棒性机器被"标记 URL 预烘焙"事实基本架空。

从 A 嫁接的(inline 简化):inline 点按路径不解析引用状态——标记 URL 预烘焙([[n]](link)link = search_doc.link or ""),onLinkPress(event.url) 直接打开 Web/有链接文档;空 URL 的文件标记降级为打开 Sources 弹层。因此完全砍掉 B 的前向引用门控和 onyxcite:// 标记重写变换

净形态:C 的可复用接缝(处理器 + processed 通道 + 共享来源层)+ A 的轻量 inline 路径 + 零 B 的重写/门控复杂度。

明确推迟到 9b(现在不建):turn/tab 包分组、时间线步骤、推理/搜索/工具子渲染器。9a 的处理器状态保持扁平(citationMapcitations[]documentMap、完成态),让 9b 的分组设计不受约束。

当前仓库中的落地印证(源码与测试佐证)

研究文档标注为 draft,但其结论在仓库中已基本实现为代码,可作为选型的实证:

  • 纯增量处理器mobile/src/chat/messageProcessor.ts 就是 Web packetProcessor 的移动移植:ProcessedMessageState 携带 nextPacketIndex 游标、citationMapcitations[] + seenCitationDocIds 去重集合、documentMapisComplete/stopReasonprocessPackets 在数组收缩时重建状态("regenerate / reload"),只处理游标之后的新包,并保持数组引用稳定以服务 memoized 消费者。handleCitationPackethandleDocumentPacket 严格对应后端三类包(citation_info、两种文档包、message_start.final_documents),其中 upsertDocumentsdocument_id 做 upsert 覆盖。
  • 契约与路由mobile/src/chat/contracts/documents.ts 移植了 SearchDoc 全字段(含 file_idprimary_owners 等),并定义了 StreamingCitation(注意与 wire 包的 citation_number 字段名差异)和 CitationMapmobile/src/chat/openSource.ts 实现 openUrlexpo-web-browser 应用内浏览器,仅接受 http/https)与 openSource(有链接 → 浏览器;有 file_id → toast "Preview isn't available on mobile yet.";否则 no-op),正是选型文档里"openSource 路由"的落地。
  • 纯选择器mobile/src/chat/citations.ts 提供 selectSources(把文档拆成 Cited / More / Files 三组,iconDocs 取前 3 个用于按钮图标栈,count 驱动 "Sources · N")以及 domainOf / faviconUrl(Google 公共 favicon 服务,无需鉴权)。
  • UI 落地mobile/src/components/chat/CitedSources.tsx 实现了 CitedSourcesBarPressableaccessibilityRole="button"accessibilityLabel 形如 Sources, ${count},图标栈 + "Sources · N")与 CitedSourcesSheetSheet 标题 "Sources",三个分区:Cited Sources / More(无被引时显示 Found Sources)/ User Files,行点击 openSource);SourceRow.tsx 展示图标 + 标题 + domainOf(link) ?? source_type · 相对时间 + ≤200 字符摘要(并剥离 Vespa <hi> 高亮标记);SourceIcon.tsx 用普通 expo-image 加载 favicon、失败回退通用文件图标。MessageRow.tsxprocessed.isComplete 后通过 selectSources 计算并渲染 Sources 脚注(见 MessageRow.tsxCitedSourcesBar/CitedSourcesSheet 的挂载处)。
  • 测试覆盖mobile/src/chat/tests/messageProcessor.test.ts 验证了 citationMap 构建与首次引用顺序去重、两种文档包 + final_documents 的 upsert、stop 置完成、跨 flush 只处理新增、数组收缩重置等关键行为;mobile/src/chat/tests/fixtures.ts 提供了研究文档点名要的 makePacket / makePlacedPacket / makeCitationPacket / makeSearchDoc / makeSearchDocsPacket 辅助函数;mobile/src/components/chat/tests/CitedSources.test.tsxcreateInitialState + processPackets 构造真实状态,断言弹层渲染 "Cited Sources" 分区与 "Sources · N" 按钮计数及 onPress 回调。

结论与后续(9b)

9a 的最终形态可以一句话概括:后端零改动(引用/文档数据早已存在于既有 NDJSON 流中),移动端以"一个纯增量处理器 + processed 状态通道 + 一组共享来源组件"为骨架,inline 标记借预烘焙 URL 走 onLinkPress 直接打开,富 UI 全部收敛到 Sources 按钮 + 底部弹层。这既完成了 Web 引用体验的移动端移植,又为下一阶段 9b(agent 时间线) 铺好了处理器与来源层的可扩展接缝。9b 将在此之上增加 turn/tab 分组、时间线步骤以及搜索/抓取子渲染器——而 9a 已确保这些扩展不需要推翻任何渲染器管道。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
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
docsdocs
暂无描述
Markdown
899
5.83 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
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
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