mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-03 11:19:10 +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
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
知识库和服务商界面更新
|
增加对 grok-3 和 Grok-3-mini 的支持
|
||||||
增加 Dangbei 小程序
|
助手支持使用拼音排序
|
||||||
可以强制使用搜索引擎覆盖模型自带搜索能力
|
网络搜索增加 Baidu, Google, Bing 支持(免费使用)
|
||||||
修复部分公式无法正常渲染问题
|
网络搜索增加 uBlacklist 订阅
|
||||||
|
快速面板 (QuickPanel) 进行性能优化
|
||||||
|
解决 mcp 依赖工具下载速度问题
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@ -6,8 +6,8 @@ const AdmZip = require('adm-zip')
|
|||||||
const { downloadWithRedirects } = require('./download')
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
// Base URL for downloading bun binaries
|
// Base URL for downloading bun binaries
|
||||||
const BUN_RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download'
|
const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download'
|
||||||
const DEFAULT_BUN_VERSION = '1.2.5' // Default fallback version
|
const DEFAULT_BUN_VERSION = '1.2.9' // Default fallback version
|
||||||
|
|
||||||
// Mapping of platform+arch to binary package name
|
// Mapping of platform+arch to binary package name
|
||||||
const BUN_PACKAGES = {
|
const BUN_PACKAGES = {
|
||||||
|
|||||||
@ -7,8 +7,8 @@ const AdmZip = require('adm-zip')
|
|||||||
const { downloadWithRedirects } = require('./download')
|
const { downloadWithRedirects } = require('./download')
|
||||||
|
|
||||||
// Base URL for downloading uv binaries
|
// Base URL for downloading uv binaries
|
||||||
const UV_RELEASE_BASE_URL = 'https://github.com/astral-sh/uv/releases/download'
|
const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download'
|
||||||
const DEFAULT_UV_VERSION = '0.6.6'
|
const DEFAULT_UV_VERSION = '0.6.14'
|
||||||
|
|
||||||
// Mapping of platform+arch to binary package name
|
// Mapping of platform+arch to binary package name
|
||||||
const UV_PACKAGES = {
|
const UV_PACKAGES = {
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import { replaceDevtoolsFont } from '@main/utils/windowUtil'
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { app, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
|
import { registerMsTTSIpcHandlers } from './services/MsTTSIpcHandler'
|
||||||
|
import mcpService from './services/MCPService'
|
||||||
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
||||||
import { registerShortcuts } from './services/ShortcutService'
|
import { registerShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
@ -96,6 +98,15 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
app.isQuitting = true
|
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
|
// 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.
|
// 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.removeServer = this.removeServer.bind(this)
|
||||||
this.restartServer = this.restartServer.bind(this)
|
this.restartServer = this.restartServer.bind(this)
|
||||||
this.stopServer = this.stopServer.bind(this)
|
this.stopServer = this.stopServer.bind(this)
|
||||||
|
this.cleanup = this.cleanup.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async initClient(server: MCPServer): Promise<Client> {
|
async initClient(server: MCPServer): Promise<Client> {
|
||||||
@ -205,6 +206,16 @@ class McpService {
|
|||||||
await this.initClient(server)
|
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) {
|
async listTools(_: Electron.IpcMainInvokeEvent, server: MCPServer) {
|
||||||
const client = await this.initClient(server)
|
const client = await this.initClient(server)
|
||||||
const serverKey = this.getServerKey(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
|
// Reasoning models
|
||||||
export const REASONING_REGEX =
|
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
|
// Embedding models
|
||||||
export const EMBEDDING_REGEX =
|
export const EMBEDDING_REGEX =
|
||||||
@ -210,7 +210,8 @@ export const FUNCTION_CALLING_MODELS = [
|
|||||||
'deepseek',
|
'deepseek',
|
||||||
'glm-4(?:-[\\w-]+)?',
|
'glm-4(?:-[\\w-]+)?',
|
||||||
'learnlm(?:-[\\w-]+)?',
|
'learnlm(?:-[\\w-]+)?',
|
||||||
'gemini(?:-[\\w-]+)?' // 提前排除了gemini的嵌入模型
|
'gemini(?:-[\\w-]+)?', // 提前排除了gemini的嵌入模型
|
||||||
|
'grok-3(?:-[\\w-]+)?'
|
||||||
]
|
]
|
||||||
|
|
||||||
const FUNCTION_CALLING_EXCLUDED_MODELS = [
|
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')
|
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) {
|
if (!model) {
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
|
addSubscribeSource as _addSubscribeSource,
|
||||||
|
removeSubscribeSource as _removeSubscribeSource,
|
||||||
setDefaultProvider as _setDefaultProvider,
|
setDefaultProvider as _setDefaultProvider,
|
||||||
|
setSubscribeSources as _setSubscribeSources,
|
||||||
|
updateSubscribeBlacklist as _updateSubscribeBlacklist,
|
||||||
updateWebSearchProvider,
|
updateWebSearchProvider,
|
||||||
updateWebSearchProviders
|
updateWebSearchProviders
|
||||||
} from '@renderer/store/websearch'
|
} from '@renderer/store/websearch'
|
||||||
@ -57,3 +61,32 @@ export const useWebSearchProvider = (id: string) => {
|
|||||||
|
|
||||||
return { provider, updateProvider }
|
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.low": "low",
|
||||||
"settings.reasoning_effort.medium": "medium",
|
"settings.reasoning_effort.medium": "medium",
|
||||||
"settings.reasoning_effort.off": "off",
|
"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"
|
"settings.more": "Assistant Settings"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -1324,6 +1324,14 @@
|
|||||||
"title": "Tavily"
|
"title": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "Web Search",
|
"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": "Override search service",
|
||||||
"overwrite_tooltip": "Force use search service instead of LLM",
|
"overwrite_tooltip": "Force use search service instead of LLM",
|
||||||
"apikey": "API key",
|
"apikey": "API key",
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
"settings.reasoning_effort.low": "短い",
|
"settings.reasoning_effort.low": "短い",
|
||||||
"settings.reasoning_effort.medium": "中程度",
|
"settings.reasoning_effort.medium": "中程度",
|
||||||
"settings.reasoning_effort.off": "オフ",
|
"settings.reasoning_effort.off": "オフ",
|
||||||
"settings.reasoning_effort.tip": "OpenAIのoシリーズとAnthropicの推論モデルのみサポートしています",
|
"settings.reasoning_effort.tip": "OpenAI o-series、Anthropic、および Grok の推論モデルのみサポート",
|
||||||
"settings.more": "アシスタント設定"
|
"settings.more": "アシスタント設定"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -1286,7 +1286,6 @@
|
|||||||
"websearch": {
|
"websearch": {
|
||||||
"blacklist": "ブラックリスト",
|
"blacklist": "ブラックリスト",
|
||||||
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
|
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
|
||||||
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
|
||||||
"check": "チェック",
|
"check": "チェック",
|
||||||
"check_failed": "検証に失敗しました",
|
"check_failed": "検証に失敗しました",
|
||||||
"check_success": "検証に成功しました",
|
"check_success": "検証に成功しました",
|
||||||
@ -1306,6 +1305,15 @@
|
|||||||
"title": "Tavily"
|
"title": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "ウェブ検索",
|
"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": "サービス検索を上書き",
|
||||||
"overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する",
|
"overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する",
|
||||||
"apikey": "API キー",
|
"apikey": "API キー",
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
"settings.reasoning_effort.low": "Короткая",
|
"settings.reasoning_effort.low": "Короткая",
|
||||||
"settings.reasoning_effort.medium": "Средняя",
|
"settings.reasoning_effort.medium": "Средняя",
|
||||||
"settings.reasoning_effort.off": "Выключено",
|
"settings.reasoning_effort.off": "Выключено",
|
||||||
"settings.reasoning_effort.tip": "Поддерживается только моделями с рассуждением OpenAI o-series и Anthropic",
|
"settings.reasoning_effort.tip": "Поддерживается только моделями рассуждений OpenAI o-series, Anthropic и Grok",
|
||||||
"settings.more": "Настройки ассистента"
|
"settings.more": "Настройки ассистента"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -1286,7 +1286,6 @@
|
|||||||
"websearch": {
|
"websearch": {
|
||||||
"blacklist": "Черный список",
|
"blacklist": "Черный список",
|
||||||
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
|
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
|
||||||
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
|
||||||
"check": "проверка",
|
"check": "проверка",
|
||||||
"check_failed": "Проверка не прошла",
|
"check_failed": "Проверка не прошла",
|
||||||
"check_success": "Проверка успешна",
|
"check_success": "Проверка успешна",
|
||||||
@ -1306,6 +1305,15 @@
|
|||||||
"title": "Tavily"
|
"title": "Tavily"
|
||||||
},
|
},
|
||||||
"title": "Поиск в Интернете",
|
"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": "Переопределить поставщика поиска",
|
||||||
"overwrite_tooltip": "Использовать поставщика поиска вместо LLM",
|
"overwrite_tooltip": "Использовать поставщика поиска вместо LLM",
|
||||||
"apikey": "Ключ API",
|
"apikey": "Ключ API",
|
||||||
|
|||||||
@ -76,7 +76,7 @@
|
|||||||
"settings.reasoning_effort.low": "短",
|
"settings.reasoning_effort.low": "短",
|
||||||
"settings.reasoning_effort.medium": "中",
|
"settings.reasoning_effort.medium": "中",
|
||||||
"settings.reasoning_effort.off": "关",
|
"settings.reasoning_effort.off": "关",
|
||||||
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series 和 Anthropic 推理模型",
|
"settings.reasoning_effort.tip": "仅支持 OpenAI o-series、Anthropic、Grok 推理模型",
|
||||||
"settings.more": "助手设置"
|
"settings.more": "助手设置"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -1304,7 +1304,7 @@
|
|||||||
"websearch": {
|
"websearch": {
|
||||||
"blacklist": "黑名单",
|
"blacklist": "黑名单",
|
||||||
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
|
"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": "检查",
|
||||||
"check_failed": "验证失败",
|
"check_failed": "验证失败",
|
||||||
"check_success": "验证成功",
|
"check_success": "验证成功",
|
||||||
@ -1317,6 +1317,14 @@
|
|||||||
"search_max_result": "搜索结果个数",
|
"search_max_result": "搜索结果个数",
|
||||||
"search_provider": "搜索服务商",
|
"search_provider": "搜索服务商",
|
||||||
"search_provider_placeholder": "选择一个搜索服务商",
|
"search_provider_placeholder": "选择一个搜索服务商",
|
||||||
|
"subscribe": "黑名单订阅",
|
||||||
|
"subscribe_update": "立即更新",
|
||||||
|
"subscribe_add": "添加订阅",
|
||||||
|
"subscribe_url": "订阅源地址",
|
||||||
|
"subscribe_name": "替代名字",
|
||||||
|
"subscribe_name.placeholder": "当下载的订阅源没有名称时所使用的替代名称",
|
||||||
|
"subscribe_add_success": "订阅源添加成功!",
|
||||||
|
"subscribe_delete": "删除订阅源",
|
||||||
"search_result_default": "默认",
|
"search_result_default": "默认",
|
||||||
"search_with_time": "搜索包含日期",
|
"search_with_time": "搜索包含日期",
|
||||||
"tavily": {
|
"tavily": {
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
"settings.reasoning_effort.low": "短",
|
"settings.reasoning_effort.low": "短",
|
||||||
"settings.reasoning_effort.medium": "中",
|
"settings.reasoning_effort.medium": "中",
|
||||||
"settings.reasoning_effort.off": "關",
|
"settings.reasoning_effort.off": "關",
|
||||||
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series 和 Anthropic 推理模型",
|
"settings.reasoning_effort.tip": "僅支援 OpenAI o-series、Anthropic 和 Grok 推理模型",
|
||||||
"settings.more": "助手設定"
|
"settings.more": "助手設定"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
@ -1284,20 +1284,10 @@
|
|||||||
"tray.show": "顯示系统匣圖示",
|
"tray.show": "顯示系统匣圖示",
|
||||||
"tray.title": "系统匣",
|
"tray.title": "系统匣",
|
||||||
"websearch": {
|
"websearch": {
|
||||||
"blacklist": "黑名單",
|
|
||||||
"blacklist_description": "以下網站不會出現在搜尋結果中",
|
|
||||||
"blacklist_tooltip": "請使用以下格式 (換行符號分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
|
||||||
"check": "檢查",
|
|
||||||
"check_failed": "驗證失敗",
|
|
||||||
"check_success": "驗證成功",
|
"check_success": "驗證成功",
|
||||||
"enhance_mode": "搜索增強模式",
|
"enhance_mode": "搜索增強模式",
|
||||||
"enhance_mode_tooltip": "使用預設模型提取關鍵詞後搜索",
|
"enhance_mode_tooltip": "使用預設模型提取關鍵詞後搜索",
|
||||||
"get_api_key": "點選這裡取得金鑰",
|
"get_api_key": "點選這裡取得金鑰",
|
||||||
"no_provider_selected": "請選擇搜尋服務商後再檢查",
|
|
||||||
"search_max_result": "搜尋結果個數",
|
|
||||||
"search_provider": "搜尋服務商",
|
|
||||||
"search_provider_placeholder": "選擇一個搜尋服務商",
|
|
||||||
"search_result_default": "預設",
|
|
||||||
"search_with_time": "搜尋包含日期",
|
"search_with_time": "搜尋包含日期",
|
||||||
"tavily": {
|
"tavily": {
|
||||||
"api_key": "Tavily API 金鑰",
|
"api_key": "Tavily API 金鑰",
|
||||||
@ -1305,6 +1295,24 @@
|
|||||||
"description": "Tavily 是一個為 AI 代理量身訂製的搜尋引擎,提供即時、準確的結果、智慧查詢建議和深入的研究能力",
|
"description": "Tavily 是一個為 AI 代理量身訂製的搜尋引擎,提供即時、準確的結果、智慧查詢建議和深入的研究能力",
|
||||||
"title": "Tavily"
|
"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": "網路搜尋",
|
"title": "網路搜尋",
|
||||||
"overwrite": "覆蓋搜尋服務商",
|
"overwrite": "覆蓋搜尋服務商",
|
||||||
"overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",
|
"overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋",
|
||||||
|
|||||||
@ -129,12 +129,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
return match ? (
|
return match ? (
|
||||||
<CodeBlockWrapper className="code-block">
|
<CodeBlockWrapper className="code-block">
|
||||||
<CodeHeader>
|
<CodeHeader>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<CodeLanguage>{'<' + language.toUpperCase() + '>'}</CodeLanguage>
|
||||||
{codeCollapsible && shouldShowExpandButton && (
|
|
||||||
<CollapseIcon expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)} />
|
|
||||||
)}
|
|
||||||
<CodeLanguage>{'<' + language.toUpperCase() + '>'}</CodeLanguage>
|
|
||||||
</div>
|
|
||||||
</CodeHeader>
|
</CodeHeader>
|
||||||
<StickyWrapper>
|
<StickyWrapper>
|
||||||
<HStack
|
<HStack
|
||||||
@ -144,6 +139,9 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
style={{ bottom: '0.2rem', right: '1rem', height: '27px' }}>
|
style={{ bottom: '0.2rem', right: '1rem', height: '27px' }}>
|
||||||
{showDownloadButton && <DownloadButton language={language} data={children} />}
|
{showDownloadButton && <DownloadButton language={language} data={children} />}
|
||||||
{codeWrappable && <UnwrapButton unwrapped={isUnwrapped} onClick={() => setIsUnwrapped(!isUnwrapped)} />}
|
{codeWrappable && <UnwrapButton unwrapped={isUnwrapped} onClick={() => setIsUnwrapped(!isUnwrapped)} />}
|
||||||
|
{codeCollapsible && shouldShowExpandButton && (
|
||||||
|
<CollapseIcon expanded={isExpanded} onClick={() => setIsExpanded(!isExpanded)} />
|
||||||
|
)}
|
||||||
<CopyButton text={children} />
|
<CopyButton text={children} />
|
||||||
</HStack>
|
</HStack>
|
||||||
</StickyWrapper>
|
</StickyWrapper>
|
||||||
@ -186,10 +184,23 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
|
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [tooltipVisible, setTooltipVisible] = useState(false)
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setTooltipVisible(false)
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapseIconWrapper onClick={onClick}>
|
<Tooltip
|
||||||
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
|
title={expanded ? t('code_block.collapse') : t('code_block.expand')}
|
||||||
</CollapseIconWrapper>
|
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 CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const copy = t('common.copy')
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
if (!text) return
|
if (!text) return
|
||||||
@ -236,10 +248,12 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
|||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
return copied ? (
|
return (
|
||||||
<CheckOutlined style={{ color: 'var(--color-primary)', ...style }} />
|
<Tooltip title={copy}>
|
||||||
) : (
|
<CopyButtonWrapper onClick={onCopy} style={style}>
|
||||||
<CopyIcon className="copy" style={style} onClick={onCopy} />
|
{copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon className="copy" />}
|
||||||
|
</CopyButtonWrapper>
|
||||||
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,14 +333,6 @@ const CodeHeader = styled.div`
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-top-right-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`
|
const CodeLanguage = styled.div`
|
||||||
@ -348,7 +354,19 @@ const CodeFooter = styled.div`
|
|||||||
color: var(--color-text-1);
|
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`
|
const ExpandButtonWrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -388,7 +406,6 @@ const CollapseIconWrapper = styled.div`
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
isMac,
|
isMac,
|
||||||
isWindows
|
isWindows
|
||||||
} from '@renderer/config/constant'
|
} from '@renderer/config/constant'
|
||||||
import { isSupportedResoningEffortModel } from '@renderer/config/models'
|
import { isGrokReasoningModel, isSupportedReasoningEffortModel } from '@renderer/config/models'
|
||||||
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
import { codeThemes } from '@renderer/context/SyntaxHighlighterProvider'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -44,7 +44,7 @@ import {
|
|||||||
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||||
import { modalConfirm } from '@renderer/utils'
|
import { modalConfirm } from '@renderer/utils'
|
||||||
import { Button, Col, InputNumber, Row, Segmented, Select, Slider, Switch, Tooltip } from 'antd'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -115,9 +115,12 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReasoningEffortChange = (value) => {
|
const onReasoningEffortChange = useCallback(
|
||||||
updateAssistantSettings({ reasoning_effort: value })
|
(value?: 'low' | 'medium' | 'high') => {
|
||||||
}
|
updateAssistantSettings({ reasoning_effort: value })
|
||||||
|
},
|
||||||
|
[updateAssistantSettings]
|
||||||
|
)
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
setTemperature(DEFAULT_TEMPERATURE)
|
setTemperature(DEFAULT_TEMPERATURE)
|
||||||
@ -146,7 +149,21 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
||||||
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
setStreamOutput(assistant?.settings?.streamOutput ?? true)
|
||||||
setReasoningEffort(assistant?.settings?.reasoning_effort)
|
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) => {
|
const formatSliderTooltip = (value?: number) => {
|
||||||
if (value === undefined) return ''
|
if (value === undefined) return ''
|
||||||
@ -263,7 +280,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{isSupportedResoningEffortModel(assistant?.model || getDefaultModel()) && (
|
{isSupportedReasoningEffortModel(assistant?.model || getDefaultModel()) && (
|
||||||
<>
|
<>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<Row align="middle">
|
<Row align="middle">
|
||||||
@ -282,12 +299,19 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
setReasoningEffort(typedValue)
|
setReasoningEffort(typedValue)
|
||||||
onReasoningEffortChange(typedValue)
|
onReasoningEffortChange(typedValue)
|
||||||
}}
|
}}
|
||||||
options={[
|
options={
|
||||||
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
|
isGrokReasoningModel(assistant?.model || getDefaultModel())
|
||||||
{ value: 'medium', label: t('assistants.settings.reasoning_effort.medium') },
|
? [
|
||||||
{ value: 'high', label: t('assistants.settings.reasoning_effort.high') },
|
{ value: 'low', label: t('assistants.settings.reasoning_effort.low') },
|
||||||
{ value: 'off', label: t('assistants.settings.reasoning_effort.off') }
|
{ 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"
|
name="group"
|
||||||
block
|
block
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -43,8 +43,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
|
window.message.error({ content: `${t('settings.mcp.installError')}: ${error.message}`, key: 'mcp-install-error' })
|
||||||
setIsInstallingUv(false)
|
setIsInstallingUv(false)
|
||||||
checkBinaries()
|
|
||||||
}
|
}
|
||||||
|
setTimeout(checkBinaries, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const installBun = async () => {
|
const installBun = async () => {
|
||||||
@ -59,8 +59,8 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
|
|||||||
key: 'mcp-install-error'
|
key: 'mcp-install-error'
|
||||||
})
|
})
|
||||||
setIsInstallingBun(false)
|
setIsInstallingBun(false)
|
||||||
checkBinaries()
|
|
||||||
}
|
}
|
||||||
|
setTimeout(checkBinaries, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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>
|
</SettingRowTitle>
|
||||||
<Switch checked={enhanceMode} onChange={(checked) => dispatch(setEnhanceMode(checked))} />
|
<Switch checked={enhanceMode} onChange={(checked) => dispatch(setEnhanceMode(checked))} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
|
<SettingDivider style={{ marginTop: 15, marginBottom: 10 }} />
|
||||||
<SettingRow>
|
<SettingRow style={{ height: 40 }}>
|
||||||
<SettingRowTitle>{t('settings.websearch.search_max_result')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.websearch.search_max_result')}</SettingRowTitle>
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={maxResults}
|
defaultValue={maxResults}
|
||||||
|
|||||||
@ -1,22 +1,63 @@
|
|||||||
|
import { CheckOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useBlacklist } from '@renderer/hooks/useWebSearchProviders'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setExcludeDomains } from '@renderer/store/websearch'
|
import { setExcludeDomains } from '@renderer/store/websearch'
|
||||||
import { parseMatchPattern } from '@renderer/utils/blacklistMatchPattern'
|
import { parseMatchPattern, parseSubscribeContent } from '@renderer/utils/blacklistMatchPattern'
|
||||||
import { Alert, Button } from 'antd'
|
import { Alert, Button, Table, TableProps } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
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 BlacklistSettings: FC = () => {
|
||||||
const [errFormat, setErrFormat] = useState(false)
|
const [errFormat, setErrFormat] = useState(false)
|
||||||
const [blacklistInput, setBlacklistInput] = useState('')
|
const [blacklistInput, setBlacklistInput] = useState('')
|
||||||
const excludeDomains = useAppSelector((state) => state.websearch.excludeDomains)
|
const excludeDomains = useAppSelector((state) => state.websearch.excludeDomains)
|
||||||
|
const { websearch, setSubscribeSources, addSubscribeSource } = useBlacklist()
|
||||||
const { theme } = useTheme()
|
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()
|
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(() => {
|
useEffect(() => {
|
||||||
if (excludeDomains) {
|
if (excludeDomains) {
|
||||||
setBlacklistInput(excludeDomains.join('\n'))
|
setBlacklistInput(excludeDomains.join('\n'))
|
||||||
@ -40,6 +81,137 @@ const BlacklistSettings: FC = () => {
|
|||||||
if (hasError) return
|
if (hasError) return
|
||||||
|
|
||||||
dispatch(setExcludeDomains(validDomains))
|
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 (
|
return (
|
||||||
@ -62,6 +234,46 @@ const BlacklistSettings: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
{errFormat && <Alert message={t('settings.websearch.blacklist_tooltip')} type="error" />}
|
{errFormat && <Alert message={t('settings.websearch.blacklist_tooltip')} type="error" />}
|
||||||
</SettingGroup>
|
</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 { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||||
import {
|
import {
|
||||||
getOpenAIWebSearchParams,
|
getOpenAIWebSearchParams,
|
||||||
|
isGrokReasoningModel,
|
||||||
isHunyuanSearchModel,
|
isHunyuanSearchModel,
|
||||||
isOpenAIoSeries,
|
isOpenAIoSeries,
|
||||||
isOpenAIWebSearch,
|
isOpenAIWebSearch,
|
||||||
@ -243,6 +244,12 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGrokReasoningModel(model)) {
|
||||||
|
return {
|
||||||
|
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isOpenAIoSeries(model)) {
|
if (isOpenAIoSeries(model)) {
|
||||||
return {
|
return {
|
||||||
reasoning_effort: assistant?.settings?.reasoning_effort
|
reasoning_effort: assistant?.settings?.reasoning_effort
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
|
|
||||||
export default abstract class BaseWebSearchProvider {
|
export default abstract class BaseWebSearchProvider {
|
||||||
@ -9,8 +10,7 @@ export default abstract class BaseWebSearchProvider {
|
|||||||
this.provider = provider
|
this.provider = provider
|
||||||
this.apiKey = this.getApiKey()
|
this.apiKey = this.getApiKey()
|
||||||
}
|
}
|
||||||
|
abstract search(query: string, websearch: WebSearchState): Promise<WebSearchResponse>
|
||||||
abstract search(query: string, maxResult: number, excludeDomains: string[]): Promise<WebSearchResponse>
|
|
||||||
|
|
||||||
public getApiKey() {
|
public getApiKey() {
|
||||||
const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || []
|
const keys = this.provider.apiKey?.split(',').map((key) => key.trim()) || []
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ExaClient } from '@agentic/exa'
|
import { ExaClient } from '@agentic/exa'
|
||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
|
|
||||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||||
@ -14,7 +15,7 @@ export default class ExaProvider extends BaseWebSearchProvider {
|
|||||||
this.exa = new ExaClient({ apiKey: this.apiKey })
|
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 {
|
try {
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
throw new Error('Search query cannot be empty')
|
throw new Error('Search query cannot be empty')
|
||||||
@ -22,7 +23,7 @@ export default class ExaProvider extends BaseWebSearchProvider {
|
|||||||
|
|
||||||
const response = await this.exa.search({
|
const response = await this.exa.search({
|
||||||
query,
|
query,
|
||||||
numResults: Math.max(1, maxResults),
|
numResults: Math.max(1, websearch.maxResults),
|
||||||
contents: {
|
contents: {
|
||||||
text: true
|
text: true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Readability } from '@mozilla/readability'
|
import { Readability } from '@mozilla/readability'
|
||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse, WebSearchResult } from '@renderer/types'
|
||||||
import TurndownService from 'turndown'
|
import TurndownService from 'turndown'
|
||||||
|
|
||||||
@ -22,11 +23,7 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
|
|||||||
super(provider)
|
super(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(
|
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
|
||||||
query: string,
|
|
||||||
maxResults: number = 15,
|
|
||||||
excludeDomains: string[] = []
|
|
||||||
): Promise<WebSearchResponse> {
|
|
||||||
const uid = nanoid()
|
const uid = nanoid()
|
||||||
try {
|
try {
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
@ -41,16 +38,11 @@ export default class LocalSearchProvider extends BaseWebSearchProvider {
|
|||||||
const content = await window.api.searchService.openUrlInSearchWindow(uid, url)
|
const content = await window.api.searchService.openUrlInSearchWindow(uid, url)
|
||||||
|
|
||||||
// Parse the content to extract URLs and metadata
|
// Parse the content to extract URLs and metadata
|
||||||
const searchItems = this.parseValidUrls(content).slice(0, maxResults)
|
const searchItems = this.parseValidUrls(content).slice(0, websearch.maxResults)
|
||||||
console.log('Total search items:', searchItems)
|
|
||||||
|
|
||||||
const validItems = searchItems
|
const validItems = searchItems
|
||||||
.filter(
|
.filter((item) => item.url.startsWith('http') || item.url.startsWith('https'))
|
||||||
(item) =>
|
.slice(0, websearch.maxResults)
|
||||||
(item.url.startsWith('http') || item.url.startsWith('https')) &&
|
|
||||||
excludeDomains.includes(new URL(item.url).host) === false
|
|
||||||
)
|
|
||||||
.slice(0, maxResults)
|
|
||||||
// console.log('Valid search items:', validItems)
|
// console.log('Valid search items:', validItems)
|
||||||
|
|
||||||
// Fetch content for each URL concurrently
|
// Fetch content for each URL concurrently
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { SearxngClient } from '@agentic/searxng'
|
import { SearxngClient } from '@agentic/searxng'
|
||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
import axios from 'axios'
|
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 {
|
try {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
throw new Error('Search query cannot be empty')
|
throw new Error('Search query cannot be empty')
|
||||||
@ -88,10 +89,9 @@ export default class SearxngProvider extends BaseWebSearchProvider {
|
|||||||
if (!result || !Array.isArray(result.results)) {
|
if (!result || !Array.isArray(result.results)) {
|
||||||
throw new Error('Invalid search results from SearxNG')
|
throw new Error('Invalid search results from SearxNG')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: result.query,
|
query: result.query,
|
||||||
results: result.results.slice(0, maxResults).map((result) => {
|
results: result.results.slice(0, websearch.maxResults).map((result) => {
|
||||||
return {
|
return {
|
||||||
title: result.title || 'No title',
|
title: result.title || 'No title',
|
||||||
content: result.content || '',
|
content: result.content || '',
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { TavilyClient } from '@agentic/tavily'
|
import { TavilyClient } from '@agentic/tavily'
|
||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
|
|
||||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||||
@ -14,7 +15,7 @@ export default class TavilyProvider extends BaseWebSearchProvider {
|
|||||||
this.tvly = new TavilyClient({ apiKey: this.apiKey })
|
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 {
|
try {
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
throw new Error('Search query cannot be empty')
|
throw new Error('Search query cannot be empty')
|
||||||
@ -22,10 +23,8 @@ export default class TavilyProvider extends BaseWebSearchProvider {
|
|||||||
|
|
||||||
const result = await this.tvly.search({
|
const result = await this.tvly.search({
|
||||||
query,
|
query,
|
||||||
max_results: Math.max(1, maxResults),
|
max_results: Math.max(1, websearch.maxResults)
|
||||||
exclude_domains: excludeDomains || []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: result.query,
|
query: result.query,
|
||||||
results: result.results.map((result) => ({
|
results: result.results.map((result) => ({
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchResponse } from '@renderer/types'
|
||||||
|
import { filterResultWithBlacklist } from '@renderer/utils/blacklistMatchPattern'
|
||||||
|
|
||||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||||
import WebSearchProviderFactory from './WebSearchProviderFactory'
|
import WebSearchProviderFactory from './WebSearchProviderFactory'
|
||||||
@ -8,7 +10,10 @@ export default class WebSearchEngineProvider {
|
|||||||
constructor(provider: WebSearchProvider) {
|
constructor(provider: WebSearchProvider) {
|
||||||
this.sdk = WebSearchProviderFactory.create(provider)
|
this.sdk = WebSearchProviderFactory.create(provider)
|
||||||
}
|
}
|
||||||
public async search(query: string, maxResult: number, excludeDomains: string[]): Promise<WebSearchResponse> {
|
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
|
||||||
return await this.sdk.search(query, maxResult, excludeDomains)
|
const result = await this.sdk.search(query, websearch)
|
||||||
|
const filteredResult = await filterResultWithBlacklist(result, websearch)
|
||||||
|
|
||||||
|
return filteredResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,16 +97,17 @@ class WebSearchService {
|
|||||||
* @returns 搜索响应
|
* @returns 搜索响应
|
||||||
*/
|
*/
|
||||||
public async search(provider: WebSearchProvider, query: string): Promise<WebSearchResponse> {
|
public async search(provider: WebSearchProvider, query: string): Promise<WebSearchResponse> {
|
||||||
const { searchWithTime, maxResults, excludeDomains } = this.getWebSearchState()
|
const websearch = this.getWebSearchState()
|
||||||
const webSearchEngine = new WebSearchEngineProvider(provider)
|
const webSearchEngine = new WebSearchEngineProvider(provider)
|
||||||
|
|
||||||
let formattedQuery = query
|
let formattedQuery = query
|
||||||
if (searchWithTime) {
|
// 有待商榷,效果一般
|
||||||
|
if (websearch.searchWithTime) {
|
||||||
formattedQuery = `today is ${dayjs().format('YYYY-MM-DD')} \r\n ${query}`
|
formattedQuery = `today is ${dayjs().format('YYYY-MM-DD')} \r\n ${query}`
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await webSearchEngine.search(formattedQuery, maxResults, excludeDomains)
|
return await webSearchEngine.search(formattedQuery, websearch)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Search failed:', error)
|
console.error('Search failed:', error)
|
||||||
throw new Error(`Search failed: ${error instanceof Error ? error.message : 'Unknown 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
|
return newProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const llmSlice = createSlice({
|
||||||
name: 'llm',
|
name: 'llm',
|
||||||
initialState: isLocalAi ? getIntegratedInitialState() : initialState,
|
initialState: isLocalAi ? getIntegratedInitialState() : initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
@ -632,6 +632,6 @@ export const {
|
|||||||
setLMStudioKeepAliveTime,
|
setLMStudioKeepAliveTime,
|
||||||
setGPUStackKeepAliveTime,
|
setGPUStackKeepAliveTime,
|
||||||
updateModel
|
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-google')
|
||||||
addWebSearchProvider(state, 'local-bing')
|
addWebSearchProvider(state, 'local-bing')
|
||||||
addWebSearchProvider(state, 'local-baidu')
|
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')
|
const qiniuProvider = state.llm.providers.find((provider) => provider.id === 'qiniu')
|
||||||
if (qiniuProvider && isEmpty(qiniuProvider.models)) {
|
if (qiniuProvider && isEmpty(qiniuProvider.models)) {
|
||||||
qiniuProvider.models = SYSTEM_MODELS.qiniu
|
qiniuProvider.models = SYSTEM_MODELS.qiniu
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import type { WebSearchProvider } from '@renderer/types'
|
import type { WebSearchProvider } from '@renderer/types'
|
||||||
|
export interface SubscribeSource {
|
||||||
|
key: number
|
||||||
|
url: string
|
||||||
|
name: string
|
||||||
|
blacklist?: string[] // 存储从该订阅源获取的黑名单
|
||||||
|
}
|
||||||
|
|
||||||
export interface WebSearchState {
|
export interface WebSearchState {
|
||||||
// 默认搜索提供商的ID
|
// 默认搜索提供商的ID
|
||||||
@ -12,6 +18,8 @@ export interface WebSearchState {
|
|||||||
maxResults: number
|
maxResults: number
|
||||||
// 要排除的域名列表
|
// 要排除的域名列表
|
||||||
excludeDomains: string[]
|
excludeDomains: string[]
|
||||||
|
// 订阅源列表
|
||||||
|
subscribeSources: SubscribeSource[]
|
||||||
// 是否启用搜索增强模式
|
// 是否启用搜索增强模式
|
||||||
enhanceMode: boolean
|
enhanceMode: boolean
|
||||||
// 是否覆盖服务商搜索
|
// 是否覆盖服务商搜索
|
||||||
@ -55,6 +63,7 @@ const initialState: WebSearchState = {
|
|||||||
searchWithTime: true,
|
searchWithTime: true,
|
||||||
maxResults: 5,
|
maxResults: 5,
|
||||||
excludeDomains: [],
|
excludeDomains: [],
|
||||||
|
subscribeSources: [],
|
||||||
enhanceMode: false,
|
enhanceMode: false,
|
||||||
overwrite: false
|
overwrite: false
|
||||||
}
|
}
|
||||||
@ -89,6 +98,33 @@ const websearchSlice = createSlice({
|
|||||||
setExcludeDomains: (state, action: PayloadAction<string[]>) => {
|
setExcludeDomains: (state, action: PayloadAction<string[]>) => {
|
||||||
state.excludeDomains = action.payload
|
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>) => {
|
setEnhanceMode: (state, action: PayloadAction<boolean>) => {
|
||||||
state.enhanceMode = action.payload
|
state.enhanceMode = action.payload
|
||||||
},
|
},
|
||||||
@ -115,6 +151,10 @@ export const {
|
|||||||
setSearchWithTime,
|
setSearchWithTime,
|
||||||
setExcludeDomains,
|
setExcludeDomains,
|
||||||
setMaxResult,
|
setMaxResult,
|
||||||
|
addSubscribeSource,
|
||||||
|
removeSubscribeSource,
|
||||||
|
updateSubscribeBlacklist,
|
||||||
|
setSubscribeSources,
|
||||||
setEnhanceMode,
|
setEnhanceMode,
|
||||||
setOverwrite,
|
setOverwrite,
|
||||||
addWebSearchProvider
|
addWebSearchProvider
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { WebSearchState } from '@renderer/store/websearch'
|
||||||
|
import { WebSearchResponse } from '@renderer/types'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
@ -170,3 +173,109 @@ function testPath(pathPattern: string, path: string): boolean {
|
|||||||
}
|
}
|
||||||
return path.slice(pos).endsWith(rest[rest.length - 1])
|
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