mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-02-22 02:24:46 +08:00
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:
parent
e0b733907e
commit
40317cd3c8
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) ||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user