refactor: Convert WebSearchSource enum to const object with Zod schema (#12866)

### What this PR does

Before this PR:
`WebSearchSource` was defined as a TypeScript `enum`.

After this PR:
`WebSearchSource` is converted to an `as const` object
(`WEB_SEARCH_SOURCE`) with a Zod schema (`WebSearchSourceSchema`) and a
derived type (`WebSearchSource`).

Part of #12846 — split out as a separate PR to reduce the number of
files to review in the final refactor PR.

### Why we need it and why it was done in this way

This aligns `WebSearchSource` with the project's existing convention of
using `as const` objects instead of enums (e.g., `EFFORT_RATIO`,
`FILE_TYPE`). The Zod schema integration also enables runtime
validation.

The following tradeoffs were made:
- The value object is renamed to `WEB_SEARCH_SOURCE` (UPPER_SNAKE_CASE)
to follow the const object convention, while the type name
`WebSearchSource` is preserved for backward compatibility.

The following alternatives were considered:
- Keeping the enum — rejected because it diverges from the project's
established pattern.

### Breaking changes

None. All runtime string values remain identical. This is a purely
internal refactor.

### Special notes for your reviewer

- This is a mechanical rename across 16 files with no behavioral
changes.
- `pnpm build:check` passes (lint + 3026 tests + typecheck).
- One pre-existing commented-out line in `AiSdkToChunkAdapter.ts:35`
still references the old `WebSearchSource` name — can be cleaned up
separately.

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: Not required (internal refactor only)

### Release note

```release-note
NONE
```
This commit is contained in:
Phantom 2026-02-13 00:25:56 +08:00 committed by GitHub
parent e0b733907e
commit 40317cd3c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 107 additions and 103 deletions

View File

@ -4,8 +4,8 @@
*/
import { loggerService } from '@logger'
import type { AISDKWebSearchResult, MCPTool, WebSearchResults } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import type { AISDKWebSearchResult, MCPTool, WebSearchResults, WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
import { ProviderSpecificError } from '@renderer/types/provider-specific-error'
@ -287,24 +287,24 @@ export class AiSdkToChunkAdapter {
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: providerMetadata.google?.groundingMetadata as WebSearchResults,
source: WebSearchSource.GEMINI
source: WEB_SEARCH_SOURCE.GEMINI
}
})
} else if (final.webSearchResults.length) {
const providerName = Object.keys(providerMetadata || {})[0]
const sourceMap: Record<string, WebSearchSource> = {
[WebSearchSource.OPENAI]: WebSearchSource.OPENAI_RESPONSE,
[WebSearchSource.ANTHROPIC]: WebSearchSource.ANTHROPIC,
[WebSearchSource.OPENROUTER]: WebSearchSource.OPENROUTER,
[WebSearchSource.GEMINI]: WebSearchSource.GEMINI,
[WEB_SEARCH_SOURCE.OPENAI]: WEB_SEARCH_SOURCE.OPENAI_RESPONSE,
[WEB_SEARCH_SOURCE.ANTHROPIC]: WEB_SEARCH_SOURCE.ANTHROPIC,
[WEB_SEARCH_SOURCE.OPENROUTER]: WEB_SEARCH_SOURCE.OPENROUTER,
[WEB_SEARCH_SOURCE.GEMINI]: WEB_SEARCH_SOURCE.GEMINI,
// [WebSearchSource.PERPLEXITY]: WebSearchSource.PERPLEXITY,
[WebSearchSource.QWEN]: WebSearchSource.QWEN,
[WebSearchSource.HUNYUAN]: WebSearchSource.HUNYUAN,
[WebSearchSource.ZHIPU]: WebSearchSource.ZHIPU,
[WebSearchSource.GROK]: WebSearchSource.GROK,
[WebSearchSource.WEBSEARCH]: WebSearchSource.WEBSEARCH
[WEB_SEARCH_SOURCE.QWEN]: WEB_SEARCH_SOURCE.QWEN,
[WEB_SEARCH_SOURCE.HUNYUAN]: WEB_SEARCH_SOURCE.HUNYUAN,
[WEB_SEARCH_SOURCE.ZHIPU]: WEB_SEARCH_SOURCE.ZHIPU,
[WEB_SEARCH_SOURCE.GROK]: WEB_SEARCH_SOURCE.GROK,
[WEB_SEARCH_SOURCE.WEBSEARCH]: WEB_SEARCH_SOURCE.WEBSEARCH
}
const source = sourceMap[providerName] || WebSearchSource.AISDK
const source = sourceMap[providerName] || WEB_SEARCH_SOURCE.AISDK
this.onChunk({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,

View File

@ -39,7 +39,7 @@ import type {
Provider,
ToolCallResponse
} from '@renderer/types'
import { EFFORT_RATIO, FILE_TYPE, WebSearchSource } from '@renderer/types'
import { EFFORT_RATIO, FILE_TYPE, WEB_SEARCH_SOURCE } from '@renderer/types'
import type {
ErrorChunk,
LLMWebSearchCompleteChunk,
@ -588,7 +588,7 @@ export class AnthropicAPIClient extends BaseApiClient<
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: content.content,
source: WebSearchSource.ANTHROPIC
source: WEB_SEARCH_SOURCE.ANTHROPIC
}
} as LLMWebSearchCompleteChunk)
break
@ -641,7 +641,7 @@ export class AnthropicAPIClient extends BaseApiClient<
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: contentBlock.content as Array<WebSearchResultBlock>,
source: WebSearchSource.ANTHROPIC
source: WEB_SEARCH_SOURCE.ANTHROPIC
}
} as LLMWebSearchCompleteChunk)
}

View File

@ -34,7 +34,7 @@ import type {
Provider,
ToolCallResponse
} from '@renderer/types'
import { EFFORT_RATIO, FILE_TYPE, WebSearchSource } from '@renderer/types'
import { EFFORT_RATIO, FILE_TYPE, WEB_SEARCH_SOURCE } from '@renderer/types'
import type { LLMWebSearchCompleteChunk, TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
import type { Message } from '@renderer/types/newMessage'
@ -656,7 +656,7 @@ export class GeminiAPIClient extends BaseApiClient<
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: candidate.groundingMetadata,
source: WebSearchSource.GEMINI
source: WEB_SEARCH_SOURCE.GEMINI
}
} satisfies LLMWebSearchCompleteChunk)
}

View File

@ -55,7 +55,7 @@ import {
isSystemProvider,
isTranslateAssistant,
SystemProviderIds,
WebSearchSource
WEB_SEARCH_SOURCE
} from '@renderer/types'
import type { TextStartChunk, ThinkingStartChunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
@ -754,7 +754,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
hasBeenCollectedWebSearch = true
return {
results: annotations,
source: WebSearchSource.OPENAI
source: WEB_SEARCH_SOURCE.OPENAI
}
}
@ -765,7 +765,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.citations,
source: WebSearchSource.GROK
source: WEB_SEARCH_SOURCE.GROK
}
}
@ -776,7 +776,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.search_results,
source: WebSearchSource.PERPLEXITY
source: WEB_SEARCH_SOURCE.PERPLEXITY
}
}
@ -787,7 +787,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.citations,
source: WebSearchSource.OPENROUTER
source: WEB_SEARCH_SOURCE.OPENROUTER
}
}
@ -798,7 +798,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return {
// @ts-ignore - web_search may not be in standard type definitions
results: chunk.web_search,
source: WebSearchSource.ZHIPU
source: WEB_SEARCH_SOURCE.ZHIPU
}
}
@ -809,7 +809,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
return {
// @ts-ignore - search_info may not be in standard type definitions
results: chunk.search_info.search_results,
source: WebSearchSource.HUNYUAN
source: WEB_SEARCH_SOURCE.HUNYUAN
}
}

View File

@ -23,7 +23,7 @@ import type {
Provider,
ToolCallResponse
} from '@renderer/types'
import { FILE_TYPE, WebSearchSource } from '@renderer/types'
import { FILE_TYPE, WEB_SEARCH_SOURCE } from '@renderer/types'
import { ChunkType } from '@renderer/types/chunk'
import type { Message } from '@renderer/types/newMessage'
import type {
@ -576,7 +576,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
controller.enqueue({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
source: WebSearchSource.OPENAI_RESPONSE,
source: WEB_SEARCH_SOURCE.OPENAI_RESPONSE,
results: output.content[0].annotations
}
})
@ -712,7 +712,7 @@ export class OpenAIResponseAPIClient extends OpenAIBaseClient<
controller.enqueue({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
source: WebSearchSource.OPENAI_RESPONSE,
source: WEB_SEARCH_SOURCE.OPENAI_RESPONSE,
results: chunk.part.annotations
}
})

View File

@ -17,7 +17,7 @@
import { loggerService } from '@logger'
import { LanguagesEnum } from '@renderer/config/translate'
import type { LegacyMessage as OldMessage, Topic, TranslateLanguageCode } from '@renderer/types'
import { FILE_TYPE, WebSearchSource } from '@renderer/types' // Import FileTypes enum
import { FILE_TYPE, WEB_SEARCH_SOURCE } from '@renderer/types' // Import FileTypes enum
import type {
BaseMessageBlock,
CitationMessageBlock,
@ -224,14 +224,14 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.groundingMetadata,
source: WebSearchSource.GEMINI
source: WEB_SEARCH_SOURCE.GEMINI
}
}
if (oldMessage.metadata?.annotations?.length) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.annotations,
source: WebSearchSource.OPENAI_RESPONSE
source: WEB_SEARCH_SOURCE.OPENAI_RESPONSE
}
}
if (oldMessage.metadata?.citations?.length) {
@ -239,14 +239,14 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
citationDataToCreate.response = {
results: oldMessage.metadata.citations,
// 无法区分统一为Openrouter
source: WebSearchSource.OPENROUTER
source: WEB_SEARCH_SOURCE.OPENROUTER
}
}
if (oldMessage.metadata?.webSearch) {
hasCitationData = true
citationDataToCreate.response = {
results: oldMessage.metadata.webSearch,
source: WebSearchSource.WEBSEARCH
source: WEB_SEARCH_SOURCE.WEBSEARCH
}
}
if (oldMessage.metadata?.webSearchInfo) {
@ -254,7 +254,7 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
citationDataToCreate.response = {
results: oldMessage.metadata.webSearchInfo,
// 无法区分统一为zhipu
source: WebSearchSource.ZHIPU
source: WEB_SEARCH_SOURCE.ZHIPU
}
}
if (oldMessage.metadata?.knowledge?.length) {

View File

@ -2,7 +2,7 @@ import type { GroundingMetadata } from '@google/genai'
import Spinner from '@renderer/components/Spinner'
import type { RootState } from '@renderer/store'
import { selectFormattedCitationsByBlockId } from '@renderer/store/messageBlock'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import { type CitationMessageBlock, MessageBlockStatus } from '@renderer/types/newMessage'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -18,7 +18,7 @@ function CitationBlock({ block }: { block: CitationMessageBlock }) {
const message = useSelector((state: RootState) => state.messages.entities[block.messageId])
const userMessageId = message?.askId || block.messageId // 如果没有 askId 则回退到 messageId
const hasGeminiBlock = block.response?.source === WebSearchSource.GEMINI
const hasGeminiBlock = block.response?.source === WEB_SEARCH_SOURCE.GEMINI
const hasCitations = useMemo(() => {
return (
(formattedCitations && formattedCitations.length > 0) ||

View File

@ -1,6 +1,6 @@
import { configureStore } from '@reduxjs/toolkit'
import type { Model } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { MainTextMessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { render, screen } from '@testing-library/react'
@ -307,7 +307,7 @@ describe('MainTextBlock', () => {
it('should integrate with citation processing when all conditions are met', () => {
const block = createMainTextBlock({
content: 'Content with citation [1]',
citationReferences: [{ citationBlockSource: WebSearchSource.OPENAI }]
citationReferences: [{ citationBlockSource: WEB_SEARCH_SOURCE.OPENAI }]
})
const mockCitations = [
@ -342,7 +342,7 @@ describe('MainTextBlock', () => {
expect(mockWithCitationTags).toHaveBeenCalledWith(
'Content with citation [1]',
mockCitations,
WebSearchSource.OPENAI
WEB_SEARCH_SOURCE.OPENAI
)
// Verify the processed content is rendered

View File

@ -20,7 +20,7 @@ import type { GenericChunk } from '@renderer/aiCore/legacy/middleware/schemas'
import { isVisionModel } from '@renderer/config/models'
import type { LlmState } from '@renderer/store/llm'
import type { Assistant, MCPCallToolResponse, MCPToolResponse, Model, Provider } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type {
Chunk,
LLMResponseCompleteChunk,
@ -1047,7 +1047,7 @@ const mockOpenaiApiClient = {
hasBeenCollectedWebSearch = true
return {
results: annotations,
source: WebSearchSource.OPENAI
source: WEB_SEARCH_SOURCE.OPENAI
}
}
@ -1058,7 +1058,7 @@ const mockOpenaiApiClient = {
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.citations,
source: WebSearchSource.GROK
source: WEB_SEARCH_SOURCE.GROK
}
}
@ -1069,7 +1069,7 @@ const mockOpenaiApiClient = {
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.search_results,
source: WebSearchSource.PERPLEXITY
source: WEB_SEARCH_SOURCE.PERPLEXITY
}
}
@ -1080,7 +1080,7 @@ const mockOpenaiApiClient = {
return {
// @ts-ignore - citations may not be in standard type definitions
results: chunk.citations,
source: WebSearchSource.OPENROUTER
source: WEB_SEARCH_SOURCE.OPENROUTER
}
}
@ -1091,7 +1091,7 @@ const mockOpenaiApiClient = {
return {
// @ts-ignore - web_search may not be in standard type definitions
results: chunk.web_search,
source: WebSearchSource.ZHIPU
source: WEB_SEARCH_SOURCE.ZHIPU
}
}
@ -1102,7 +1102,7 @@ const mockOpenaiApiClient = {
return {
// @ts-ignore - search_info may not be in standard type definitions
results: chunk.search_info.search_results,
source: WebSearchSource.HUNYUAN
source: WEB_SEARCH_SOURCE.HUNYUAN
}
}
return null
@ -1415,7 +1415,7 @@ const mockGeminiApiClient = {
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: candidate.groundingMetadata,
source: WebSearchSource.GEMINI
source: WEB_SEARCH_SOURCE.GEMINI
}
} as LLMWebSearchCompleteChunk)
}
@ -1529,7 +1529,7 @@ const mockAnthropicApiClient = {
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: content.content,
source: WebSearchSource.ANTHROPIC
source: WEB_SEARCH_SOURCE.ANTHROPIC
}
} as LLMWebSearchCompleteChunk)
break
@ -1582,7 +1582,7 @@ const mockAnthropicApiClient = {
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
results: contentBlock.content as Array<WebSearchResultBlock>,
source: WebSearchSource.ANTHROPIC
source: WEB_SEARCH_SOURCE.ANTHROPIC
}
})
}

View File

@ -1,5 +1,5 @@
import { loggerService } from '@logger'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { CitationMessageBlock, MessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { createMainTextBlock } from '@renderer/utils/messageUtils/create'
@ -54,7 +54,7 @@ export const createTextCallbacks = (deps: TextCallbacksDependencies) => {
const citationBlockId = getCitationBlockId() || getCitationBlockIdFromTool()
const citationBlockSource = citationBlockId
? (getState().messageBlocks.entities[citationBlockId] as CitationMessageBlock).response?.source
: WebSearchSource.WEBSEARCH
: WEB_SEARCH_SOURCE.WEBSEARCH
if (text) {
const blockChanges: Partial<MessageBlock> = {
content: text,

View File

@ -3,7 +3,7 @@ import type { AppDispatch } from '@renderer/store'
import store from '@renderer/store'
import { toolPermissionsActions } from '@renderer/store/toolPermissions'
import type { MCPToolResponse, NormalToolResponse } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { ToolMessageBlock } from '@renderer/types/newMessage'
import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { createCitationBlock, createToolBlock } from '@renderer/utils/messageUtils/create'
@ -159,7 +159,7 @@ export const createToolCallbacks = (deps: ToolCallbacksDependencies) => {
const citationBlock = createCitationBlock(
assistantMsgId,
{
response: { results: toolResponse.response, source: WebSearchSource.WEBSEARCH }
response: { results: toolResponse.response, source: WEB_SEARCH_SOURCE.WEBSEARCH }
},
{
status: MessageBlockStatus.SUCCESS

View File

@ -26,7 +26,7 @@ import type {
NormalToolResponse,
WebSearchProviderResponse
} from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { CitationMessageBlock, MessageBlock, ToolMessageBlock } from '@renderer/types/newMessage'
import { MessageBlockType } from '@renderer/types/newMessage'
@ -120,7 +120,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
// 1. Handle Web Search Responses
if (block.response) {
switch (block.response.source) {
case WebSearchSource.GEMINI: {
case WEB_SEARCH_SOURCE.GEMINI: {
const groundingMetadata = block.response.results as GroundingMetadata
formattedCitations =
groundingMetadata?.groundingChunks?.map((chunk, index) => ({
@ -133,7 +133,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
})) || []
break
}
case WebSearchSource.OPENAI_RESPONSE:
case WEB_SEARCH_SOURCE.OPENAI_RESPONSE:
formattedCitations =
(block.response.results as OpenAI.Responses.ResponseOutputText.URLCitation[])?.map((result, index) => {
let hostname: string | undefined
@ -152,7 +152,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
}
}) || []
break
case WebSearchSource.OPENAI:
case WEB_SEARCH_SOURCE.OPENAI:
formattedCitations =
(block.response.results as OpenAI.Chat.Completions.ChatCompletionMessage.Annotation[])?.map((url, index) => {
const urlCitation = url.url_citation
@ -172,7 +172,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
}
}) || []
break
case WebSearchSource.ANTHROPIC:
case WEB_SEARCH_SOURCE.ANTHROPIC:
formattedCitations =
(block.response.results as Array<WebSearchResultBlock>)?.map((result, index) => {
const { url } = result
@ -192,7 +192,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
}
}) || []
break
case WebSearchSource.PERPLEXITY: {
case WEB_SEARCH_SOURCE.PERPLEXITY: {
formattedCitations =
(block.response.results as any[])?.map((result, index) => ({
number: index + 1,
@ -203,8 +203,8 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
})) || []
break
}
case WebSearchSource.GROK:
case WebSearchSource.OPENROUTER:
case WEB_SEARCH_SOURCE.GROK:
case WEB_SEARCH_SOURCE.OPENROUTER:
formattedCitations =
(block.response.results as AISDKWebSearchResult[])?.map((result, index) => {
const url = result.url
@ -230,8 +230,8 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
}
}) || []
break
case WebSearchSource.ZHIPU:
case WebSearchSource.HUNYUAN:
case WEB_SEARCH_SOURCE.ZHIPU:
case WEB_SEARCH_SOURCE.HUNYUAN:
formattedCitations =
(block.response.results as any[])?.map((result, index) => ({
number: index + 1,
@ -241,7 +241,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
type: 'websearch'
})) || []
break
case WebSearchSource.WEBSEARCH:
case WEB_SEARCH_SOURCE.WEBSEARCH:
formattedCitations =
(block.response.results as WebSearchProviderResponse)?.results?.map((result, index) => ({
number: index + 1,
@ -252,7 +252,7 @@ export const formatCitationsFromBlock = (block: CitationMessageBlock | undefined
type: 'websearch'
})) || []
break
case WebSearchSource.AISDK:
case WEB_SEARCH_SOURCE.AISDK:
formattedCitations =
(block.response?.results as AISDKWebSearchResult[])?.map((result, index) => ({
number: index + 1,

View File

@ -6,7 +6,7 @@ import type { AppDispatch } from '@renderer/store'
import { messageBlocksSlice } from '@renderer/store/messageBlock'
import { messagesSlice } from '@renderer/store/newMessage'
import type { Assistant, ExternalToolResult, MCPTool, Model } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import type { Chunk } from '@renderer/types/chunk'
import { ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
@ -563,7 +563,7 @@ describe('streamCallback Integration Tests', () => {
const callbacks = createMockCallbacks(mockAssistantMsgId, mockTopicId, mockAssistant, dispatch, getState)
const mockWebSearchResult = {
source: WebSearchSource.WEBSEARCH,
source: WEB_SEARCH_SOURCE.WEBSEARCH,
results: [{ title: 'Test Result', url: 'http://example.com', snippet: 'Test snippet' }]
}
@ -722,7 +722,7 @@ describe('streamCallback Integration Tests', () => {
const mockExternalToolResult: ExternalToolResult = {
webSearch: {
source: WebSearchSource.WEBSEARCH,
source: WEB_SEARCH_SOURCE.WEBSEARCH,
results: [{ title: 'External Result', url: 'http://external.com', snippet: 'External snippet' }]
},
knowledge: [

View File

@ -720,20 +720,24 @@ export type WebSearchResults =
| AISDKWebSearchResult[]
| any[]
export enum WebSearchSource {
WEBSEARCH = 'websearch',
OPENAI = 'openai',
OPENAI_RESPONSE = 'openai-response',
OPENROUTER = 'openrouter',
ANTHROPIC = 'anthropic',
GEMINI = 'gemini',
PERPLEXITY = 'perplexity',
QWEN = 'qwen',
HUNYUAN = 'hunyuan',
ZHIPU = 'zhipu',
GROK = 'grok',
AISDK = 'ai-sdk'
}
export const WEB_SEARCH_SOURCE = {
WEBSEARCH: 'websearch',
OPENAI: 'openai',
OPENAI_RESPONSE: 'openai-response',
OPENROUTER: 'openrouter',
ANTHROPIC: 'anthropic',
GEMINI: 'gemini',
PERPLEXITY: 'perplexity',
QWEN: 'qwen',
HUNYUAN: 'hunyuan',
ZHIPU: 'zhipu',
GROK: 'grok',
AISDK: 'ai-sdk'
} as const
export const WebSearchSourceSchema = z.enum(objectValues(WEB_SEARCH_SOURCE))
export type WebSearchSource = z.infer<typeof WebSearchSourceSchema>
export type WebSearchResponse = {
results?: WebSearchResults

View File

@ -1,6 +1,6 @@
import type { GroundingSupport } from '@google/genai'
import type { Citation } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import { describe, expect, it, vi } from 'vitest'
import {
@ -33,21 +33,21 @@ describe('citation', () => {
describe('determineCitationSource', () => {
it('should find the the citation source', () => {
const citationReferences = [{ citationBlockId: 'block1', citationBlockSource: WebSearchSource.OPENAI }]
const citationReferences = [{ citationBlockId: 'block1', citationBlockSource: WEB_SEARCH_SOURCE.OPENAI }]
const result = determineCitationSource(citationReferences)
expect(result).toBe(WebSearchSource.OPENAI)
expect(result).toBe(WEB_SEARCH_SOURCE.OPENAI)
})
it('should find first valid source in citation references', () => {
const citationReferences = [
{ citationBlockId: 'block1' }, // no source
{ citationBlockId: 'block2', citationBlockSource: WebSearchSource.GEMINI },
{ citationBlockId: 'block3', citationBlockSource: WebSearchSource.GEMINI }
{ citationBlockId: 'block2', citationBlockSource: WEB_SEARCH_SOURCE.GEMINI },
{ citationBlockId: 'block3', citationBlockSource: WEB_SEARCH_SOURCE.GEMINI }
]
const result = determineCitationSource(citationReferences)
expect(result).toBe(WebSearchSource.GEMINI)
expect(result).toBe(WEB_SEARCH_SOURCE.GEMINI)
})
it('should return undefined when no sources available', () => {
@ -99,7 +99,7 @@ describe('citation', () => {
}
]
const result = withCitationTags(content, citations, WebSearchSource.OPENAI)
const result = withCitationTags(content, citations, WEB_SEARCH_SOURCE.OPENAI)
expect(result).toContain('[<sup data-citation=')
expect(result).toContain('1</sup>](https://example.com)')
@ -122,7 +122,7 @@ describe('citation', () => {
}
]
const result = withCitationTags(content, citations, WebSearchSource.GEMINI)
const result = withCitationTags(content, citations, WEB_SEARCH_SOURCE.GEMINI)
expect(result).toContain('Test content[<sup data-citation=')
expect(result).toContain('1</sup>](https://example.com)')
@ -239,7 +239,7 @@ Numbered list:
const citations: Citation[] = [{ number: 1, url: 'https://example.com', title: 'Test' }]
const citationMap = createCitationMap(citations)
for (const sourceType of [WebSearchSource.OPENAI, WebSearchSource.OPENAI_RESPONSE]) {
for (const sourceType of [WEB_SEARCH_SOURCE.OPENAI, WEB_SEARCH_SOURCE.OPENAI_RESPONSE]) {
const result = normalizeCitationMarks(content, citationMap, sourceType)
expect(result).toBe('Text with [cite:1] citation')
}
@ -250,7 +250,7 @@ Numbered list:
const citations: Citation[] = [{ number: 1, url: 'https://example.com', title: 'Test' }]
const citationMap = createCitationMap(citations)
for (const sourceType of [WebSearchSource.OPENAI, WebSearchSource.OPENAI_RESPONSE]) {
for (const sourceType of [WEB_SEARCH_SOURCE.OPENAI, WEB_SEARCH_SOURCE.OPENAI_RESPONSE]) {
const result = normalizeCitationMarks(content, citationMap, sourceType)
expect(result).toBe('Text with [<sup>3</sup>](https://missing.com) citation')
}
@ -265,7 +265,7 @@ Numbered list:
]
const citationMap = new Map(citations.map((c) => [c.number, c]))
const normalized = normalizeCitationMarks(content, citationMap, WebSearchSource.PERPLEXITY)
const normalized = normalizeCitationMarks(content, citationMap, WEB_SEARCH_SOURCE.PERPLEXITY)
expect(normalized).toBe('Perplexity citations [cite:1]')
})
@ -275,7 +275,7 @@ Numbered list:
const citationMap = new Map(citations.map((c) => [c.number, c]))
// 2号引用不存在应该保持原样
const normalized = normalizeCitationMarks(content, citationMap, WebSearchSource.PERPLEXITY)
const normalized = normalizeCitationMarks(content, citationMap, WEB_SEARCH_SOURCE.PERPLEXITY)
expect(normalized).toBe('Text with [<sup>2</sup>](https://notfound.com) citation')
})
})
@ -295,7 +295,7 @@ Numbered list:
]
const citationMap = createCitationMap(citations)
const result = normalizeCitationMarks(content, citationMap, WebSearchSource.GEMINI)
const result = normalizeCitationMarks(content, citationMap, WEB_SEARCH_SOURCE.GEMINI)
expect(result).toBe('This is test content[cite:1][cite:2] from Gemini')
})
@ -305,7 +305,7 @@ Numbered list:
const citations: Citation[] = [{ number: 1, url: 'https://example.com', title: 'Test' }]
const citationMap = createCitationMap(citations)
const result = normalizeCitationMarks(content, citationMap, WebSearchSource.GEMINI)
const result = normalizeCitationMarks(content, citationMap, WEB_SEARCH_SOURCE.GEMINI)
expect(result).toBe('Content without metadata')
})
@ -358,7 +358,7 @@ Numbered list:
]
const citationMap = createCitationMap(citations)
const result = normalizeCitationMarks(content, citationMap, WebSearchSource.OPENAI)
const result = normalizeCitationMarks(content, citationMap, WEB_SEARCH_SOURCE.OPENAI)
expect(result).toBe('Text with [1] and [cite:2] and other [3] formats')
})

View File

@ -1,6 +1,6 @@
import type { GroundingSupport } from '@google/genai'
import type { Citation } from '@renderer/types'
import { WebSearchSource } from '@renderer/types'
import type { Citation, WebSearchSource } from '@renderer/types'
import { WEB_SEARCH_SOURCE } from '@renderer/types'
import { cleanMarkdownContent, encodeHTML } from './formats'
@ -112,9 +112,9 @@ export function normalizeCitationMarks(
}
switch (sourceType) {
case WebSearchSource.OPENAI:
case WebSearchSource.OPENAI_RESPONSE:
case WebSearchSource.PERPLEXITY: {
case WEB_SEARCH_SOURCE.OPENAI:
case WEB_SEARCH_SOURCE.OPENAI_RESPONSE:
case WEB_SEARCH_SOURCE.PERPLEXITY: {
// OpenAI 格式: [<sup>N</sup>](url) → [cite:N]
applyReplacements(/\[<sup>(\d+)<\/sup>\]\([^)]*\)/g, (match) => {
const citationNum = parseInt(match[1], 10)
@ -122,7 +122,7 @@ export function normalizeCitationMarks(
})
break
}
case WebSearchSource.GEMINI: {
case WEB_SEARCH_SOURCE.GEMINI: {
// Gemini 格式: 根据metadata添加 [cite:N]
const firstCitation = Array.from(citationMap.values())[0]
if (firstCitation?.metadata) {