v2-refactor-temp/docs/webSearch/service-architecture.md
本文档定义下一版 Main-side WebSearch service 的目标架构。
这次重构的核心不是把现有 searchUrls / searchKeywords 简单改名,而是修正领域模型:
当前实现用 provider id 把搜索能力分成 KeywordSearchProviderId 和 UrlSearchProviderId,这会把“provider 是谁”和“它能做什么”混在一起。Jina 是最典型的问题:同一个 Jina 服务同时提供 URL 内容抓取和关键词搜索,但旧模型只能把它放进其中一类。
本次目标是把 Main-side WebSearch 调整成 Provider + Capability 架构。
Provider 表示一个外部或本地 WebSearch 服务的配置主体。
Provider 负责承载:
api / mcpProvider 不等同于一个执行动作。一个 provider 可以支持多个 capability。
Capability 表示 provider 能执行的动作。
当前只定义两个 capability:
searchKeywordsfetchUrlsCapability 决定输入类型、provider driver 方法、endpoint 选择和测试方式。
searchKeywordssearchKeywords 表示使用关键词或自然语言 query 执行 Web 搜索。
输入是一个或多个 keyword query。
输出是统一的 WebSearchResponse:
queryresults[]title / content / url示例 provider:
fetchUrlsfetchUrls 表示抓取输入 URL 的正文内容。
输入是一个或多个 URL。
它不是搜索,不应做相关搜索、摘要扩展或 SERP 查询。它只把指定 URL 转成可供模型消费的内容。
示例 provider:
旧名称 searchUrls 不再作为目标架构概念使用,因为它把 URL 内容抓取误称为搜索。
Request 表示一次 Main-side WebSearch 执行。
一次 request 必须只走一个工具入口。需要同时搜索关键词和抓取 URL 时,上游调用方应分别调用 searchKeywords 和 fetchUrls。
目标模型:
Provider
-> capabilities[]
-> searchKeywords
-> fetchUrls
不再用两组 provider id 数组表达能力:
KEYWORD_SEARCH_PROVIDER_IDS 决定谁能搜索关键词。URL_SEARCH_PROVIDER_IDS 决定谁能抓取 URL。这些数组的问题是:一个 provider 只能被归到某个输入类别下,无法自然表达 Jina 这种多能力 provider。
Main-side service 的目标公共入口是两个意图明确的方法:
searchKeywords({
providerId?,
keywords
})
fetchUrls({
providerId?,
urls
})
原因:
searchKeywords 和 fetchUrls 对 AI SDK tools 是两个不同工具。capability 字段。requestId;工具调用身份、lifecycle、abort 和 UI 状态由 tool block / tool runtime 承担。调用方职责:
searchKeywords({ keywords: ['北京天气'] })。fetchUrls({ urls: ['https://xxx.com'] })。providerId 是可选覆盖;不传时由 service 按 capability 读取默认 provider preference。Provider driver 不再统一暴露一个含糊的 search(query) 方法。
目标方法:
searchKeywords(input, config, httpOptions?)fetchUrls(input, config, httpOptions?)Driver 只实现自己支持的 capability。service 在调用前必须根据 provider capability registry 校验支持关系。
Jina 在目标架构里是一个 provider,而不是两个 provider。
目标 provider id:
jina
Jina 支持两个 capability:
searchKeywordsfetchUrlsJina 的两个能力共享同一组 API key,但使用不同 endpoint。
Jina 官方 Reader 文档也把这两个能力分开描述:
https://r.jina.ai 用于读取 URL 并获取内容。https://s.jina.ai 用于搜索网络并获取 SERP。参考:
本次重构不兼容 v2 开发过程中的中间数据。
因此可以做破坏性调整:
jina-reader 可以改为 jina。searchUrls request type 可以移除或替换。这不改变 v1 到 v2 的正式迁移职责;已发布 v1 数据仍应通过 src/main/data/migration/v2/ 下的 migrator 进入目标结构。
如果后续需要保护 v2 开发分支上的中间配置,应作为新的兼容性任务单独设计,而不是污染这次 Main service 架构。
WebSearch preset 应参考 File Processing preset 的 layered preset pattern:
src/shared/data/presets/。File Processing 已经采用这个形状:
capabilities: [
{
feature: 'document_to_markdown',
inputs: ['document'],
output: 'markdown',
apiHost: 'https://mineru.net',
modelId: 'pipeline'
}
]
WebSearch 应复用这个设计语言,而不是重新发明一套 defaultApiHost / capabilityApiHosts 命名。
但 WebSearch 不需要照搬 File Processing 的 inputs / output 字段。File Processing 需要它们,是因为同一个 processor feature 要声明支持的文件输入类别和产物类型;WebSearch 的 capability 名已经决定输入语义,且输出统一是 WebSearchResponse。
Shared contract 应定义稳定 capability:
type WebSearchCapability = 'searchKeywords' | 'fetchUrls'
目标 request contract:
type WebSearchSearchKeywordsRequest = {
providerId?: WebSearchProviderId
keywords: string[]
}
type WebSearchFetchUrlsRequest = {
providerId?: WebSearchProviderId
urls: string[]
}
输入约束:
keywords 必须至少包含一个非空 keyword query。urls 必须至少包含一个合法 URL。searchKeywords request 不接受 URL 抓取语义。fetchUrls request 不接受 keyword 搜索语义。providerId 是调用方可选覆盖,不应要求 AI 模型每次指定 provider。WebSearch 默认 provider 参考 File Processing 的 feature default 模式:每个 capability 一个 default preference。
目标 preference keys:
'chat.web_search.default_search_keywords_provider': WebSearchProviderId | null
'chat.web_search.default_fetch_urls_provider': WebSearchProviderId | null
规则:
searchKeywords 未传 providerId 时读取 chat.web_search.default_search_keywords_provider。fetchUrls 未传 providerId 时读取 chat.web_search.default_fetch_urls_provider。null 时,service 抛配置错误,不自动选择首个可用 provider。providerId 时,使用该 provider 作为覆盖。chat.web_search.default_provider 不进入目标架构;本次不做旧 preference 兼容迁移。继续使用统一 response:
type WebSearchResult = {
title: string
content: string
url: string
sourceInput: string
}
type WebSearchResponse = {
query?: string
providerId: WebSearchProviderId
capability: WebSearchCapability
inputs: string[]
results: WebSearchResult[]
}
query 的含义按 capability 解释:
searchKeywords:调用方传入的 keyword query 合并展示。fetchUrls:调用方传入的 URL 合并展示。results 始终是模型可消费内容,不暴露 provider 原始返回结构。
Trace metadata:
providerId 是本次 tool call 实际使用的 provider,包括 default provider 解析后的结果。capability 是本次 tool call 的能力:searchKeywords 或 fetchUrls。inputs 是本次 tool call 的规范化输入数组。sourceInput 记录单条 result 来自哪个 keyword 或 URL,用于后续按 tool call / query 分组和去重。warnings、failedInputs。requestId 不属于新的 WebSearch 领域 contract:
因此实现迁移时应删除 WebSearch request / response / status 中的 requestId,而不是保留空字段或透传字段。
query 必须保留 request input 的语义,而不是 provider 处理后的 query:
autopromptString、Bocha originalQuery、Tavily 返回的 query 等 provider response 字段覆盖它。searchWithTime 这类注入后的 query。fetchUrls,query 保留输入 URL;最终跳转 URL 或 provider 返回 URL 应放在 result 的 url 字段。如果后续确实需要展示 provider 改写后的 query,应新增 provider metadata 字段单独承载,不能改变 query 的含义。
Provider definition 应表达 capability 与 endpoint 的关系。
目标结构参考 File Processing preset:
type WebSearchProviderFeatureCapability =
| {
feature: 'searchKeywords'
apiHost?: string
}
| {
feature: 'fetchUrls'
apiHost?: string
}
type WebSearchProviderPresetConfig = {
name: string
type: WebSearchProviderType
capabilities: readonly WebSearchProviderFeatureCapability[]
}
type WebSearchProviderPreset = {
id: WebSearchProviderId
} & WebSearchProviderPresetConfig
命名规则:
feature 作为 discriminant,保持与 File Processing 的 capability schema 一致。apiHost 是 capability 默认 endpoint,不再使用 provider 级 defaultApiHost。inputs / output 字段;searchKeywords 固定接收 keyword query,fetchUrls 固定接收 URL,输出统一为 WebSearchResponse。对于只有一个 endpoint 的 provider,也把 endpoint 放在唯一 capability 上。
对于 Jina 这种多 endpoint provider,必须按 capability 配置 endpoint:
jina.capabilities[feature=searchKeywords].apiHost -> https://s.jina.ai
jina.capabilities[feature=fetchUrls].apiHost -> https://r.jina.ai
Preset map 的目标形状:
export const WEB_SEARCH_PROVIDER_PRESET_MAP = {
jina: {
name: 'Jina',
type: 'api',
capabilities: [
{
feature: 'searchKeywords',
apiHost: 'https://s.jina.ai'
},
{
feature: 'fetchUrls',
apiHost: 'https://r.jina.ai'
}
]
}
} as const satisfies Record<WebSearchProviderId, WebSearchProviderPresetConfig>
Provider override 需要支持 capability-specific endpoint。
目标语义:
示例语义:
type WebSearchProviderCapabilityOverride = {
apiHost?: string
}
type WebSearchProviderOverride = {
apiKeys?: string[]
capabilities?: Partial<Record<WebSearchCapability, WebSearchProviderCapabilityOverride>>
engines?: string[]
basicAuthUsername?: string
basicAuthPassword?: string
}
apiHost 单字段不再足以表达目标架构。实现时应一次性替换为 capability-aware shape。
Merged provider config 应和 File Processing 一样保留 capability array,而不是把 capability override 暴露为 Record:
type ResolvedWebSearchProvider = WebSearchProviderPreset & {
apiKeys?: string[]
capabilities: WebSearchProviderFeatureCapability[]
engines?: string[]
basicAuthUsername?: string
basicAuthPassword?: string
}
merge 规则:
feature merge 回 preset 的 capabilities[]。目标公共执行流:
Caller
-> WebSearchService.searchKeywords(request)
or WebSearchService.fetchUrls(request)
-> resolve providerId from request override or capability default preference
-> resolve provider config
-> validate provider supports the method capability
-> create provider driver
-> fanout request keywords/urls with matching driver method
-> Promise.allSettled()
-> reject immediately on AbortError
-> log partial failures
-> require at least one successful keyword or URL
-> merge successful responses and keep request keywords/urls as response.query
-> apply blacklist
-> post process
-> WebSearchResponse
keywords / urls 使用 fanout 执行。
每个 keyword 或 URL 独立调用 provider capability 方法:
searchKeywords 调用 driver 的 searchKeywords(input, ...)。fetchUrls 调用 driver 的 fetchUrls(input, ...)。多个 keyword 或 URL 的结果按完成后的 successful results 合并。
实现可以有一个私有 helper 承载共同流程,例如:
private runCapability({
providerId,
feature,
inputs
})
这个 helper 是实现细节,不作为 AI SDK tools 或对外 service contract。
继续保留当前 Main-side 语义:
warnings / failedInputs;当前 response contract 只返回成功结果。WebSearch tool 化后仍然需要展示搜索结果。
目标 UI 数据来源:
searchKeywords / fetchUrls tool call 都产生一个 WebSearchResponse。searchKeywords 和 fetchUrls 都输出 { title, content, url }[],UI 不需要知道 provider 原始协议。推荐聚合规则:
searchKeywords 与 fetchUrls 可以在同一个面板展示,但应保留来源标签或分组标题。这类结果展示不依赖 chat.web_search.active_searches。active_searches 只适合表达运行中的短暂进度,不适合保存或聚合搜索结果。
chat.web_search.active_searches新 Main-side WebSearch 架构不再需要 chat.web_search.active_searches。
当前代码里的 chat.web_search.active_searches 是旧 UI 进度通道:
CitationBlock 读取它来显示 processing spinner 文案。fetch_complete / partial_failure / cutoff。Tool 化后,这个 shared cache 不再是结果展示边界:
fetch_complete 可以由 tool result 的 sources count 和 tool block 完成状态表达。partial_failure 是 service 内部 fanout 的降级语义,不一定需要 UI 单独展示。cutoff 是后处理细节,不应要求一个跨窗口 cache key 才能工作。目标架构中:
WebSearchResponse.results。chat.web_search.active_searches。WebSearchStatus / WebSearchPhase。旧 Renderer WebSearch service 已从新的 AI 执行链路移除。若后续清理旧 Redux slice 时仍看到 chat.web_search.active_searches,应按旧链路残留删除,而不是重新接回 Main-side WebSearch 架构。
处理顺序固定为:
merge successful responses
-> blacklist filter
-> post processing
理由:
目标 capability matrix:
| Provider | ID | searchKeywords | fetchUrls | Notes |
|---|---|---|---|---|
| Zhipu | zhipu | Yes | No | API keyword search |
| Tavily | tavily | Yes | No | API keyword search |
| Searxng | searxng | Yes | No | 搜索后内部抓取搜索结果 URL 正文,但对外仍是 searchKeywords |
| Exa | exa | Yes | No | API keyword search |
| Exa MCP | exa-mcp | Yes | No | MCP-style keyword search |
| Bocha | bocha | Yes | No | API keyword search |
| Querit | querit | Yes | No | API keyword search |
| Fetch | fetch | No | Yes | 本地 URL 内容抓取 |
| Jina | jina | Yes | Yes | s.jina.ai 搜索,r.jina.ai 抓取 URL |
注意:Searxng 内部会抓取搜索结果页面内容,但它的外部 capability 仍是 searchKeywords。Capability 描述的是调用方输入语义,不是 provider 内部实现步骤。
Settings 仍按 provider 展示配置,但需要展示 provider 支持的 capability。
目标展示语义:
searchKeywords 和 fetchUrls 两个 endpoint。Provider check 应按 capability 执行。
检查输入:
searchKeywords 使用稳定 test query,例如 test query。fetchUrls 使用稳定 URL,例如 https://example.com。检查结果:
当前 WebSearch 执行链路已经切到 Main-side service:
WebSearchService.searchKeywords() / WebSearchService.fetchUrls()。WebSearchService 和 Renderer provider drivers 已删除。assistant.enableWebSearch。builtin_web_search 和 builtin_fetch_urls 两个 external tools。当前仍不做:
rag post processing 实现。searchWithTime 恢复。chat.web_search.active_searches。searchWithTime 仍视为旧 Renderer WebSearch 栈里的遗留行为,不进入新的 Main-side runtime contract。
实现本架构时必须更新或新增以下测试。
覆盖:
searchKeywords 和 fetchUrls。覆盖:
fetchUrls 使用 https://r.jina.ai endpoint。searchKeywords 使用 https://s.jina.ai endpoint。fetchUrls。fetchUrls。fetchUrls 在发请求前失败。覆盖:
searchKeywords() fanout 与结果合并。fetchUrls() fanout 与结果合并。providerId 时读取对应 capability default provider。null 时抛配置错误。providerId / capability / inputs 和 result 级 sourceInput。WebSearchResponse.results 可被 assistant turn 级搜索结果面板聚合。覆盖:
完成本架构后,Main-side WebSearch 应具备以下形态:
Shared Provider Preset
-> provider capabilities
-> capability endpoint defaults
Preference Override
-> provider credentials
-> capability endpoint overrides
Main WebSearchService
-> searchKeywords(providerId?, keywords)
-> fetchUrls(providerId?, urls)
-> provider capability validation
-> provider driver capability method
-> result normalization
-> blacklist
-> post processing
这时 WebSearch 的核心边界会变成:
这个模型可以自然支持 Jina 这类多能力 provider,也避免继续把 URL 内容抓取伪装成搜索。