From c3b5cbee8f3706d4785887b1c554bbf16d0e6245 Mon Sep 17 00:00:00 2001 From: LiuVaayne <10231735+vaayne@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:19:02 +0800 Subject: [PATCH 01/12] Clean up MCPService connections on app quit (#4647) * Clean up MCPService connections on app quit * Improve application shutdown error handling --- src/main/index.ts | 11 +++++++++++ src/main/services/MCPService.ts | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/index.ts b/src/main/index.ts index 816695bb2d..5102225781 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,9 +3,11 @@ 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 mcpService from './services/MCPService' import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient' import { registerShortcuts } from './services/ShortcutService' import { TrayService } from './services/TrayService' @@ -92,6 +94,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. } diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ecfa14a83c..782b9cc9cd 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -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 { @@ -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 From afd1381d7fb1432a2394d10358221e55b452244c Mon Sep 17 00:00:00 2001 From: ousugo Date: Thu, 10 Apr 2025 10:57:11 +0800 Subject: [PATCH 02/12] refactor(CodeBlock): simplify header layout and adjust CollapseIcon position --- .../src/pages/home/Markdown/CodeBlock.tsx | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index e6f6a57cdc..82451e127e 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -129,12 +129,7 @@ const CodeBlock: React.FC = ({ children, className }) => { return match ? ( -
- {codeCollapsible && shouldShowExpandButton && ( - setIsExpanded(!isExpanded)} /> - )} - {'<' + language.toUpperCase() + '>'} -
+ {'<' + language.toUpperCase() + '>'}
= ({ children, className }) => { style={{ bottom: '0.2rem', right: '1rem', height: '27px' }}> {showDownloadButton && } {codeWrappable && setIsUnwrapped(!isUnwrapped)} />} + {codeCollapsible && shouldShowExpandButton && ( + setIsExpanded(!isExpanded)} /> + )} @@ -319,14 +317,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` @@ -384,13 +374,8 @@ const CollapseIconWrapper = styled.div` height: 20px; border-radius: 4px; cursor: pointer; - color: var(--color-text-3); + color: var(--color-text-1); transition: all 0.2s ease; - - &:hover { - background-color: var(--color-background-soft); - color: var(--color-text-1); - } ` const UnwrapButtonWrapper = styled.div` From 2e0251aed7f49243caa6fcdc95eb7f40c0a1f4e1 Mon Sep 17 00:00:00 2001 From: Chen Tao <70054568+eeee0717@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:25:38 +0800 Subject: [PATCH 03/12] feat: support ublacklist subscribe (#2974) * feat: support ublacklist subscribe * Merge branch 'main' into feat-ublacklist * chore * chore --- .../src/hooks/useWebSearchProviders.ts | 33 +++ src/renderer/src/i18n/locales/en-us.json | 11 +- src/renderer/src/i18n/locales/ja-jp.json | 11 +- src/renderer/src/i18n/locales/ru-ru.json | 11 +- src/renderer/src/i18n/locales/zh-cn.json | 13 +- src/renderer/src/i18n/locales/zh-tw.json | 29 ++- .../WebSearchSettings/AddSubscribePopup.tsx | 120 ++++++++++ .../WebSearchSettings/BlacklistSettings.tsx | 213 +++++++++++++++++- .../BaseWebSearchProvider.ts | 4 +- .../WebSearchProvider/ExaProvider.ts | 5 +- .../WebSearchProvider/LocalSearchProvider.ts | 18 +- .../WebSearchProvider/SearxngProvider.ts | 6 +- .../WebSearchProvider/TavilyProvider.ts | 7 +- .../src/providers/WebSearchProvider/index.ts | 9 +- src/renderer/src/services/WebSearchService.ts | 7 +- src/renderer/src/store/websearch.ts | 39 ++++ .../src/utils/blacklistMatchPattern.ts | 108 +++++++++ 17 files changed, 599 insertions(+), 45 deletions(-) create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx diff --git a/src/renderer/src/hooks/useWebSearchProviders.ts b/src/renderer/src/hooks/useWebSearchProviders.ts index dc9c0ee084..b018d2a65c 100644 --- a/src/renderer/src/hooks/useWebSearchProviders.ts +++ b/src/renderer/src/hooks/useWebSearchProviders.ts @@ -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 + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fca3b4e1b5..bcd980da41 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1299,7 +1299,16 @@ "description": "Tavily is a search engine tailored for AI agents, delivering real-time, accurate results, intelligent query suggestions, and in-depth research capabilities.", "title": "Tavily" }, - "title": "Web Search", + "title": "Web Search", + "subscribe": "Subscribe", + "subscribe_tooltip": "Subscribe to the blacklist.", + "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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 8b71523bad..d1a39138b7 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1279,7 +1279,6 @@ "websearch": { "blacklist": "ブラックリスト", "blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません", - "blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "check": "チェック", "check_failed": "検証に失敗しました", "check_success": "検証に成功しました", @@ -1299,6 +1298,16 @@ "title": "Tavily" }, "title": "ウェブ検索", + "blacklist_tooltip": "マッチパターン: *://*.example.com/*\n正規表現: /example\\.(net|org)/", + "subscribe": "購読", + "subscribe_tooltip": "ブラックリストに登録する", + "subscribe_update": "今すぐ更新", + "subscribe_add": "サブスクリプションを追加", + "subscribe_url": "フィードのURL", + "subscribe_name": "代替名", + "subscribe_name.placeholder": "ダウンロードしたフィードに名前がない場合に使用される代替名", + "subscribe_add_success": "フィードの追加が成功しました!", + "subscribe_delete": "フィードの削除", "overwrite": "サービス検索を上書き", "overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する", "apikey": "API キー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b22f009d9f..c07c0c8132 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1279,7 +1279,6 @@ "websearch": { "blacklist": "Черный список", "blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска", - "blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", "check": "проверка", "check_failed": "Проверка не прошла", "check_success": "Проверка успешна", @@ -1299,6 +1298,16 @@ "title": "Tavily" }, "title": "Поиск в Интернете", + "blacklist_tooltip": "Соответствующий шаблон: *://*.example.com/*\nРегулярное выражение: /example\\.(net|org)/", + "subscribe": "Подписка", + "subscribe_tooltip": "Подписаться на черный список", + "subscribe_update": "Обновить сейчас", + "subscribe_add": "Добавить подписку", + "subscribe_url": "Адрес источника подписки", + "subscribe_name": "альтернативное имя", + "subscribe_name.placeholder": "替代名称, используемый, когда загружаемый подписочный источник не имеет названия", + "subscribe_add_success": "Подписка добавлена успешно!", + "subscribe_delete": "Удалить источник подписки", "overwrite": "Переопределить поставщика поиска", "overwrite_tooltip": "Использовать поставщика поиска вместо LLM", "apikey": "Ключ API", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 6a585dd6bc..43359bf989 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1280,7 +1280,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": "验证成功", @@ -1293,6 +1293,15 @@ "search_max_result": "搜索结果个数", "search_provider": "搜索服务商", "search_provider_placeholder": "选择一个搜索服务商", + "subscribe": "订阅", + "subscribe_tooltip": "订阅黑名单列表", + "subscribe_update": "立即更新", + "subscribe_add": "添加订阅", + "subscribe_url": "订阅源地址", + "subscribe_name": "替代名字", + "subscribe_name.placeholder": "当下载的订阅源没有名称时所使用的替代名称", + "subscribe_add_success": "订阅源添加成功!", + "subscribe_delete": "删除订阅源", "search_result_default": "默认", "search_with_time": "搜索包含日期", "tavily": { @@ -1372,3 +1381,5 @@ } } } + + diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b814472ea5..41a25f788a 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1277,20 +1277,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 金鑰", @@ -1298,6 +1288,25 @@ "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_tooltip": "訂閱黑名單列表", + "subscribe_update": "立即更新", + "subscribe_add": "添加訂閱", + "subscribe_url": "訂閱源地址", + "subscribe_name": "替代名稱", + "subscribe_name.placeholder": "當下載的訂閱源沒有名稱時所使用的替代名稱", + "subscribe_add_success": "訂閱源添加成功!", + "subscribe_delete": "刪除訂閱源", "title": "網路搜尋", "overwrite": "覆蓋搜尋服務商", "overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋", diff --git a/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx b/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx new file mode 100644 index 0000000000..9ce19e2eac --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/AddSubscribePopup.tsx @@ -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 = ({ 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['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 ( + +
+ + { + try { + const url = new URL(e.target.value) + form.setFieldValue('name', url.hostname) + } catch (e) { + // URL不合法,忽略 + } + }} + /> + + + + + + + +
+
+ ) +} + +export default class AddSubscribePopup { + static topviewId = 0 + static hide() { + TopView.hide('AddSubscribePopup') + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'AddSubscribePopup' + ) + }) + } +} diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx index 8441c0e336..23d9ffab99 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BlacklistSettings.tsx @@ -1,22 +1,61 @@ +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 = TableProps['rowSelection'] +interface DataType { + key: React.Key + url: string + name: string +} +const columns: TableProps['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([]) + const [dataSource, setDataSource] = useState( + 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 +79,137 @@ const BlacklistSettings: FC = () => { if (hasError) return dispatch(setExcludeDomains(validDomains)) + window.message.info({ + content: t('message.save.success.title'), + duration: 4, + icon: , + key: 'save-blacklist-info' + }) + } + const onSelectChange = (newSelectedRowKeys: React.Key[]) => { + console.log('selectedRowKeys changed: ', newSelectedRowKeys) + setSelectedRowKeys(newSelectedRowKeys) + } + + const rowSelection: TableRowSelection = { + 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 ( @@ -61,6 +231,45 @@ const BlacklistSettings: FC = () => { {t('common.save')} {errFormat && } + + {t('settings.websearch.subscribe')} +
+ + {t('settings.websearch.subscribe_tooltip')} + + + + rowSelection={{ type: 'checkbox', ...rowSelection }} + columns={columns} + dataSource={dataSource} + pagination={{ position: ['none'] }} + /> + + + + +
) diff --git a/src/renderer/src/providers/WebSearchProvider/BaseWebSearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/BaseWebSearchProvider.ts index 8359e037b5..ca7e33fad6 100644 --- a/src/renderer/src/providers/WebSearchProvider/BaseWebSearchProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/BaseWebSearchProvider.ts @@ -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 + abstract search(query: string, websearch: WebSearchState): Promise public getApiKey() { const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || [] diff --git a/src/renderer/src/providers/WebSearchProvider/ExaProvider.ts b/src/renderer/src/providers/WebSearchProvider/ExaProvider.ts index 6f9189075f..79140a026e 100644 --- a/src/renderer/src/providers/WebSearchProvider/ExaProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/ExaProvider.ts @@ -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 { + public async search(query: string, websearch: WebSearchState): Promise { 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 } diff --git a/src/renderer/src/providers/WebSearchProvider/LocalSearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/LocalSearchProvider.ts index 2bb11f7f6f..b5a6e595b9 100644 --- a/src/renderer/src/providers/WebSearchProvider/LocalSearchProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/LocalSearchProvider.ts @@ -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 { + public async search(query: string, websearch: WebSearchState): Promise { 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 diff --git a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts index 0b0a811996..3fd7993b20 100644 --- a/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/SearxngProvider.ts @@ -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 { + public async search(query: string, websearch: WebSearchState): Promise { 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 || '', diff --git a/src/renderer/src/providers/WebSearchProvider/TavilyProvider.ts b/src/renderer/src/providers/WebSearchProvider/TavilyProvider.ts index f45747aa53..c1d7a528de 100644 --- a/src/renderer/src/providers/WebSearchProvider/TavilyProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/TavilyProvider.ts @@ -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 { + public async search(query: string, websearch: WebSearchState): Promise { 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) => ({ diff --git a/src/renderer/src/providers/WebSearchProvider/index.ts b/src/renderer/src/providers/WebSearchProvider/index.ts index 1839960aaf..999e9e0a7c 100644 --- a/src/renderer/src/providers/WebSearchProvider/index.ts +++ b/src/renderer/src/providers/WebSearchProvider/index.ts @@ -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 { - return await this.sdk.search(query, maxResult, excludeDomains) + public async search(query: string, websearch: WebSearchState): Promise { + const result = await this.sdk.search(query, websearch) + const filteredResult = await filterResultWithBlacklist(result, websearch) + + return filteredResult } } diff --git a/src/renderer/src/services/WebSearchService.ts b/src/renderer/src/services/WebSearchService.ts index 3c05a0fe22..883cd2f3fd 100644 --- a/src/renderer/src/services/WebSearchService.ts +++ b/src/renderer/src/services/WebSearchService.ts @@ -97,16 +97,17 @@ class WebSearchService { * @returns 搜索响应 */ public async search(provider: WebSearchProvider, query: string): Promise { - 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'}`) diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 48a06d8477..82e471f9ea 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -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,7 @@ export interface WebSearchState { maxResults: number // 要排除的域名列表 excludeDomains: string[] + subscribeSources: SubscribeSource[] // 是否启用搜索增强模式 enhanceMode: boolean // 是否覆盖服务商搜索 @@ -55,6 +62,7 @@ const initialState: WebSearchState = { searchWithTime: true, maxResults: 5, excludeDomains: [], + subscribeSources: [], enhanceMode: false, overwrite: false } @@ -89,6 +97,33 @@ const websearchSlice = createSlice({ setExcludeDomains: (state, action: PayloadAction) => { state.excludeDomains = action.payload }, + // 添加订阅源 + addSubscribeSource: (state, action: PayloadAction>) => { + 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) => { + 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) => { + state.subscribeSources = action.payload + }, setEnhanceMode: (state, action: PayloadAction) => { state.enhanceMode = action.payload }, @@ -115,6 +150,10 @@ export const { setSearchWithTime, setExcludeDomains, setMaxResult, + addSubscribeSource, + removeSubscribeSource, + updateSubscribeBlacklist, + setSubscribeSources, setEnhanceMode, setOverwrite, addWebSearchProvider diff --git a/src/renderer/src/utils/blacklistMatchPattern.ts b/src/renderer/src/utils/blacklistMatchPattern.ts index 38e3ddfd1d..d9a11d0924 100644 --- a/src/renderer/src/utils/blacklistMatchPattern.ts +++ b/src/renderer/src/utils/blacklistMatchPattern.ts @@ -1,3 +1,6 @@ +import { WebSearchState } from '@renderer/store/websearch' +import { WebSearchResponse } from '@renderer/types' + /* * MIT License * @@ -170,3 +173,108 @@ function testPath(pathPattern: string, path: string): boolean { } return path.slice(pos).endsWith(rest[rest.length - 1]) } + +// 添加新的解析函数 +export async function parseSubscribeContent(url: string): Promise { + 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 { + console.log('filterResultWithBlacklist', response) + // 没有结果或者没有黑名单规则时,直接返回原始结果 + if (!response.results?.length || (!websearch.excludeDomains.length && !websearch.subscribeSources.length)) { + return response + } + + // 创建匹配模式映射实例 + const patternMap = new MatchPatternMap() + + // 合并所有黑名单规则 + const blacklistPatterns: string[] = [ + ...websearch.excludeDomains, + ...(websearch.subscribeSources?.length + ? websearch.subscribeSources.reduce((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 + } +} From 57fa0aad38a95f236bcce92760f535f97c16393c Mon Sep 17 00:00:00 2001 From: Asurada <43401755+ousugo@users.noreply.github.com> Date: Thu, 10 Apr 2025 18:43:20 +0800 Subject: [PATCH 04/12] feat(xAI): Add support for Grok-3-mini and update reasoning effort logic (#4657) * feat(models): add grok-3-mini support and update reasoning effort logic in SettingsTab and OpenAIProvider * feat(settings): update reasoning effort logic for Grok models and enhance localization in multiple languages * fix(models): correct spelling of reasoning in model support functions * fix(settings): correct spelling of reasoning_effort in OpenAIProvider --- src/renderer/src/config/models.ts | 23 +++++++++-- src/renderer/src/i18n/locales/en-us.json | 2 +- src/renderer/src/i18n/locales/ja-jp.json | 2 +- src/renderer/src/i18n/locales/ru-ru.json | 2 +- src/renderer/src/i18n/locales/zh-cn.json | 2 +- src/renderer/src/i18n/locales/zh-tw.json | 2 +- .../src/pages/home/Tabs/SettingsTab.tsx | 39 ++++++++++++++----- .../providers/AiProvider/OpenAIProvider.ts | 7 ++++ 8 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 7291a37b5b..12a88c4e3f 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -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 = @@ -2202,12 +2202,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 } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index bcd980da41..ad886b8ce3 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -59,7 +59,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": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index d1a39138b7..bab44d013a 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index c07c0c8132..6af1f48003 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 43359bf989..8bf5b619d4 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 41a25f788a..fa2fcb0a54 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": { diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index a46be2d0ee..3aad779d14 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -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' @@ -146,7 +146,21 @@ const SettingsTab: FC = (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 +277,7 @@ const SettingsTab: FC = (props) => { )} - {isSupportedResoningEffortModel(assistant?.model || getDefaultModel()) && ( + {isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && ( <> @@ -282,12 +296,19 @@ const SettingsTab: FC = (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 /> diff --git a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts index 1ece14e99b..964694703e 100644 --- a/src/renderer/src/providers/AiProvider/OpenAIProvider.ts +++ b/src/renderer/src/providers/AiProvider/OpenAIProvider.ts @@ -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 From 78a46963272f63ca1004cb599898836464149933 Mon Sep 17 00:00:00 2001 From: ousugo Date: Thu, 10 Apr 2025 21:25:08 +0800 Subject: [PATCH 05/12] feat(models): add grok-3 support to FUNCTION_CALLING_MODELS --- src/renderer/src/config/models.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index 12a88c4e3f..b02c478a00 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -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 = [ From e0a47de8f7f554379fea98ca1ed89009fd22eeb9 Mon Sep 17 00:00:00 2001 From: ousugo Date: Thu, 10 Apr 2025 20:54:01 +0800 Subject: [PATCH 06/12] feat(CodeBlock): add tooltips for collapse and copy buttons --- .../src/pages/home/Markdown/CodeBlock.tsx | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index 82451e127e..0802e25f56 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -184,10 +184,23 @@ const CodeBlock: React.FC = ({ 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 ( - - {expanded ? : } - + + + {expanded ? : } + + ) } @@ -225,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 @@ -234,10 +248,12 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t setTimeout(() => setCopied(false), 2000) } - return copied ? ( - - ) : ( - + return ( + + + {copied ? : } + + ) } @@ -338,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; @@ -374,8 +402,12 @@ const CollapseIconWrapper = styled.div` height: 20px; border-radius: 4px; cursor: pointer; - color: var(--color-text-1); + color: var(--color-text-3); transition: all 0.2s ease; + + &:hover { + color: var(--color-text-1); + } ` const UnwrapButtonWrapper = styled.div` From a9eb235c431529246e0718d2ecb5204ef36e1233 Mon Sep 17 00:00:00 2001 From: ousugo Date: Thu, 10 Apr 2025 21:15:28 +0800 Subject: [PATCH 07/12] refactor(SettingsTab): update reasoning effort change handler to use useCallback for performance optimization --- src/renderer/src/pages/home/Tabs/SettingsTab.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx index 3aad779d14..28269161f3 100644 --- a/src/renderer/src/pages/home/Tabs/SettingsTab.tsx +++ b/src/renderer/src/pages/home/Tabs/SettingsTab.tsx @@ -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) => { } } - const onReasoningEffortChange = (value) => { - updateAssistantSettings({ reasoning_effort: value }) - } + const onReasoningEffortChange = useCallback( + (value?: 'low' | 'medium' | 'high') => { + updateAssistantSettings({ reasoning_effort: value }) + }, + [updateAssistantSettings] + ) const onReset = () => { setTemperature(DEFAULT_TEMPERATURE) From 978c3ea3cf52ec4871f98f9c9e6bf5e33b18e9b4 Mon Sep 17 00:00:00 2001 From: kangfenmao Date: Thu, 10 Apr 2025 22:12:27 +0800 Subject: [PATCH 08/12] feat(i18n): update subscription terminology in multiple languages for consistency --- src/renderer/src/i18n/locales/en-us.json | 5 ++-- src/renderer/src/i18n/locales/ja-jp.json | 3 +-- src/renderer/src/i18n/locales/ru-ru.json | 3 +-- src/renderer/src/i18n/locales/zh-cn.json | 5 +--- src/renderer/src/i18n/locales/zh-tw.json | 3 +-- .../WebSearchSettings/BasicSettings.tsx | 4 +-- .../WebSearchSettings/BlacklistSettings.tsx | 27 ++++++++++--------- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index ad886b8ce3..5a27f32d9e 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1299,9 +1299,8 @@ "description": "Tavily is a search engine tailored for AI agents, delivering real-time, accurate results, intelligent query suggestions, and in-depth research capabilities.", "title": "Tavily" }, - "title": "Web Search", - "subscribe": "Subscribe", - "subscribe_tooltip": "Subscribe to the blacklist.", + "title": "Web Search", + "subscribe": "Blacklist Subscription", "subscribe_update": "Update now", "subscribe_add": "Add Subscription", "subscribe_url": "Subscription feed address", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index bab44d013a..03d303165f 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1299,8 +1299,7 @@ }, "title": "ウェブ検索", "blacklist_tooltip": "マッチパターン: *://*.example.com/*\n正規表現: /example\\.(net|org)/", - "subscribe": "購読", - "subscribe_tooltip": "ブラックリストに登録する", + "subscribe": "ブラックリスト購読", "subscribe_update": "今すぐ更新", "subscribe_add": "サブスクリプションを追加", "subscribe_url": "フィードのURL", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 6af1f48003..97a2f8e831 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1299,8 +1299,7 @@ }, "title": "Поиск в Интернете", "blacklist_tooltip": "Соответствующий шаблон: *://*.example.com/*\nРегулярное выражение: /example\\.(net|org)/", - "subscribe": "Подписка", - "subscribe_tooltip": "Подписаться на черный список", + "subscribe": "Черный список подписки", "subscribe_update": "Обновить сейчас", "subscribe_add": "Добавить подписку", "subscribe_url": "Адрес источника подписки", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8bf5b619d4..c7074215a1 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1293,8 +1293,7 @@ "search_max_result": "搜索结果个数", "search_provider": "搜索服务商", "search_provider_placeholder": "选择一个搜索服务商", - "subscribe": "订阅", - "subscribe_tooltip": "订阅黑名单列表", + "subscribe": "黑名单订阅", "subscribe_update": "立即更新", "subscribe_add": "添加订阅", "subscribe_url": "订阅源地址", @@ -1381,5 +1380,3 @@ } } } - - diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fa2fcb0a54..1f5d71c54c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1298,8 +1298,7 @@ "no_provider_selected": "請選擇搜索服務商後再檢查", "check_failed": "驗證失敗", "blacklist_tooltip": "匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/", - "subscribe": "訂閱", - "subscribe_tooltip": "訂閱黑名單列表", + "subscribe": "黑名單訂閱", "subscribe_update": "立即更新", "subscribe_add": "添加訂閱", "subscribe_url": "訂閱源地址", diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx index d3e15c5897..937f84c1e6 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx @@ -46,8 +46,8 @@ const BasicSettings: FC = () => { dispatch(setEnhanceMode(checked))} /> - - + + {t('settings.websearch.search_max_result')} ['columns'] = [ { title: t('common.name'), dataIndex: 'name', key: 'name' }, { @@ -26,6 +27,7 @@ const columns: TableProps['columns'] = [ key: 'url' } ] + const BlacklistSettings: FC = () => { const [errFormat, setErrFormat] = useState(false) const [blacklistInput, setBlacklistInput] = useState('') @@ -231,26 +233,27 @@ const BlacklistSettings: FC = () => { {t('common.save')} {errFormat && } + + + + {t('settings.websearch.subscribe')} + + - {t('settings.websearch.subscribe')}
- - {t('settings.websearch.subscribe_tooltip')} - - rowSelection={{ type: 'checkbox', ...rowSelection }} columns={columns} dataSource={dataSource} pagination={{ position: ['none'] }} /> - +