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)"为原则,用Popover、SelectButton、Switch三个新原语复刻 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 数据已经携带tools与knowledge_sources字段,移动端无需新增目录获取请求;- 后端
SendMessageRequest已经接受deep_research、allowed_tool_ids、forced_tool_id、internal_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.py | get_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_ACCESS:GET /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.ts的resolveButtonState,但去掉 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 }) | 单元素强制语义;forcedToolId,toggleForcedTool(id),clear();切换 agent 时重置 | 临时 |
useAgentPreferences() | disabledToolIdsFor(agentId)/setDisabledToolIds(agentId, ids)(乐观更新 + PATCH + invalidate);TanStack Query 以serverUrl为 key 请求GET /user/assistant/preferences | 服务端持久化 |
useConnectorSources() | GET /manage/connector-status→BasicCCPairInfo[].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.ts | SELECT_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.tsx | DocumentSource→ logo/字形(使用SOURCE_META) |
mobile/src/components/chat/ToolbarControls.tsx | 在InputBar渲染深度研究胶囊 + 强制工具胶囊 + Actions 触发按钮 |
mobile/src/chat/tools.ts | ToolSnapshot类型、工具 id 常量、判定函数、getIconForToolId |
mobile/src/chat/sources.ts | DocumentSource、SOURCE_META、buildInternalSearchFilters |
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.ts | GET/manage/connector-status→ 源列表 |
mobile/src/hooks/useSourceSelection.ts | 每 agent 临时源选择 + 搜索耦合 |
mobile/src/api/chat/agentPreferences.ts | getAgentPreferences()/patchAgentPreferences(agentId, ids) |
mobile/src/api/chat/connectors.ts | getConnectorSources()(connector-status 获取) |
mobile/src/state/ComposerToolsProvider.tsx | Context 中枢,聚合四个字段;resolveToolOptions() |
mobile/src/icons/{hourglass,globe,cpu,link,server,plug,unplug,slash}.tsx | 8 个新 SVG 图标(精确的 Web path 数据见 04-implementation-plan) |
被修改的文件:
| 文件 | 变更 |
|---|---|
mobile/src/chat/agents.ts | 扩展MinimalAgent,增加tools: ToolSnapshot[]、knowledge_sources: DocumentSource[] |
mobile/src/api/settings.ts | 给WorkspaceSettings增加deep_research_enabled?: boolean |
mobile/src/api/chat/stream.ts | 给SendMessageBody增加allowed_tool_ids?、forced_tool_id?、internal_search_filters?;新增InternalSearchFilters类型 |
mobile/src/hooks/useChatController.ts | submit(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()贯穿到sendWithAttachments→submit |
mobile/src/api/query-keys.ts | 新增agentPreferences、connectorSourceskey(以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 + GAPleft = 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-03,selected状态 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)在2px↔17px间平移。轨道背景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
单行LineItemButton:selected={forcedToolId===tool.id},onPress=强制切换(搜索工具尚未被强制时 → 打开源子视图,镜像 WebActionLineItem.tsx:98-108)。rightChildren:尾部启用/禁用控件(常显Switch或带SvgSlash的图标Button——移动端无 hover,无法"悬停显示")+ 部分搜索源时的EnabledCount文本 + 搜索下钻的SvgChevronRight。禁用工具渲染为暗淡样式(移动端LineItemButton无删除线——用color="muted"+ 删除线样式,记录为平台差异)。
SourceSwitchList.tsx
返回箭头头部(SvgChevronLeftButton)+ 全部启用/全部禁用LineItemButton(SvgPlug/SvgUnplug)+ 每源行(leading=<SourceIcon>、标签、rightChildren=<Switch>)。镜像SwitchList.tsx:61-119。(搜索框省略——Tier-2 范围。)
ToolbarControls.tsx
按顺序渲染:Actions 触发按钮(SvgSlidersButton,ref作为 popover 锚点)——当displayableTools(agent.tools).length>0时;深度研究SelectButton(SvgHourglass,state=on?"selected":"empty",foldable={!on})——当deep_research_enabled && hasSearchToolsAvailable(agent.tools)时;强制工具胶囊SelectButton(state="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 16,link除外 =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-298—submit增加第 4 个参数toolOptions?: ChatToolOptions;body 字面量设置deep_research: toolOptions?.deepResearch ?? false、allowed_tool_ids、forced_tool_id、internal_search_filters。runChatStream原样转发body。stream.ts—SendMessageBody扩展;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_COLORS(button.styles.ts:30-189)一样,不允许计算键,且只用 NativeWind 语义 token 类(bg-*、text-*)。移动端无 hover,因此折叠 Web 的 hover 单元格;保持icon与fg分离(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 使用连接器列表将其视为"全部可访问"。移动端useConnectorSources(GET /manage/connector-status,任何可聊天访问的用户)提供该列表;非默认 agent 使用agent.knowledge_sources,为空但有搜索工具时回退为全部(ActionsPopover/index.tsx:199-210)。Federated connectors(GET /federated,EE)推迟——这是 Tier-2 针对 federated-only 源的有文档记录的缺口。
7.9disabled_tool_ids与 Web 共享
移动端禁用的工具 PATCH 到与 Web 读取的同一个每用户每 agent 记录——有意的跨客户端同步。PATCH 发送完整数组(后端字段为必填)。乐观更新后 invalidate;守卫快速连续切换的竞态。
7.10 PII / 缓存
connectorSources与agentPreferences非聊天内容,可安全持久化到 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 列表、hasSearchToolsAvailable、displayableTools过滤、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 原语(Popover、SelectButton、Switch)在观感与交互上精确镜像 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),仅供参考