news 2026/9/10 2:25:43

Onyx 移动端输入栏控件(ActionsPopover + Deep Research 开关)详细设计解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Onyx 移动端输入栏控件(ActionsPopover + Deep Research 开关)详细设计解析

Onyx 移动端输入栏控件(ActionsPopover + Deep Research 开关)详细设计解析

【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer

本文是 Onyx(原 Danswer)移动端聊天输入栏控件改造的详细设计(Detailed Design)文档解读。它聚焦于一个明确的工程目标:把 Web 端聊天输入栏的工具栏控件移植到 React Native 移动端——包括深度研究(Deep Research)开关胶囊锚定式 ActionsPopover 弹层(强制使用某个工具、启用/禁用工具、以及知识源选择子视图)。读者读完本文后,将掌握移动端如何在零后端改动的前提下,复用/persona已下发的工具与知识源数据、通过四个新增请求字段打通发送链路,以及如何以"Web 优先(Web-Parity-First)"为原则,用PopoverSelectButtonSwitch三个新原语复刻 Web 交互细节,并理解其中平台驱动的差异点与实现风险。

该设计文档属于 docs/mobile-chat/input-bar-controls/ 系列(00-index → 01-research → 02-high-level-design → 03-detailed-design → 04-implementation-plan → 05-pr-roadmap)的第三篇,处于"详细设计"阶段,对应仓库路径 03-detailed-design.md。


一、设计总览:零后端改动,纯前端移植

本设计的核心前提是**"Web 端是移植的事实来源(source of truth),移动端在观感与结构上对齐 Web,并记录平台驱动的差异"**(即文档中定义的 Approach B — Web-Parity-First)。

最关键的负载性事实(load-bearing facts)在设计文档与源码中均可得到验证:

  • /persona接口返回的 Agent 数据已经携带toolsknowledge_sources字段,移动端无需新增目录获取请求;
  • 后端SendMessageRequest已经接受deep_researchallowed_tool_idsforced_tool_idinternal_search_filters四个发送字段;
  • 每个 Agent 的disabled_tool_ids已经有对应的数据表与GET/PATCH接口。

因此结论非常明确:没有后端工作,没有数据库迁移,全部工作集中在移动端(mobile/目录,React Native + Expo)。

二、数据库设计:复用既有表,不做任何 Schema 变更

设计文档明确给出:N/A — no schema changes。唯一的持久化状态是每个 Agent 的disabled_tool_ids,而该数据通路已完整存在:

位置说明
backend/onyx/db/models.py 中Assistant__UserSpecificConfig__tablename__ = "assistant__user_specific_config"复合主键(assistant_id, user_id)disabled_tool_ids: ARRAY(Integer) NOT NULL,两个外键均带ondelete="CASCADE"
迁移backend/alembic/versions/b329d00a9ea6_adding_assistant_specific_user_.py该表已存在并被迁移
DB 层backend/onyx/db/user_preferences.pyget_all_user_assistant_specific_configs(按 user_id 查询全部配置);update_assistant_preferences(upsert:已存在则更新disabled_tool_ids,否则新建行后db_session.commit()
接口backend/onyx/server/manage/users.py两个接口均要求Permission.BASIC_ACCESSGET /user/assistant/preferences返回UserSpecificAssistantPreferences(即dict[int, {disabled_tool_ids: list[int]}]);PATCH /user/assistant/{assistant_id}/preferences,请求体为UserSpecificAssistantPreference {disabled_tool_ids: list[int]},返回 200 空 body

源码佐证:update_assistant_preferences的 upsert 逻辑(backend/onyx/db/user_preferences.py)与两个路由的完整实现(backend/onyx/server/manage/users.py)与设计文档描述完全一致。

四个发送字段已存在于后端请求模型

设计文档指出SendMessageRequest上已有的四个发送字段,源码确认如下(backend/onyx/server/query_and_chat/models.py):

allowed_tool_ids: list[int] | None = None forced_tool_id: int | None = None file_descriptors: list[FileDescriptor] = [] internal_search_filters: BaseFilters | None = None deep_research: bool = False

其中internal_search_filters的类型BaseFilters定义在 backend/onyx/context/search/models.py,其source_type: list[DocumentSource] | None = None等其余字段默认为None——这正是设计文档"最小化传输形状"(只发送{ source_type: [...] },其余字段后端默认 null)的依据。

三、类 / 接口设计:移动端新契约

3.1 聊天层新契约(mobile/src/chat/

设计文档给出了三组核心 TypeScript 契约,全部"镜像"Web 端:

工具快照与工具判定(tools.ts,对应 Web 的web/src/lib/tools/interfaces.tsTier-2 子集)

export interface ToolSnapshot { id: number; name: string; display_name: string; description: string; in_code_tool_id: string | null; // 匹配 SEARCH_TOOL_ID 等 mcp_server_id: number | null; // MCP 工具在 Tier-2 从列表排除 chat_selectable: boolean; // 可见性过滤 }

内置工具标识常量(取自 Web 的web/src/app/app/components/tools/constants.ts):

export const SEARCH_TOOL_ID = "SearchTool"; export const WEB_SEARCH_TOOL_ID = "WebSearchTool"; export const IMAGE_GENERATION_TOOL_ID = "ImageGenerationTool"; export const FILE_READER_TOOL_ID = "FileReaderTool"; // 始终从列表隐藏 // (PYTHON_TOOL_ID, OPEN_URL_TOOL_ID, CODING_AGENT_TOOL_ID 用于图标映射)

工具判定函数族

export function isSearchTool(t: ToolSnapshot): boolean; // in_code_tool_id === SEARCH_TOOL_ID export function hasSearchToolsAvailable(tools: ToolSnapshot[]): boolean; // Search 或 WebSearch 存在 export function displayableTools(tools: ToolSnapshot[]): ToolSnapshot[]; // chat_selectable、排除 MCP 与 FileReader export function computeAllowedToolIds(tools, disabledToolIds): number[] | null; // agent 工具 − 被禁用的工具;无禁用时返回 null(后端视 null = 允许全部) export const getIconForToolId: (inCodeToolId: string | null) => IconFunctionComponent;

知识源与搜索过滤(sources.ts,对应 Web 的ValidSources+ 源元数据 Tier-2 子集)

export type DocumentSource = string; // snake_case 线格式值 ("web","google_drive",…) export interface SourceMeta { icon: IconFunctionComponent; displayName: string; } export const SOURCE_META: Record<DocumentSource, SourceMeta>; // 兜底 → 通用 globe/file export function getSourceMeta(s: DocumentSource): SourceMeta; export function buildInternalSearchFilters(selectedSources): InternalSearchFilters | null; // → { source_type } | null

线格式新增类型(mobile/src/api/chat/stream.ts

export interface InternalSearchFilters { source_type: DocumentSource[] | null; }

最终解析后的发送载荷(提交给submit()的聚合对象)

export interface ChatToolOptions { deepResearch: boolean; allowedToolIds: number[] | null; forcedToolId: number | null; internalSearchFilters: InternalSearchFilters | null; }

3.2 新 UI 原语(mobile/src/components/ui/

select-button.tsx—— 有状态胶囊按钮(镜像 Opal 的 SelectButton,状态驱动、无 hover):

type SelectState = "empty" | "selected"; type SelectVariant = "select-light"; // Tier-2 唯一需要的变体 interface SelectButtonProps { icon?: IconFunctionComponent; children?: string; // 标签 state?: SelectState; // 默认 "empty" variant?: SelectVariant; // 默认 "select-light" foldable?: boolean; // 折叠时隐藏标签(仅图标) disabled?: boolean; onPress?: () => void; accessibilityLabel?: string; }

配套的select-button.styles.ts提供SELECT_COLORS: Record<SelectVariant, Record<SelectState, Record<"rest"|"active"|"disabled", {bg;fg;icon}>>>颜色矩阵,以及resolveSelectState(disabled, pressed)状态解析函数——镜像button.styles.tsresolveButtonState,但去掉 hover(移动端没有 hover)。

switch.tsx—— 开关(镜像 Opal Switch):

interface SwitchProps { checked: boolean; onCheckedChange: (checked: boolean) => void; disabled?: boolean; accessibilityLabel?: string; }

轨道 32×18rounded-full;滑块 14×14;选中时轨道背景为action-link-05;滑块位移用 reanimated 动画。

popover.tsx—— 锚定浮动面板(Portal + reanimated +measureInWindow):

interface PopoverProps { open: boolean; onClose: () => void; anchorRef: RefObject<View>; // 触发组件,打开时测量 width?: number; // 默认 240(对应 Web "lg" = w-60) children: ReactNode; }

从底部停靠的触发组件向上打开;钳制在屏幕内;打开时调用Keyboard.dismiss()

3.3 状态 Hook(mobile/src/hooks/)与状态提供器(mobile/src/state/

Hook职责持久性
useDeepResearchToggle({ chatSessionId, agentId })深度研究开关;完全复刻 Web 的 reset 语义(ref 守卫:仅当previousId !== null && previousId !== chatSessionId时重置;agentId变化时总是重置)。移植自web/src/hooks/useDeepResearchToggle.ts(55 行)临时(ephemeral)
useForcedTools({ agentId })单元素强制语义;forcedToolIdtoggleForcedTool(id)clear();切换 agent 时重置临时
useAgentPreferences()disabledToolIdsFor(agentId)/setDisabledToolIds(agentId, ids)(乐观更新 + PATCH + invalidate);TanStack Query 以serverUrl为 key 请求GET /user/assistant/preferences服务端持久化
useConnectorSources()GET /manage/connector-statusBasicCCPairInfo[].map(c => c.source)去重(federated/federated为 EE 专属,Tier-2 暂缓)可缓存
useSourceSelection({ agentId, availableSources, hasSearchTool })每个 agent 的临时源选择;isEnabled(s)/toggle(s)/enableAll()/disableAll()/initialized;availableSources 首次非空时自动初始化为全部临时

状态聚合器ComposerToolsProvider.tsx(context hub):

useComposerTools(): { ...triggers/state for InputBar + ActionsPopover... resolveToolOptions(): ChatToolOptions; // submit() 消费的对象 }

它挂载上述四个 Hook(以${sessionId}:${projectId}+agentId为 key),并对外暴露resolveToolOptions()

{ deepResearch, allowedToolIds: computeAllowedToolIds(tools, disabled), forcedToolId, internalSearchFilters: buildInternalSearchFilters(selectedSources) }

四、新增文件与目录结构

设计文档给出了完整的新增文件清单(新文件按职责):

文件职责
mobile/src/components/ui/popover.tsx锚定浮动面板原语(测量触发组件、Portal、向上打开、键盘处理)
mobile/src/components/ui/select-button.tsx有状态胶囊原语(empty/selected、可折叠);支撑深度研究 + 强制工具胶囊
mobile/src/components/ui/select-button.styles.tsSELECT_COLORS矩阵 +resolveSelectState(镜像button.styles.ts
mobile/src/components/ui/switch.tsx轨道+滑块开关(reanimated);源行使用
mobile/src/components/chat/ActionsPopover.tsx工具菜单:组合Popover+ 主列表 + 源子视图;持有open/subView
mobile/src/components/chat/ActionLineItem.tsx单工具行:点击=强制、尾部启用/禁用 + 下钻箭头
mobile/src/components/chat/SourceSwitchList.tsx二级视图:返回 + 全部启用/全部禁用 +Switch
mobile/src/components/chat/SourceIcon.tsxDocumentSource→ logo/字形(使用SOURCE_META
mobile/src/components/chat/ToolbarControls.tsxInputBar渲染深度研究胶囊 + 强制工具胶囊 + Actions 触发按钮
mobile/src/chat/tools.tsToolSnapshot类型、工具 id 常量、判定函数、getIconForToolId
mobile/src/chat/sources.tsDocumentSourceSOURCE_METAbuildInternalSearchFilters
mobile/src/hooks/useDeepResearchToggle.ts临时深度研究状态(Web hook 移植)
mobile/src/hooks/useForcedTools.ts单强制状态,agent 变化时重置
mobile/src/hooks/useAgentPreferences.ts每 agentdisabled_tool_ids的 GET/PATCH
mobile/src/hooks/useConnectorSources.tsGET/manage/connector-status→ 源列表
mobile/src/hooks/useSourceSelection.ts每 agent 临时源选择 + 搜索耦合
mobile/src/api/chat/agentPreferences.tsgetAgentPreferences()/patchAgentPreferences(agentId, ids)
mobile/src/api/chat/connectors.tsgetConnectorSources()(connector-status 获取)
mobile/src/state/ComposerToolsProvider.tsxContext 中枢,聚合四个字段;resolveToolOptions()
mobile/src/icons/{hourglass,globe,cpu,link,server,plug,unplug,slash}.tsx8 个新 SVG 图标(精确的 Web path 数据见 04-implementation-plan)

被修改的文件

文件变更
mobile/src/chat/agents.ts扩展MinimalAgent,增加tools: ToolSnapshot[]knowledge_sources: DocumentSource[]
mobile/src/api/settings.tsWorkspaceSettings增加deep_research_enabled?: boolean
mobile/src/api/chat/stream.tsSendMessageBody增加allowed_tool_ids?forced_tool_id?internal_search_filters?;新增InternalSearchFilters类型
mobile/src/hooks/useChatController.tssubmit(text, files?, onAccepted?, toolOptions?);替换硬编码的deep_research: false:296)并填充三个新字段(:291
mobile/src/components/chat/InputBar.tsx在左侧簇(:119-127)渲染<ToolbarControls>;接收 agent + toolbar props
mobile/src/components/chat/ChatSurface.tsx包裹ComposerToolsProvider;传入liveAgent.tools;将resolveToolOptions()贯穿到sendWithAttachmentssubmit
mobile/src/api/query-keys.ts新增agentPreferencesconnectorSourceskey(以serverUrl为 key)

完整目录树(新增部分)

mobile/src/ ├── components/ │ ├── ui/ │ │ ├── popover.tsx (new) │ │ ├── select-button.tsx (new) │ │ ├── select-button.styles.ts (new) │ │ ├── switch.tsx (new) │ │ ├── button.tsx / button.styles.ts (SELECT_COLORS 的参照) │ │ └── line-item-button.tsx (复用 — rightChildren 插槽已存在) │ └── chat/ │ ├── ActionsPopover.tsx (new) │ ├── ActionLineItem.tsx (new) │ ├── SourceSwitchList.tsx (new) │ ├── SourceIcon.tsx (new) │ ├── ToolbarControls.tsx (new) │ ├── InputBar.tsx (modified: 左侧簇渲染 ToolbarControls) │ ├── ChatSurface.tsx (modified: ComposerToolsProvider + 贯穿选项) │ └── FilePickerSheet.tsx (unchanged — 独立的回形针底表) ├── chat/ │ ├── tools.ts (new) │ ├── sources.ts (new) │ └── agents.ts (modified: 扩展 MinimalAgent) ├── hooks/ │ ├── useDeepResearchToggle.ts (new) │ ├── useForcedTools.ts (new) │ ├── useAgentPreferences.ts (new) │ ├── useConnectorSources.ts (new) │ ├── useSourceSelection.ts (new) │ └── useChatController.ts (modified: submit toolOptions + body 构建) ├── api/ │ ├── chat/ │ │ ├── agentPreferences.ts (new) │ │ ├── connectors.ts (new) │ │ └── stream.ts (modified: SendMessageBody + InternalSearchFilters) │ ├── settings.ts (modified: deep_research_enabled) │ └── query-keys.ts (modified) ├── state/ │ └── ComposerToolsProvider.tsx (new) └── icons/ ├── hourglass.tsx globe.tsx cpu.tsx link.tsx (new) └── server.tsx plug.tsx unplug.tsx slash.tsx (new)

五、每个文件的实现要点

popover.tsx

打开时调用anchorRef.current.measureInWindow((x,y,w,h)=>…)记录触发组件矩形;渲染一个全屏<Portal name="actions-popover">,内含透明的外部点击Pressable(点击关闭)与Animated.View面板。定位公式:

  • bottom = windowHeight - anchorY + GAP
  • left = clamp(anchorX, GUTTER, screenW - width - GUTTER)
  • maxHeight = anchorY - insets.top - GAP,内容放入ScrollView

打开时Keyboard.dismiss();reanimated 实现FadeIn+ 从底部原点的轻微translateY/scale。镜像 Opal 的Popover.Content side="bottom" align="start" width="lg"web/lib/opal/.../popover/components.tsx),但翻转为向上打开——这是文档记录的平台差异(divergence)。

select-button.tsx+.styles.ts

Button的方式构建,但使用有状态矩阵SELECT_COLORS[variant][state][colorState](单元格为{bg,fg,icon}),resolveSelectState(disabled, pressed)(无 hover)。select-light变体所有状态透明背景:empty状态 fg=text-04/icon=text-03selected状态 fg/icon=action-link-05(取自stateful/styles.css:228-316)。

foldable折叠为仅图标——由于移动端没有:hover,实现为条件标签渲染,由state/按压驱动(Web 的foldable={!enabled}语义 = 关闭时仅图标、开启时显示标签;深度研究场景恰好需要这种表现,无需 hover 展开)。图标 16px(iconWrapperlg = 1rem)。

switch.tsx

role="switch"Pressable轨道(32×18,rounded-full),reanimated 滑块(14×14)在2px17px间平移。轨道背景background-tint-03→ 选中action-link-05;禁用变体遵循switch/styles.css。受控于checked/onCheckedChange

ActionsPopover.tsx

组合Popover;本地subView: {type:"sources"} | null(镜像 Web 的secondaryView)。主视图:displayableTools(agent.tools).map(t => <ActionLineItem/>)。二级视图:<SourceSwitchList/>。读写useComposerTools。面板内原地替换内容并带滑动动画(reanimatedLinearTransition),保持同一面板与锚点。

ActionLineItem.tsx

单行LineItemButtonselected={forcedToolId===tool.id}onPress=强制切换(搜索工具尚未被强制时 → 打开源子视图,镜像 WebActionLineItem.tsx:98-108)。rightChildren:尾部启用/禁用控件(常显SwitchSvgSlash的图标Button——移动端无 hover,无法"悬停显示")+ 部分搜索源时的EnabledCount文本 + 搜索下钻的SvgChevronRight。禁用工具渲染为暗淡样式(移动端LineItemButton无删除线——用color="muted"+ 删除线样式,记录为平台差异)。

SourceSwitchList.tsx

返回箭头头部(SvgChevronLeftButton)+ 全部启用/全部禁用LineItemButtonSvgPlug/SvgUnplug)+ 每源行(leading=<SourceIcon>、标签、rightChildren=<Switch>)。镜像SwitchList.tsx:61-119。(搜索框省略——Tier-2 范围。)

ToolbarControls.tsx

按顺序渲染:Actions 触发按钮(SvgSlidersButtonref作为 popover 锚点)——当displayableTools(agent.tools).length>0时;深度研究SelectButtonSvgHourglassstate=on?"selected":"empty"foldable={!on})——当deep_research_enabled && hasSearchToolsAvailable(agent.tools)时;强制工具胶囊SelectButtonstate="selected",工具图标+名称,点击移除)。挂载<ActionsPopover>

API 薄封装

agentPreferences.ts/connectors.ts是薄apiFetch包装(路径为裸路径——getBaseUrl()已追加/api):

apiFetch("/user/assistant/preferences") apiFetch(`/user/assistant/${id}/preferences`, {method:"PATCH", body}) apiFetch("/manage/connector-status")

8 个新图标

react-native-svg移植 Web 的 path 数据(viewBox0 0 16 16link除外 =0 0 17 9+rotate(315deg)),精确数据在04-implementation-plan.md中逐字捕获。

六、集成点(Integration Points)

设计文档列出了所有与现有代码的衔接点:

  • InputBar.tsx:119-148— 左侧簇(flex-row items-center gap-8,当前仅回形针)在回形针后新增<ToolbarControls agent={…} tools={…}/>。右侧簇(发送/停止)不变。
  • ChatSurface.tsx— 子树包裹<ComposerToolsProvider sessionId agentId agent={liveAgent}>sendWithAttachments调用submit(text, descriptors, onAccepted, resolveToolOptions())
  • useChatController.ts:228,291-298submit增加第 4 个参数toolOptions?: ChatToolOptions;body 字面量设置deep_research: toolOptions?.deepResearch ?? falseallowed_tool_idsforced_tool_idinternal_search_filtersrunChatStream原样转发body
  • stream.tsSendMessageBody扩展;JSON 原样序列化。
  • useAgents()/useLiveAgent— 无需改动;MinimalAgent扩展后,liveAgent.tools/knowledge_sources自动解析(数据已在/persona线上,见 backend/onyx/server/features/persona/models.py)。
  • useWorkspaceSettings()— 从现有/settingsGET 读取deep_research_enabled(backend/onyx/server/settings/models.py)。
  • app/_layout.tsx:71— 现有<PortalHost/>承载 popover;无需改动。
  • @onyx-ai/shared本次功能不涉及。契约原生存在于mobile/src/chat/(符合"聊天层原生而非共享"的既定决策,参见mobile/src/chat/contracts/projects.ts先例)。共享抽取推迟到未来有成熟复用需求时。

端到端流程验证

对应高层面设计(02-high-level-design.md)的调用链:

GET /persona ──► agent.tools[], agent.knowledge_sources[](已在线,仅需扩展类型) ChatSurface: useLiveAgent + useWorkspaceSettings └─ ComposerToolsProvider(以 session+agent 为 key 的状态中枢) ├─ useDeepResearchToggle → deepResearchEnabled(临时) ├─ useForcedTools → forcedToolId(临时) ├─ useAgentPreferences → disabledToolIds ◄──► GET/PATCH /…/agent-preferences └─ useSourceSelection → selectedSources(临时) └─ resolveToolOptions() → { deep_research, allowed_tool_ids, forced_tool_id, internal_search_filters } └─ InputBar: [paperclip] [Actions ▸] [DeepResearch pill] [forced-tool pills…] [send] └─ ActionsPopover ──► PortalHost(锚定) ├─ ActionLineItem 行(强制 + 启用/禁用 + 箭头) └─ SourceSwitchList(返回 + 全部启用 + Switch 行) 发送:useChatController.submit(text, files, onAccepted, toolOptions) └─ 构建 SendMessageBody(+4 字段)→ runChatStream → POST /chat/send-chat-message

七、实现前必读的重要注意事项

设计文档用整节篇幅列出实现前的关键风险与约束,这些是移植成功与否的分水岭:

7.1 键盘邻近底部栏上的锚定弹层是 #1 风险

锚定弹层位于键盘邻近的底部停靠栏,是最大风险点。要求:

  • 向上打开bottom锚定数学);打开时Keyboard.dismiss()
  • left/width钳制到屏幕内;maxHeight上限 + 滚动;
  • keyboardWillShow/Hide与屏幕旋转时重新测量

这是设备端关卡(on-device gate)——Agent 无法验证,负责人必须运行 dev build。兜底方案:若锚定在设备上不稳定,将相同的ActionsPopover内容通过FilePickerSheet底表外壳渲染(仅替换容器)——内容与渲染器无关。

7.2SELECT_COLORS必须是完全类型化的字面量矩阵

BUTTON_COLORSbutton.styles.ts:30-189)一样,不允许计算键,且只用 NativeWind 语义 token 类(bg-*text-*)。移动端无 hover,因此折叠 Web 的 hover 单元格;保持iconfg分离(Web 分别设置)。

7.3foldable= 状态驱动,而非 hover 驱动

Web 的Interactive.Foldable是 CSS:hover触发的网格动画。移动端仅在胶囊展开时渲染标签(深度研究场景为state==="selected"),不要尝试 hover 展开

7.4 无 hover 的启用/禁用交互

Web 在行 hover 时显示SvgSlash。移动端需要决定一个固定交互(工具行尾部的常显Switch,或常显SvgSlash图标Button)并一致应用。移植 Web 的守卫:禁用当前被强制工具会清除强制ActionLineItem.tsx:98-108)。推荐尾部Switch,与源行保持一致。

7.5 搜索工具 ↔ 知识源耦合:必须精确移植守卫,否则渲染死循环

移植自ActionsPopover/index.tsx:reconcile effect 由if (searchToolId===null || !sourcesInitialized) return;:742-759)门控,且仅在不一致时切换。强制工具是单元素集合(:299-307)。启用某源会自动固定搜索;禁用最后一个源会解除固定(:365-396);previouslyEnabledSourcesRef(普通 ref)在搜索重新启用时恢复源(:790-810)。必须镜像sourcesInitialized等价守卫并保持"仅在不同时切换"的幂等性(setSearchToolEnabled:762-770)。先交付源→过滤选择,再把耦合作为独立的、jest 测试的步骤加入。

7.6useDeepResearchToggle的 null→新会话保留语义

精确复刻 ref 守卫:仅在previousId !== null && previousId !== chatSessionId时重置为 false;agentId变化时总是重置。naive 的[chatSessionId]重置会在发送中途的 null→新会话转换时丢失标志位。(web/src/hooks/useDeepResearchToggle.ts:31-44。)

7.7allowed_tool_ids:发送计算后的启用列表,绝不发送裸[]

computeAllowedToolIds在无禁用时返回null(后端视null= 允许全部);发送[]禁用一切。必须匹配 Web 的enabledToolIds语义。

7.8 默认 Agent(id 0)的知识源

knowledge_sources对 id 0 为空;Web 使用连接器列表将其视为"全部可访问"。移动端useConnectorSourcesGET /manage/connector-status,任何可聊天访问的用户)提供该列表;非默认 agent 使用agent.knowledge_sources,为空但有搜索工具时回退为全部(ActionsPopover/index.tsx:199-210)。Federated connectorsGET /federated,EE)推迟——这是 Tier-2 针对 federated-only 源的有文档记录的缺口。

7.9disabled_tool_ids与 Web 共享

移动端禁用的工具 PATCH 到与 Web 读取的同一个每用户每 agent 记录——有意的跨客户端同步。PATCH 发送完整数组(后端字段为必填)。乐观更新后 invalidate;守卫快速连续切换的竞态。

7.10 PII / 缓存

connectorSourcesagentPreferences非聊天内容,可安全持久化到 MMKV Query 缓存。不要持久化selectedSources/forcedToolId/deepResearch(临时状态,存在于 provider 状态,绝不进入 Query)。

7.11internal_search_filters最小化形状

仅发送{ source_type: [...] }BaseFilters其余字段后端默认 null(backend/onyx/context/search/models.py)。值为 snake_caseDocumentSource字符串。

7.12FILE_READER_TOOL_ID与 MCP 工具始终排除

displayableTools从 Actions 列表排除它们;MCP/OAuth 行、action 搜索框、管理端 "More Actions" 链接均不在范围内。

7.13 测试策略

tools.ts/sources.ts是纯函数 → jest 单元测试(computeAllowedToolIds的 null vs 列表、hasSearchToolsAvailabledisplayableTools过滤、buildInternalSearchFilters)。useDeepResearchTogglereset 矩阵与源耦合 reducer 是其他高价值单测目标。测试中直接导入叶子组件(reanimated barrels 会令 jest 崩溃——参见mobile/CLAUDE.md)。

八、与系列文档的关系

本详细设计承接 01-research.md(需求、Tier-2 范围决策、两端代码扫描、popover 行业分析、三种方案与选型)与 02-high-level-design.md(端到端流程、组件交互图、端到端场景、关键决策),并向下输出给 04-implementation-plan.md(CLAUDE.md 格式计划 +plan-challenge六项检查全过 + 8 个新图标精确 SVG path)与 05-pr-roadmap.md(4 个评审级 PR:基础 → 深度研究骨架 → ActionsPopover 工具 → 源 + 耦合,含范围、文件、测试与漂移检查点)。

九、总结

03-detailed-design.md展示了 Onyx 移动端一次典型的"零后端改动、纯前端移植"功能设计:通过复用已存在的/persona数据、四个已接受的后端请求字段,以及每 agentdisabled_tool_ids的既有表与接口,将 Web 端的深度研究开关与 Actions 工具/源选择控件完整复刻到 React Native 端。设计的精髓在于"Web-Parity-First"的严格对齐:三个新 UI 原语(PopoverSelectButtonSwitch)在观感与交互上精确镜像 Web/Opal 实现,同时诚实记录移动端平台驱动的差异(向上打开、无 hover、状态驱动折叠);而实现注意事项部分则把移植中最容易踩坑的边界条件(锚定弹层的键盘处理、搜索-源耦合的守卫、allowed_tool_ids的 null 语义、deep-research 的 reset 矩阵)全部显式化,为后续实现与评审提供了可验证的执行清单。

【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:25:39

用 Automator 在访达中一键新建文件:右键快速操作全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:24:14

Python CSV读写完全指南:标准库、pandas与编码避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:23:56

基于LabVIEW的分布式四驱扭矩主动分配测控系统实战解析

简介&#xff1a;基于LabVIEW的汽车扭矩主动分配测控系统是一份源自全国虚拟仪器大赛的完整工程方案&#xff0c;面向测控、汽车电子方向的工程师与学习者&#xff0c;解决驱动轮扭矩实时分配与车辆稳定性控制问题。压缩包内共5个文件&#xff0c;包含Simulink模型&#xff08;…

作者头像 李华
网站建设 2026/9/10 2:22:21

EasyExcel迁移Apache Fesod实战:从POI冲突到复杂表头与嵌套List处理

月初和同事聊起要不要把项目里的Excel导入导出模块重写&#xff0c;起因是看到群里有人贴了一段NoSuchFieldError: factory的堆栈&#xff0c;下面一群人回复“EasyExcel老毛病又犯了”。当时我心里咯噔一下&#xff0c;知道这个“老朋友”怕是真到了该告别的时候。后来把核心导…

作者头像 李华