mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 19:30:17 +08:00
Merge remote-tracking branch 'origin/main' into 1600822305-patch-2
This commit is contained in:
commit
fa4dfecfe1
@ -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 依赖工具下载速度问题
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 キー",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": "強制使用搜尋服務商而不是大語言模型進行搜尋",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
/>
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()) || []
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 || '',
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'}`)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user