Merge remote-tracking branch 'origin/main' into 1600822305-patch-2

This commit is contained in:
1600822305 2025-04-11 17:00:07 +08:00
commit fa4dfecfe1
31 changed files with 755 additions and 105 deletions

View File

@ -90,7 +90,9 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
知识库和服务商界面更新
增加 Dangbei 小程序
可以强制使用搜索引擎覆盖模型自带搜索能力
修复部分公式无法正常渲染问题
增加对 grok-3 和 Grok-3-mini 的支持
助手支持使用拼音排序
网络搜索增加 Baidu, Google, Bing 支持(免费使用)
网络搜索增加 uBlacklist 订阅
快速面板 (QuickPanel) 进行性能优化
解决 mcp 依赖工具下载速度问题

View File

@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.2.1",
"version": "1.2.2",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",

View File

@ -6,8 +6,8 @@ const AdmZip = require('adm-zip')
const { downloadWithRedirects } = require('./download')
// Base URL for downloading bun binaries
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download'
const DEFAULT_BUN_VERSION = '1.2.9' // Default fallback version
// Mapping of platform+arch to binary package name
const BUN_PACKAGES = {

View File

@ -7,8 +7,8 @@ const AdmZip = require('adm-zip')
const { downloadWithRedirects } = require('./download')
// Base URL for downloading uv binaries
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.6'
const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download'
const DEFAULT_UV_VERSION = '0.6.14'
// Mapping of platform+arch to binary package name
const UV_PACKAGES = {

View File

@ -3,10 +3,12 @@ import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
import { registerIpc } from './ipc'
import { configManager } from './services/ConfigManager'
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
import mcpService from './services/MCPService'
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
@ -96,6 +98,15 @@ if (!app.requestSingleInstanceLock()) {
app.isQuitting = true
})
app.on('will-quit', async () => {
// event.preventDefault()
try {
await mcpService.cleanup()
} catch (error) {
Logger.error('Error cleaning up MCP service:', error)
}
})
// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.
}

View File

@ -39,6 +39,7 @@ class McpService {
this.removeServer = this.removeServer.bind(this)
this.restartServer = this.restartServer.bind(this)
this.stopServer = this.stopServer.bind(this)
this.cleanup = this.cleanup.bind(this)
}
async initClient(server: MCPServer): Promise<Client> {
@ -205,6 +206,16 @@ class McpService {
await this.initClient(server)
}
async cleanup() {
for (const [key] of this.clients) {
try {
await this.closeClient(key)
} catch (error) {
Logger.error(`[MCP] Failed to close client: ${error}`)
}
}
}
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
const client = await this.initClient(server)
const serverKey = this.getServerKey(server)
@ -324,4 +335,5 @@ class McpService {
}
}
export default new McpService()
const mcpService = new McpService()
export default mcpService

View File

@ -186,7 +186,7 @@ export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|
// Reasoning models
export const REASONING_REGEX =
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*)$/i
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-3-mini(?:-[\w-]+)?\b.*)$/i
// Embedding models
export const EMBEDDING_REGEX =
@ -210,7 +210,8 @@ export const FUNCTION_CALLING_MODELS = [
'deepseek',
'glm-4(?:-[\\w-]+)?',
'learnlm(?:-[\\w-]+)?',
'gemini(?:-[\\w-]+)?' // 提前排除了gemini的嵌入模型
'gemini(?:-[\\w-]+)?', // 提前排除了gemini的嵌入模型
'grok-3(?:-[\\w-]+)?'
]
const FUNCTION_CALLING_EXCLUDED_MODELS = [
@ -2202,12 +2203,29 @@ export function isOpenAIWebSearch(model: Model): boolean {
return model.id.includes('gpt-4o-search-preview') || model.id.includes('gpt-4o-mini-search-preview')
}
export function isSupportedResoningEffortModel(model?: Model): boolean {
export function isSupportedReasoningEffortModel(model?: Model): boolean {
if (!model) {
return false
}
if (model.id.includes('claude-3-7-sonnet') || model.id.includes('claude-3.7-sonnet') || isOpenAIoSeries(model)) {
if (
model.id.includes('claude-3-7-sonnet') ||
model.id.includes('claude-3.7-sonnet') ||
isOpenAIoSeries(model) ||
isGrokReasoningModel(model)
) {
return true
}
return false
}
export function isGrokReasoningModel(model?: Model): boolean {
if (!model) {
return false
}
if (model.id.includes('grok-3-mini')) {
return true
}

View File

@ -1,6 +1,10 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addSubscribeSource as _addSubscribeSource,
removeSubscribeSource as _removeSubscribeSource,
setDefaultProvider as _setDefaultProvider,
setSubscribeSources as _setSubscribeSources,
updateSubscribeBlacklist as _updateSubscribeBlacklist,
updateWebSearchProvider,
updateWebSearchProviders
} from '@renderer/store/websearch'
@ -57,3 +61,32 @@ export const useWebSearchProvider = (id: string) => {
return { provider, updateProvider }
}
export const useBlacklist = () => {
const dispatch = useAppDispatch()
const websearch = useAppSelector((state) => state.websearch)
const addSubscribeSource = ({ url, name, blacklist }) => {
dispatch(_addSubscribeSource({ url, name, blacklist }))
}
const removeSubscribeSource = (key: number) => {
dispatch(_removeSubscribeSource(key))
}
const updateSubscribeBlacklist = (key: number, blacklist: string[]) => {
dispatch(_updateSubscribeBlacklist({ key, blacklist }))
}
const setSubscribeSources = (sources: { key: number; url: string; name: string; blacklist?: string[] }[]) => {
dispatch(_setSubscribeSources(sources))
}
return {
websearch,
addSubscribeSource,
removeSubscribeSource,
updateSubscribeBlacklist,
setSubscribeSources
}
}

View File

@ -76,7 +76,7 @@
"settings.reasoning_effort.low": "low",
"settings.reasoning_effort.medium": "medium",
"settings.reasoning_effort.off": "off",
"settings.reasoning_effort.tip": "Only supports OpenAI o-series and Anthropic reasoning models",
"settings.reasoning_effort.tip": "Only supported by OpenAI o-series, Anthropic, and Grok reasoning models",
"settings.more": "Assistant Settings"
},
"auth": {
@ -1324,6 +1324,14 @@
"title": "Tavily"
},
"title": "Web Search",
"subscribe": "Blacklist Subscription",
"subscribe_update": "Update now",
"subscribe_add": "Add Subscription",
"subscribe_url": "Subscription feed address",
"subscribe_name": "Alternative name",
"subscribe_name.placeholder": "Alternative name used when the downloaded subscription feed has no name.",
"subscribe_add_success": "Subscription feed added successfully!",
"subscribe_delete": "Delete subscription source",
"overwrite": "Override search service",
"overwrite_tooltip": "Force use search service instead of LLM",
"apikey": "API key",

View File

@ -59,7 +59,7 @@
"settings.reasoning_effort.low": "短い",
"settings.reasoning_effort.medium": "中程度",
"settings.reasoning_effort.off": "オフ",
"settings.reasoning_effort.tip": "OpenAIのoシリーズとAnthropicの推論モデルのみサポートしています",
"settings.reasoning_effort.tip": "OpenAI o-series、Anthropic、および Grok の推論モデルのみサポート",
"settings.more": "アシスタント設定"
},
"auth": {
@ -1286,7 +1286,6 @@
"websearch": {
"blacklist": "ブラックリスト",
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
"check": "チェック",
"check_failed": "検証に失敗しました",
"check_success": "検証に成功しました",
@ -1306,6 +1305,15 @@
"title": "Tavily"
},
"title": "ウェブ検索",
"blacklist_tooltip": "マッチパターン: *://*.example.com/*\n正規表現: /example\\.(net|org)/",
"subscribe": "ブラックリスト購読",
"subscribe_update": "今すぐ更新",
"subscribe_add": "サブスクリプションを追加",
"subscribe_url": "フィードのURL",
"subscribe_name": "代替名",
"subscribe_name.placeholder": "ダウンロードしたフィードに名前がない場合に使用される代替名",
"subscribe_add_success": "フィードの追加が成功しました!",
"subscribe_delete": "フィードの削除",
"overwrite": "サービス検索を上書き",
"overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する",
"apikey": "API キー",

View File

@ -59,7 +59,7 @@
"settings.reasoning_effort.low": "Короткая",
"settings.reasoning_effort.medium": "Средняя",
"settings.reasoning_effort.off": "Выключено",
"settings.reasoning_effort.tip": "Поддерживается только моделями с рассуждением OpenAI o-series и Anthropic",
"settings.reasoning_effort.tip": "Поддерживается только моделями рассуждений OpenAI o-series, Anthropic и Grok",
"settings.more": "Настройки ассистента"
},
"auth": {
@ -1286,7 +1286,6 @@
"websearch": {
"blacklist": "Черный список",
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
"check": "проверка",
"check_failed": "Проверка не прошла",
"check_success": "Проверка успешна",
@ -1306,6 +1305,15 @@
"title": "Tavily"
},
"title": "Поиск в Интернете",
"blacklist_tooltip": "Соответствующий шаблон: *://*.example.com/*\nРегулярное выражение: /example\\.(net|org)/",
"subscribe": "Черный список подписки",
"subscribe_update": "Обновить сейчас",
"subscribe_add": "Добавить подписку",
"subscribe_url": "Адрес источника подписки",
"subscribe_name": "альтернативное имя",
"subscribe_name.placeholder": "替代名称, используемый, когда загружаемый подписочный источник не имеет названия",
"subscribe_add_success": "Подписка добавлена успешно!",
"subscribe_delete": "Удалить источник подписки",
"overwrite": "Переопределить поставщика поиска",
"overwrite_tooltip": "Использовать поставщика поиска вместо LLM",
"apikey": "Ключ API",

View File

@ -76,7 +76,7 @@
"settings.reasoning_effort.low": "短",
"settings.reasoning_effort.medium": "中",
"settings.reasoning_effort.off": "关",
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series 和 Anthropic 推理模型",
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型",
"settings.more": "助手设置"
},
"auth": {
@ -1304,7 +1304,7 @@
"websearch": {
"blacklist": "黑名单",
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
"blacklist_tooltip": "请使用以下格式(换行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
"blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
"check": "检查",
"check_failed": "验证失败",
"check_success": "验证成功",
@ -1317,6 +1317,14 @@
"search_max_result": "搜索结果个数",
"search_provider": "搜索服务商",
"search_provider_placeholder": "选择一个搜索服务商",
"subscribe": "黑名单订阅",
"subscribe_update": "立即更新",
"subscribe_add": "添加订阅",
"subscribe_url": "订阅源地址",
"subscribe_name": "替代名字",
"subscribe_name.placeholder": "当下载的订阅源没有名称时所使用的替代名称",
"subscribe_add_success": "订阅源添加成功!",
"subscribe_delete": "删除订阅源",
"search_result_default": "默认",
"search_with_time": "搜索包含日期",
"tavily": {

View File

@ -59,7 +59,7 @@
"settings.reasoning_effort.low": "短",
"settings.reasoning_effort.medium": "中",
"settings.reasoning_effort.off": "關",
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series 和 Anthropic 推理模型",
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series、Anthropic 和 Grok 推理模型",
"settings.more": "助手設定"
},
"auth": {
@ -1284,20 +1284,10 @@
"tray.show": "顯示系统匣圖示",
"tray.title": "系统匣",
"websearch": {
"blacklist": "黑名單",
"blacklist_description": "以下網站不會出現在搜尋結果中",
"blacklist_tooltip": "請使用以下格式 (換行符號分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
"check": "檢查",
"check_failed": "驗證失敗",
"check_success": "驗證成功",
"enhance_mode": "搜索增強模式",
"enhance_mode_tooltip": "使用預設模型提取關鍵詞後搜索",
"get_api_key": "點選這裡取得金鑰",
"no_provider_selected": "請選擇搜尋服務商後再檢查",
"search_max_result": "搜尋結果個數",
"search_provider": "搜尋服務商",
"search_provider_placeholder": "選擇一個搜尋服務商",
"search_result_default": "預設",
"search_with_time": "搜尋包含日期",
"tavily": {
"api_key": "Tavily API 金鑰",
@ -1305,6 +1295,24 @@
"description": "Tavily 是一個為 AI 代理量身訂製的搜尋引擎,提供即時、準確的結果、智慧查詢建議和深入的研究能力",
"title": "Tavily"
},
"blacklist": "黑名單",
"blacklist_description": "以下網站不會出現在搜索結果中",
"search_max_result": "搜尋結果個數",
"search_result_default": "預設",
"check": "檢查",
"search_provider": "搜尋服務商",
"search_provider_placeholder": "選擇一個搜尋服務商",
"no_provider_selected": "請選擇搜索服務商後再檢查",
"check_failed": "驗證失敗",
"blacklist_tooltip": "匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/",
"subscribe": "黑名單訂閱",
"subscribe_update": "立即更新",
"subscribe_add": "添加訂閱",
"subscribe_url": "訂閱源地址",
"subscribe_name": "替代名稱",
"subscribe_name.placeholder": "當下載的訂閱源沒有名稱時所使用的替代名稱",
"subscribe_add_success": "訂閱源添加成功!",
"subscribe_delete": "刪除訂閱源",
"title": "網路搜尋",
"overwrite": "覆蓋搜尋服務商",
"overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",

View File

@ -129,12 +129,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
return match ? (
<CodeBlockWrapper className="code-block">
<CodeHeader>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{codeCollapsible && shouldShowExpandButton && (
<CollapseIcon expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)} />
)}
<CodeLanguage>{'<' + language.toUpperCase() + '>'}</CodeLanguage>
</div>
<CodeLanguage>{'<' + language.toUpperCase() + '>'}</CodeLanguage>
</CodeHeader>
<StickyWrapper>
<HStack
@ -144,6 +139,9 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
style={{ bottom: '0.2rem', right: '1rem', height: '27px' }}>
{showDownloadButton && <DownloadButton language={language} data={children} />}
{codeWrappable && <UnwrapButton unwrapped={isUnwrapped} onClick={() => setIsUnwrapped(!isUnwrapped)} />}
{codeCollapsible && shouldShowExpandButton && (
<CollapseIcon expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)} />
)}
<CopyButton text={children} />
</HStack>
</StickyWrapper>
@ -186,10 +184,23 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
}
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
const { t } = useTranslation()
const [tooltipVisible, setTooltipVisible] = useState(false)
const handleClick = () => {
setTooltipVisible(false)
onClick()
}
return (
<CollapseIconWrapper onClick={onClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
<Tooltip
title={expanded ? t('code_block.collapse') : t('code_block.expand')}
open={tooltipVisible}
onOpenChange={setTooltipVisible}>
<CollapseIconWrapper onClick={handleClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
</Tooltip>
)
}
@ -227,6 +238,7 @@ const UnwrapButton: React.FC<{ unwrapped: boolean; onClick: () => void }> = ({ u
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
const [copied, setCopied] = useState(false)
const { t } = useTranslation()
const copy = t('common.copy')
const onCopy = () => {
if (!text) return
@ -236,10 +248,12 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
setTimeout(() => setCopied(false), 2000)
}
return copied ? (
<CheckOutlined style={{ color: 'var(--color-primary)', ...style }} />
) : (
<CopyIcon className="copy" style={style} onClick={onCopy} />
return (
<Tooltip title={copy}>
<CopyButtonWrapper onClick={onCopy} style={style}>
{copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon className="copy" />}
</CopyButtonWrapper>
</Tooltip>
)
}
@ -319,14 +333,6 @@ const CodeHeader = styled.div`
padding: 0 10px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
.copy {
cursor: pointer;
color: var(--color-text-3);
transition: color 0.3s;
}
.copy:hover {
color: var(--color-text-1);
}
`
const CodeLanguage = styled.div`
@ -348,7 +354,19 @@ const CodeFooter = styled.div`
color: var(--color-text-1);
}
`
const CopyButtonWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-text-3);
transition: color 0.3s;
font-size: 16px;
&:hover {
color: var(--color-text-1);
}
`
const ExpandButtonWrapper = styled.div`
position: relative;
cursor: pointer;
@ -388,7 +406,6 @@ const CollapseIconWrapper = styled.div`
transition: all 0.2s ease;
&:hover {
background-color: var(--color-background-soft);
color: var(--color-text-1);
}
`

View File

@ -8,7 +8,7 @@ import {
isMac,
isWindows
} from '@renderer/config/constant'
import { isSupportedResoningEffortModel } from '@renderer/config/models'
import { isGrokReasoningModel, isSupportedReasoningEffortModel } from '@renderer/config/models'
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@ -44,7 +44,7 @@ import {
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { Button, Col, InputNumber, Row, Segmented, Select, Slider, Switch, Tooltip } from 'antd'
import { FC, useEffect, useState } from 'react'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -115,9 +115,12 @@ const SettingsTab: FC<Props> = (props) => {
}
}
const onReasoningEffortChange = (value) => {
updateAssistantSettings({ reasoning_effort: value })
}
const onReasoningEffortChange = useCallback(
(value?: 'low' | 'medium' | 'high') => {
updateAssistantSettings({ reasoning_effort: value })
},
[updateAssistantSettings]
)
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
@ -146,7 +149,21 @@ const SettingsTab: FC<Props> = (props) => {
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
setStreamOutput(assistant?.settings?.streamOutput ?? true)
setReasoningEffort(assistant?.settings?.reasoning_effort)
}, [assistant])
// 当是Grok模型时处理reasoning_effort的设置
// For Grok models, only 'low' and 'high' reasoning efforts are supported.
// This ensures compatibility with the model's capabilities and avoids unsupported configurations.
if (isGrokReasoningModel(assistant?.model || getDefaultModel())) {
const currentEffort = assistant?.settings?.reasoning_effort
if (!currentEffort || currentEffort === 'low') {
setReasoningEffort('low') // Default to 'low' if no effort is set or if it's already 'low'.
onReasoningEffortChange('low')
} else if (currentEffort === 'medium' || currentEffort === 'high') {
setReasoningEffort('high') // Force 'high' for 'medium' or 'high' to simplify the configuration.
onReasoningEffortChange('high')
}
}
}, [assistant, onReasoningEffortChange])
const formatSliderTooltip = (value?: number) => {
if (value === undefined) return ''
@ -263,7 +280,7 @@ const SettingsTab: FC<Props> = (props) => {
</Col>
</Row>
)}
{isSupportedResoningEffortModel(assistant?.model || getDefaultModel()) && (
{isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && (
<>
<SettingDivider />
<Row align="middle">
@ -282,12 +299,19 @@ const SettingsTab: FC<Props> = (props) => {
setReasoningEffort(typedValue)
onReasoningEffortChange(typedValue)
}}
options={[
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
{ value: 'medium', label: t('assistants.settings.reasoning_effort.medium') },
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') },
{ value: 'off', label: t('assistants.settings.reasoning_effort.off') }
]}
options={
isGrokReasoningModel(assistant?.model || getDefaultModel())
? [
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') }
]
: [
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
{ value: 'medium', label: t('assistants.settings.reasoning_effort.medium') },
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') },
{ value: 'off', label: t('assistants.settings.reasoning_effort.off') }
]
}
name="group"
block
/>

View File

@ -43,8 +43,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
} catch (error: any) {
window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
setIsInstallingUv(false)
checkBinaries()
}
setTimeout(checkBinaries, 1000)
}
const installBun = async () => {
@ -59,8 +59,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
key: 'mcp-install-error'
})
setIsInstallingBun(false)
checkBinaries()
}
setTimeout(checkBinaries, 1000)
}
useEffect(() => {

View File

@ -0,0 +1,120 @@
import { TopView } from '@renderer/components/TopView'
import { Button, Form, FormProps, Input, Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ShowParams {
title: string
}
interface Props extends ShowParams {
resolve: (data: any) => void
}
type FieldType = {
url: string
name?: string
}
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
const url = values.url.trim()
const name = values.name?.trim() || url
if (!url) {
window.message.error(t('settings.websearch.url_required'))
return
}
// 验证URL格式
try {
new URL(url)
} catch (e) {
window.message.error(t('settings.websearch.url_invalid'))
return
}
resolve({ url, name })
}
return (
<Modal
title={title}
open={open}
onOk={onOk}
onCancel={onCancel}
maskClosable={false}
afterClose={onClose}
footer={null}
centered>
<Form
form={form}
labelCol={{ flex: '110px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 25 }}
onFinish={onFinish}>
<Form.Item name="url" label={t('settings.websearch.subscribe_url')} rules={[{ required: true }]}>
<Input
placeholder="https://git.io/ublacklist"
spellCheck={false}
maxLength={500}
onChange={(e) => {
try {
const url = new URL(e.target.value)
form.setFieldValue('name', url.hostname)
} catch (e) {
// URL不合法忽略
}
}}
/>
</Form.Item>
<Form.Item name="name" label={t('settings.websearch.subscribe_name')}>
<Input placeholder={t('settings.websearch.subscribe_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item label=" ">
<Button type="primary" htmlType="submit">
{t('settings.websearch.subscribe_add')}
</Button>
</Form.Item>
</Form>
</Modal>
)
}
export default class AddSubscribePopup {
static topviewId = 0
static hide() {
TopView.hide('AddSubscribePopup')
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
{...props}
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'AddSubscribePopup'
)
})
}
}

View File

@ -46,8 +46,8 @@ const BasicSettings: FC = () => {
</SettingRowTitle>
<Switch checked={enhanceMode} onChange={(checked) => dispatch(setEnhanceMode(checked))} />
</SettingRow>
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
<SettingRow>
<SettingDivider style={{ marginTop: 15, marginBottom: 10 }} />
<SettingRow style={{ height: 40 }}>
<SettingRowTitle>{t('settings.websearch.search_max_result')}</SettingRowTitle>
<Slider
defaultValue={maxResults}

View File

@ -1,22 +1,63 @@
import { CheckOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useBlacklist } from '@renderer/hooks/useWebSearchProviders'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setExcludeDomains } from '@renderer/store/websearch'
import { parseMatchPattern } from '@renderer/utils/blacklistMatchPattern'
import { Alert, Button } from 'antd'
import { parseMatchPattern, parseSubscribeContent } from '@renderer/utils/blacklistMatchPattern'
import { Alert, Button, Table, TableProps } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { t } from 'i18next'
import { FC, useEffect, useState } from 'react'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import AddSubscribePopup from './AddSubscribePopup'
type TableRowSelection<T extends object = object> = TableProps<T>['rowSelection']
interface DataType {
key: React.Key
url: string
name: string
}
const columns: TableProps<DataType>['columns'] = [
{ title: t('common.name'), dataIndex: 'name', key: 'name' },
{
title: 'URL',
dataIndex: 'url',
key: 'url'
}
]
const BlacklistSettings: FC = () => {
const [errFormat, setErrFormat] = useState(false)
const [blacklistInput, setBlacklistInput] = useState('')
const excludeDomains = useAppSelector((state) => state.websearch.excludeDomains)
const { websearch, setSubscribeSources, addSubscribeSource } = useBlacklist()
const { theme } = useTheme()
const [subscribeChecking, setSubscribeChecking] = useState(false)
const [subscribeValid, setSubscribeValid] = useState(false)
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
const [dataSource, setDataSource] = useState<DataType[]>(
websearch.subscribeSources?.map((source) => ({
key: source.key,
url: source.url,
name: source.name
})) || []
)
const dispatch = useAppDispatch()
useEffect(() => {
setDataSource(
(websearch.subscribeSources || []).map((source) => ({
key: source.key,
url: source.url,
name: source.name
}))
)
console.log('subscribeSources', websearch.subscribeSources)
}, [websearch.subscribeSources])
useEffect(() => {
if (excludeDomains) {
setBlacklistInput(excludeDomains.join('\n'))
@ -40,6 +81,137 @@ const BlacklistSettings: FC = () => {
if (hasError) return
dispatch(setExcludeDomains(validDomains))
window.message.info({
content: t('message.save.success.title'),
duration: 4,
icon: <InfoCircleOutlined />,
key: 'save-blacklist-info'
})
}
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys)
setSelectedRowKeys(newSelectedRowKeys)
}
const rowSelection: TableRowSelection<DataType> = {
selectedRowKeys,
onChange: onSelectChange
}
async function updateSubscribe() {
setSubscribeChecking(true)
try {
// 获取选中的订阅源
const selectedSources = dataSource.filter((item) => selectedRowKeys.includes(item.key))
// 用于存储所有成功解析的订阅源数据
const updatedSources: {
key: number
url: string
name: string
blacklist: string[]
}[] = []
// 为每个选中的订阅源获取并解析内容
for (const source of selectedSources) {
try {
// 获取并解析订阅源内容
const blacklist = await parseSubscribeContent(source.url)
if (blacklist.length > 0) {
updatedSources.push({
key: Number(source.key),
url: source.url,
name: source.name,
blacklist
})
}
} catch (error) {
console.error(`Error updating subscribe source ${source.url}:`, error)
// 显示具体源更新失败的消息
window.message.warning({
content: t('settings.websearch.subscribe_source_update_failed', { url: source.url }),
duration: 3
})
}
}
if (updatedSources.length > 0) {
// 更新 Redux store
setSubscribeSources(updatedSources)
setSubscribeValid(true)
// 显示成功消息
window.message.success({
content: t('settings.websearch.subscribe_update_success'),
duration: 2
})
setTimeout(() => setSubscribeValid(false), 3000)
} else {
setSubscribeValid(false)
throw new Error('No valid sources updated')
}
} catch (error) {
console.error('Error updating subscribes:', error)
window.message.error({
content: t('settings.websearch.subscribe_update_failed'),
duration: 2
})
}
setSubscribeChecking(false)
}
// 修改 handleAddSubscribe 函数
async function handleAddSubscribe() {
setSubscribeChecking(true)
const result = await AddSubscribePopup.show({
title: t('settings.websearch.subscribe_add')
})
if (result && result.url) {
try {
// 获取并解析订阅源内容
const blacklist = await parseSubscribeContent(result.url)
if (blacklist.length === 0) {
throw new Error('No valid patterns found in subscribe content')
}
// 添加到 Redux store
addSubscribeSource({
url: result.url,
name: result.name || result.url,
blacklist
})
setSubscribeValid(true)
// 显示成功消息
window.message.success({
content: t('settings.websearch.subscribe_add_success'),
duration: 2
})
setTimeout(() => setSubscribeValid(false), 3000)
} catch (error) {
setSubscribeValid(false)
window.message.error({
content: t('settings.websearch.subscribe_add_failed'),
duration: 2
})
}
}
setSubscribeChecking(false)
}
function handleDeleteSubscribe() {
try {
// 过滤掉被选中要删除的项目
const remainingSources =
websearch.subscribeSources?.filter((source) => !selectedRowKeys.includes(source.key)) || []
// 更新 Redux store
setSubscribeSources(remainingSources)
// 清空选中状态
setSelectedRowKeys([])
} catch (error) {
console.error('Error deleting subscribes:', error)
}
}
return (
@ -62,6 +234,46 @@ const BlacklistSettings: FC = () => {
</Button>
{errFormat && <Alert message={t('settings.websearch.blacklist_tooltip')} type="error" />}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>
{t('settings.websearch.subscribe')}
<Button
type={subscribeValid ? 'primary' : 'default'}
ghost={subscribeValid}
disabled={subscribeChecking}
onClick={handleAddSubscribe}>
{t('settings.websearch.subscribe_add')}
</Button>
</SettingTitle>
<SettingDivider />
<div style={{ display: 'flex', flexDirection: 'column', gap: '5px' }}>
<Table<DataType>
rowSelection={{ type: 'checkbox', ...rowSelection }}
columns={columns}
dataSource={dataSource}
pagination={{ position: ['none'] }}
/>
<SettingRow style={{ height: 50 }}>
<Button
type={subscribeValid ? 'primary' : 'default'}
ghost={subscribeValid}
disabled={subscribeChecking || selectedRowKeys.length === 0}
style={{ width: 100 }}
onClick={updateSubscribe}>
{subscribeChecking ? (
<LoadingOutlined spin />
) : subscribeValid ? (
<CheckOutlined />
) : (
t('settings.websearch.subscribe_update')
)}
</Button>
<Button style={{ width: 100 }} disabled={selectedRowKeys.length === 0} onClick={handleDeleteSubscribe}>
{t('settings.websearch.subscribe_delete')}
</Button>
</SettingRow>
</div>
</SettingGroup>
</>
)
}

View File

@ -1,6 +1,7 @@
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import {
getOpenAIWebSearchParams,
isGrokReasoningModel,
isHunyuanSearchModel,
isOpenAIoSeries,
isOpenAIWebSearch,
@ -243,6 +244,12 @@ export default class OpenAIProvider extends BaseProvider {
}
}
if (isGrokReasoningModel(model)) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort
}
}
if (isOpenAIoSeries(model)) {
return {
reasoning_effort: assistant?.settings?.reasoning_effort

View File

@ -1,3 +1,4 @@
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
export default abstract class BaseWebSearchProvider {
@ -9,8 +10,7 @@ export default abstract class BaseWebSearchProvider {
this.provider = provider
this.apiKey = this.getApiKey()
}
abstract search(query: string, maxResult: number, excludeDomains: string[]): Promise<WebSearchResponse>
abstract search(query: string, websearch: WebSearchState): Promise<WebSearchResponse>
public getApiKey() {
const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || []

View File

@ -1,4 +1,5 @@
import { ExaClient } from '@agentic/exa'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import BaseWebSearchProvider from './BaseWebSearchProvider'
@ -14,7 +15,7 @@ export default class ExaProvider extends BaseWebSearchProvider {
this.exa = new ExaClient({ apiKey: this.apiKey })
}
public async search(query: string, maxResults: number): Promise<WebSearchResponse> {
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
try {
if (!query.trim()) {
throw new Error('Search query cannot be empty')
@ -22,7 +23,7 @@ export default class ExaProvider extends BaseWebSearchProvider {
const response = await this.exa.search({
query,
numResults: Math.max(1, maxResults),
numResults: Math.max(1, websearch.maxResults),
contents: {
text: true
}

View File

@ -1,5 +1,6 @@
import { Readability } from '@mozilla/readability'
import { nanoid } from '@reduxjs/toolkit'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
import TurndownService from 'turndown'
@ -22,11 +23,7 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
super(provider)
}
public async search(
query: string,
maxResults: number = 15,
excludeDomains: string[] = []
): Promise<WebSearchResponse> {
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
const uid = nanoid()
try {
if (!query.trim()) {
@ -41,16 +38,11 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
const content = await window.api.searchService.openUrlInSearchWindow(uid, url)
// Parse the content to extract URLs and metadata
const searchItems = this.parseValidUrls(content).slice(0, maxResults)
console.log('Total search items:', searchItems)
const searchItems = this.parseValidUrls(content).slice(0, websearch.maxResults)
const validItems = searchItems
.filter(
(item) =>
(item.url.startsWith('http') || item.url.startsWith('https')) &&
excludeDomains.includes(new URL(item.url).host) === false
)
.slice(0, maxResults)
.filter((item) => item.url.startsWith('http') || item.url.startsWith('https'))
.slice(0, websearch.maxResults)
// console.log('Valid search items:', validItems)
// Fetch content for each URL concurrently

View File

@ -1,4 +1,5 @@
import { SearxngClient } from '@agentic/searxng'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import axios from 'axios'
@ -68,7 +69,7 @@ export default class SearxngProvider extends BaseWebSearchProvider {
}
}
public async search(query: string, maxResults: number): Promise<WebSearchResponse> {
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
try {
if (!query) {
throw new Error('Search query cannot be empty')
@ -88,10 +89,9 @@ export default class SearxngProvider extends BaseWebSearchProvider {
if (!result || !Array.isArray(result.results)) {
throw new Error('Invalid search results from SearxNG')
}
return {
query: result.query,
results: result.results.slice(0, maxResults).map((result) => {
results: result.results.slice(0, websearch.maxResults).map((result) => {
return {
title: result.title || 'No title',
content: result.content || '',

View File

@ -1,4 +1,5 @@
import { TavilyClient } from '@agentic/tavily'
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import BaseWebSearchProvider from './BaseWebSearchProvider'
@ -14,7 +15,7 @@ export default class TavilyProvider extends BaseWebSearchProvider {
this.tvly = new TavilyClient({ apiKey: this.apiKey })
}
public async search(query: string, maxResults: number, excludeDomains: string[]): Promise<WebSearchResponse> {
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
try {
if (!query.trim()) {
throw new Error('Search query cannot be empty')
@ -22,10 +23,8 @@ export default class TavilyProvider extends BaseWebSearchProvider {
const result = await this.tvly.search({
query,
max_results: Math.max(1, maxResults),
exclude_domains: excludeDomains || []
max_results: Math.max(1, websearch.maxResults)
})
return {
query: result.query,
results: result.results.map((result) => ({

View File

@ -1,4 +1,6 @@
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
import { filterResultWithBlacklist } from '@renderer/utils/blacklistMatchPattern'
import BaseWebSearchProvider from './BaseWebSearchProvider'
import WebSearchProviderFactory from './WebSearchProviderFactory'
@ -8,7 +10,10 @@ export default class WebSearchEngineProvider {
constructor(provider: WebSearchProvider) {
this.sdk = WebSearchProviderFactory.create(provider)
}
public async search(query: string, maxResult: number, excludeDomains: string[]): Promise<WebSearchResponse> {
return await this.sdk.search(query, maxResult, excludeDomains)
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
const result = await this.sdk.search(query, websearch)
const filteredResult = await filterResultWithBlacklist(result, websearch)
return filteredResult
}
}

View File

@ -97,16 +97,17 @@ class WebSearchService {
* @returns
*/
public async search(provider: WebSearchProvider, query: string): Promise<WebSearchResponse> {
const { searchWithTime, maxResults, excludeDomains } = this.getWebSearchState()
const websearch = this.getWebSearchState()
const webSearchEngine = new WebSearchEngineProvider(provider)
let formattedQuery = query
if (searchWithTime) {
// 有待商榷,效果一般
if (websearch.searchWithTime) {
formattedQuery = `today is ${dayjs().format('YYYY-MM-DD')} \r\n ${query}`
}
try {
return await webSearchEngine.search(formattedQuery, maxResults, excludeDomains)
return await webSearchEngine.search(formattedQuery, websearch)
} catch (error) {
console.error('Search failed:', error)
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`)

View File

@ -541,7 +541,7 @@ export const moveProvider = (providers: Provider[], id: string, position: number
return newProviders
}
const settingsSlice = createSlice({
const llmSlice = createSlice({
name: 'llm',
initialState: isLocalAi ? getIntegratedInitialState() : initialState,
reducers: {
@ -632,6 +632,6 @@ export const {
setLMStudioKeepAliveTime,
setGPUStackKeepAliveTime,
updateModel
} = settingsSlice.actions
} = llmSlice.actions
export default settingsSlice.reducer
export default llmSlice.reducer

View File

@ -1198,6 +1198,13 @@ const migrateConfig = {
addWebSearchProvider(state, 'local-google')
addWebSearchProvider(state, 'local-bing')
addWebSearchProvider(state, 'local-baidu')
if (state.websearch) {
if (isEmpty(state.websearch.subscribeSources)) {
state.websearch.subscribeSources = []
}
}
const qiniuProvider = state.llm.providers.find((provider) => provider.id === 'qiniu')
if (qiniuProvider && isEmpty(qiniuProvider.models)) {
qiniuProvider.models = SYSTEM_MODELS.qiniu

View File

@ -1,5 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { WebSearchProvider } from '@renderer/types'
export interface SubscribeSource {
key: number
url: string
name: string
blacklist?: string[] // 存储从该订阅源获取的黑名单
}
export interface WebSearchState {
// 默认搜索提供商的ID
@ -12,6 +18,8 @@ export interface WebSearchState {
maxResults: number
// 要排除的域名列表
excludeDomains: string[]
// 订阅源列表
subscribeSources: SubscribeSource[]
// 是否启用搜索增强模式
enhanceMode: boolean
// 是否覆盖服务商搜索
@ -55,6 +63,7 @@ const initialState: WebSearchState = {
searchWithTime: true,
maxResults: 5,
excludeDomains: [],
subscribeSources: [],
enhanceMode: false,
overwrite: false
}
@ -89,6 +98,33 @@ const websearchSlice = createSlice({
setExcludeDomains: (state, action: PayloadAction<string[]>) => {
state.excludeDomains = action.payload
},
// 添加订阅源
addSubscribeSource: (state, action: PayloadAction<Omit<SubscribeSource, 'key'>>) => {
state.subscribeSources = state.subscribeSources || []
const newKey =
state.subscribeSources.length > 0 ? Math.max(...state.subscribeSources.map((item) => item.key)) + 1 : 0
state.subscribeSources.push({
key: newKey,
url: action.payload.url,
name: action.payload.name,
blacklist: action.payload.blacklist
})
},
// 删除订阅源
removeSubscribeSource: (state, action: PayloadAction<number>) => {
state.subscribeSources = state.subscribeSources.filter((source) => source.key !== action.payload)
},
// 更新订阅源的黑名单
updateSubscribeBlacklist: (state, action: PayloadAction<{ key: number; blacklist: string[] }>) => {
const source = state.subscribeSources.find((s) => s.key === action.payload.key)
if (source) {
source.blacklist = action.payload.blacklist
}
},
// 更新订阅源列表
setSubscribeSources: (state, action: PayloadAction<SubscribeSource[]>) => {
state.subscribeSources = action.payload
},
setEnhanceMode: (state, action: PayloadAction<boolean>) => {
state.enhanceMode = action.payload
},
@ -115,6 +151,10 @@ export const {
setSearchWithTime,
setExcludeDomains,
setMaxResult,
addSubscribeSource,
removeSubscribeSource,
updateSubscribeBlacklist,
setSubscribeSources,
setEnhanceMode,
setOverwrite,
addWebSearchProvider

View File

@ -1,3 +1,6 @@
import { WebSearchState } from '@renderer/store/websearch'
import { WebSearchResponse } from '@renderer/types'
/*
* MIT License
*
@ -170,3 +173,109 @@ function testPath(pathPattern: string, path: string): boolean {
}
return path.slice(pos).endsWith(rest[rest.length - 1])
}
// 添加新的解析函数
export async function parseSubscribeContent(url: string): Promise<string[]> {
try {
// 获取订阅源内容
const response = await fetch(url)
console.log('response', response)
if (!response.ok) {
throw new Error('Failed to fetch subscribe content')
}
const content = await response.text()
// 按行分割内容
const lines = content.split('\n')
// 过滤出有效的匹配模式
const patterns = lines
.filter((line) => line.trim() !== '' && !line.startsWith('#'))
.map((line) => line.trim())
.filter((pattern) => parseMatchPattern(pattern) !== null)
return patterns
} catch (error) {
console.error('Error parsing subscribe content:', error)
throw error
}
}
export async function filterResultWithBlacklist(
response: WebSearchResponse,
websearch: WebSearchState
): Promise<WebSearchResponse> {
console.log('filterResultWithBlacklist', response)
// 没有结果或者没有黑名单规则时,直接返回原始结果
if (!response.results?.length || (!websearch?.excludeDomains?.length && !websearch?.subscribeSources?.length)) {
return response
}
// 创建匹配模式映射实例
const patternMap = new MatchPatternMap<string>()
// 合并所有黑名单规则
const blacklistPatterns: string[] = [
...websearch.excludeDomains,
...(websearch.subscribeSources?.length
? websearch.subscribeSources.reduce<string[]>((acc, source) => {
return acc.concat(source.blacklist || [])
}, [])
: [])
]
// 正则表达式规则集合
const regexPatterns: RegExp[] = []
// 分类处理黑名单规则
blacklistPatterns.forEach((pattern) => {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
// 处理正则表达式格式
try {
const regexPattern = pattern.slice(1, -1)
regexPatterns.push(new RegExp(regexPattern, 'i'))
} catch (error) {
console.error('Invalid regex pattern:', pattern, error)
}
} else {
// 处理匹配模式格式
try {
patternMap.set(pattern, pattern)
} catch (error) {
console.error('Invalid match pattern:', pattern, error)
}
}
})
// 过滤搜索结果
const filteredResults = response.results.filter((result) => {
try {
const url = new URL(result.url)
// 检查URL是否匹配任何正则表达式规则
const matchesRegex = regexPatterns.some((regex) => regex.test(url.hostname))
if (matchesRegex) {
return false
}
// 检查URL是否匹配任何匹配模式规则
const matchesPattern = patternMap.get(result.url).length > 0
if (matchesPattern) {
return false
}
return true
} catch (error) {
console.error('Error processing URL:', result.url, error)
return true // 如果URL解析失败保留该结果
}
})
console.log('filterResultWithBlacklist filtered results:', filteredResults)
return {
...response,
results: filteredResults
}
}