From 68541a769ed9fae230416cbc711df85ee74ec3ee Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Thu, 24 Apr 2025 06:38:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/renderer/src/App.tsx | 2 + .../DeepResearch/DeepResearchPanel.css | 67 ++ .../DeepResearch/DeepResearchPanel.tsx | 472 ++++++++++ .../src/components/DeepResearch/index.ts | 4 + .../src/components/GeminiInitializer.tsx | 35 + .../src/components/WebSearchInitializer.tsx | 37 +- src/renderer/src/components/app/Sidebar.tsx | 7 +- src/renderer/src/hooks/useWebSearchStore.ts | 13 + src/renderer/src/i18n/locales/en-us.json | 48 +- src/renderer/src/i18n/locales/zh-cn.json | 48 +- src/renderer/src/locales/en/deepresearch.json | 34 + .../src/locales/zh-CN/deepresearch.json | 34 + .../pages/deepresearch/DeepResearchPage.tsx | 36 + .../DisplaySettings/DisplaySettings.tsx | 3 + .../DisplaySettings/EnableDeepResearch.tsx | 65 ++ .../DisplaySettings/SidebarIconsManager.tsx | 6 +- .../DeepResearchSettings.tsx | 175 ++++ .../DeepResearchShortcut.tsx | 55 ++ .../WebSearchSettings/DeepSearchSettings.tsx | 228 +++++ .../settings/WebSearchSettings/index.tsx | 4 + .../WebSearchProvider/DeepResearchProvider.ts | 777 ++++++++++++++++ .../WebSearchProvider/DeepSearchProvider.ts | 863 ++++++++++++++--- .../WebSearchProviderFactory.ts | 3 + src/renderer/src/router/RouterConfig.tsx | 6 + src/renderer/src/store/settings.ts | 42 +- src/renderer/src/store/websearch.ts | 107 ++- src/renderer/src/types/index.ts | 28 + yarn.lock | 869 +++++++++++++++++- 29 files changed, 3914 insertions(+), 156 deletions(-) create mode 100644 src/renderer/src/components/DeepResearch/DeepResearchPanel.css create mode 100644 src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx create mode 100644 src/renderer/src/components/DeepResearch/index.ts create mode 100644 src/renderer/src/components/GeminiInitializer.tsx create mode 100644 src/renderer/src/hooks/useWebSearchStore.ts create mode 100644 src/renderer/src/locales/en/deepresearch.json create mode 100644 src/renderer/src/locales/zh-CN/deepresearch.json create mode 100644 src/renderer/src/pages/deepresearch/DeepResearchPage.tsx create mode 100644 src/renderer/src/pages/settings/DisplaySettings/EnableDeepResearch.tsx create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/DeepResearchSettings.tsx create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/DeepResearchShortcut.tsx create mode 100644 src/renderer/src/pages/settings/WebSearchSettings/DeepSearchSettings.tsx create mode 100644 src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts diff --git a/package.json b/package.json index b588ec3025..65b6b94324 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,8 @@ "@monaco-editor/react": "^4.7.0", "@mozilla/readability": "^0.6.0", "@notionhq/client": "^2.2.15", + "@sentry/electron": "^6.5.0", + "@sentry/react": "^9.14.0", "@shikijs/markdown-it": "^3.2.2", "@strongtz/win32-arm64-msvc": "^0.4.7", "@tryfabric/martian": "^1.2.4", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 111bb2fd28..2d2849cb01 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -5,6 +5,7 @@ import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' import DeepClaudeProvider from './components/DeepClaudeProvider' +import GeminiInitializer from './components/GeminiInitializer' import MemoryProvider from './components/MemoryProvider' import PDFSettingsInitializer from './components/PDFSettingsInitializer' import WebSearchInitializer from './components/WebSearchInitializer' @@ -26,6 +27,7 @@ function App(): React.ReactElement { + diff --git a/src/renderer/src/components/DeepResearch/DeepResearchPanel.css b/src/renderer/src/components/DeepResearch/DeepResearchPanel.css new file mode 100644 index 0000000000..8cefa53b40 --- /dev/null +++ b/src/renderer/src/components/DeepResearch/DeepResearchPanel.css @@ -0,0 +1,67 @@ +.deep-research-container { + padding: 20px; + max-width: 100%; + overflow-x: hidden; +} + +.token-stats { + margin-top: 5px; + font-size: 12px; + color: #888; +} + +.source-link { + word-break: break-word; + overflow-wrap: break-word; + display: block; +} + +.research-loading { + text-align: center; + padding: 40px; +} + +.loading-status { + margin-top: 20px; +} + +.iteration-info { + margin-top: 10px; +} + +.progress-container { + width: 100%; + margin-top: 20px; +} + +.progress-bar-container { + height: 8px; + background: #f0f0f0; + border-radius: 4px; + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: #1890ff; + border-radius: 4px; + transition: width 0.3s ease; +} + +.progress-percentage { + margin-top: 5px; +} + +.error-message { + color: red; + margin-bottom: 20px; +} + +.direct-answer-card { + background-color: #f0f8ff; + margin-bottom: 20px; +} + +.direct-answer-title { + color: #1890ff; +} diff --git a/src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx b/src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx new file mode 100644 index 0000000000..28420fa204 --- /dev/null +++ b/src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx @@ -0,0 +1,472 @@ +import './DeepResearchPanel.css' + +import { + BulbOutlined, + DownloadOutlined, + ExperimentOutlined, + FileSearchOutlined, + HistoryOutlined, + LinkOutlined, + SearchOutlined +} from '@ant-design/icons' +import DeepResearchProvider from '@renderer/providers/WebSearchProvider/DeepResearchProvider' +import { ResearchIteration, ResearchReport, WebSearchResult } from '@renderer/types' +import { Button, Card, Collapse, Divider, Input, List, message, Modal, Space, Spin, Tag, Typography } from 'antd' +import React, { useEffect, useState } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' + +import { useWebSearchStore } from '../../hooks/useWebSearchStore' + +const { Title, Paragraph, Text } = Typography +const { Panel } = Collapse + +// 定义历史研究记录的接口 +interface ResearchHistory { + id: string + query: string + date: string + report: ResearchReport +} + +const DeepResearchPanel: React.FC = () => { + const [query, setQuery] = useState('') + const [isResearching, setIsResearching] = useState(false) + const [report, setReport] = useState(null) + const [error, setError] = useState(null) + const [maxIterations, setMaxIterations] = useState(3) + const [historyVisible, setHistoryVisible] = useState(false) + const [history, setHistory] = useState([]) + const [currentIteration, setCurrentIteration] = useState(0) + const [progressStatus, setProgressStatus] = useState('') + const [progressPercent, setProgressPercent] = useState(0) + + const { providers, selectedProvider, websearch } = useWebSearchStore() + + // 加载历史记录 + useEffect(() => { + const loadHistory = async () => { + try { + const savedHistory = localStorage.getItem('deepResearchHistory') + if (savedHistory) { + setHistory(JSON.parse(savedHistory)) + } + } catch (err) { + console.error('加载历史记录失败:', err) + } + } + + loadHistory() + }, []) + + // 保存历史记录 + const saveToHistory = (newReport: ResearchReport) => { + try { + const newHistory: ResearchHistory = { + id: Date.now().toString(), + query: newReport.originalQuery, + date: new Date().toLocaleString(), + report: newReport + } + + const updatedHistory = [newHistory, ...history].slice(0, 20) // 只保存20条记录 + setHistory(updatedHistory) + localStorage.setItem('deepResearchHistory', JSON.stringify(updatedHistory)) + } catch (err) { + console.error('保存历史记录失败:', err) + } + } + + // 导出报告为Markdown文件 + const exportToMarkdown = (reportToExport: ResearchReport) => { + try { + let markdown = `# 深度研究报告: ${reportToExport.originalQuery}\n\n` + + // 添加问题回答 + markdown += `## 问题回答\n\n${reportToExport.directAnswer}\n\n` + + // 添加关键见解 + markdown += `## 关键见解\n\n` + reportToExport.keyInsights.forEach((insight) => { + markdown += `- ${insight}\n` + }) + + // 添加研究总结 + markdown += `\n## 研究总结\n\n${reportToExport.summary}\n\n` + + // 添加研究过程 + markdown += `## 研究过程\n\n` + reportToExport.iterations.forEach((iteration, index) => { + markdown += `### 迭代 ${index + 1}: ${iteration.query}\n\n` + markdown += `#### 分析\n\n${iteration.analysis}\n\n` + + if (iteration.followUpQueries.length > 0) { + markdown += `#### 后续查询\n\n` + iteration.followUpQueries.forEach((q) => { + markdown += `- ${q}\n` + }) + markdown += '\n' + } + }) + + // 添加信息来源 + markdown += `## 信息来源\n\n` + reportToExport.sources.forEach((source) => { + markdown += `- [${source}](${source})\n` + }) + + // 添加Token统计 + if (reportToExport.tokenUsage) { + markdown += `\n## Token统计\n\n` + markdown += `- 输入Token数: ${reportToExport.tokenUsage.inputTokens.toLocaleString()}\n` + markdown += `- 输出Token数: ${reportToExport.tokenUsage.outputTokens.toLocaleString()}\n` + markdown += `- 总计Token数: ${reportToExport.tokenUsage.totalTokens.toLocaleString()}\n` + } + + // 创建Blob并下载 + const blob = new Blob([markdown], { type: 'text/markdown' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `深度研究-${reportToExport.originalQuery.substring(0, 20)}-${new Date().toISOString().split('T')[0]}.md` + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + + message.success('报告导出成功') + } catch (err) { + console.error('导出报告失败:', err) + message.error('导出报告失败') + } + } + + // 从历史记录中加载报告 + const loadFromHistory = (historyItem: ResearchHistory) => { + setReport(historyItem.report) + setQuery(historyItem.query) + setHistoryVisible(false) + } + + const handleQueryChange = (e: React.ChangeEvent) => { + setQuery(e.target.value) + } + + const handleMaxIterationsChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value) + if (!isNaN(value) && value > 0) { + setMaxIterations(value) + } + } + + const startResearch = async () => { + if (!query.trim()) { + setError('请输入研究查询') + return + } + + if (!selectedProvider) { + setError('请选择搜索提供商') + return + } + + setIsResearching(true) + setError(null) + setReport(null) + setCurrentIteration(0) + setProgressStatus('准备中...') + setProgressPercent(0) + + try { + const provider = providers.find((p) => p.id === selectedProvider) + if (!provider) { + throw new Error('找不到选定的搜索提供商') + } + + const deepResearchProvider = new DeepResearchProvider(provider) + deepResearchProvider.setAnalysisConfig({ + maxIterations, + modelId: websearch?.deepResearchConfig?.modelId + }) + + // 确保 websearch 存在,如果不存在则创建一个空对象 + const webSearchState = websearch || { + defaultProvider: selectedProvider, + providers, + maxResults: 10, + excludeDomains: [], + searchWithTime: false, + subscribeSources: [], + overwrite: false, + deepResearchConfig: { + maxIterations, + maxResultsPerQuery: 50, + autoSummary: true, + enableQueryOptimization: true + } + } + + // 添加进度回调 + const progressCallback = (iteration: number, status: string, percent: number) => { + setCurrentIteration(iteration) + setProgressStatus(status) + setProgressPercent(percent) + } + + // 开始研究 + const researchReport = await deepResearchProvider.research(query, webSearchState, progressCallback) + setReport(researchReport) + + // 保存到历史记录 + saveToHistory(researchReport) + } catch (err: any) { + console.error('深度研究失败:', err) + setError(`研究过程中出错: ${err?.message || '未知错误'}`) + } finally { + setIsResearching(false) + setProgressStatus('') + setProgressPercent(100) + } + } + + const renderResultItem = (result: WebSearchResult) => ( + + + {result.title} + + } + size="small" + style={{ width: '100%', wordBreak: 'break-word', overflowWrap: 'break-word' }}> + + {result.content ? result.content.substring(0, 200) + '...' : '无内容'} + + + 来源: {result.url} + + + + ) + + const renderIteration = (iteration: ResearchIteration, index: number) => ( + + + + 迭代 {index + 1}: {iteration.query} + + + } + key={index}> + 搜索结果 + + + + + 分析 + + + {iteration.analysis} + + + + + + 后续查询 + + {iteration.followUpQueries.map((q, i) => ( + + {q} + + ))} + + + ) + + const renderReport = () => { + if (!report) return null + + return ( +
+ + + <ExperimentOutlined /> 深度研究报告: {report.originalQuery} + + {report.tokenUsage && ( +
+ Token统计: 输入 {report.tokenUsage.inputTokens.toLocaleString()} | 输出{' '} + {report.tokenUsage.outputTokens.toLocaleString()} | 总计 {report.tokenUsage.totalTokens.toLocaleString()} +
+ )} + + + + + 问题回答 + + + + {report.directAnswer} + + + + + + + <BulbOutlined /> 关键见解 + + ( + + {item} + + )} + /> + + + + 研究总结 + + + {report.summary} + + + + + + 研究迭代 + {report.iterations.map((iteration, index) => renderIteration(iteration, index))} + + + + + <LinkOutlined /> 信息来源 + + ( + + + {source} + + + )} + /> +
+
+ ) + } + + // 渲染历史记录对话框 + const renderHistoryModal = () => ( + + 历史研究记录 + + } + open={historyVisible} + onCancel={() => setHistoryVisible(false)} + footer={null} + width={800}> + ( + loadFromHistory(item)}> + 加载 + , + + ]}> + +
日期: {item.date}
+
迭代次数: {item.report.iterations.length}
+ + } + /> +
+ )} + locale={{ emptyText: '暂无历史记录' }} + /> +
+ ) + + return ( +
+ + <ExperimentOutlined /> 深度研究 + + 深度研究功能通过多轮搜索、分析和总结,为您提供全面的研究报告。 + + + } + size="large" + /> + + + 最大迭代次数: + + + + + + + {report && ( + + )} + + + + {error &&
{error}
} + + {isResearching && ( +
+ +
+
正在进行深度研究: {progressStatus}
+
+ 迭代 {currentIteration}/{maxIterations} +
+
+
+
+
+
{progressPercent}%
+
+
+
+ )} + + {report && renderReport()} + + {/* 渲染历史记录对话框 */} + {renderHistoryModal()} +
+ ) +} + +export default DeepResearchPanel diff --git a/src/renderer/src/components/DeepResearch/index.ts b/src/renderer/src/components/DeepResearch/index.ts new file mode 100644 index 0000000000..57fef59c26 --- /dev/null +++ b/src/renderer/src/components/DeepResearch/index.ts @@ -0,0 +1,4 @@ +import DeepResearchPanel from './DeepResearchPanel' + +export { DeepResearchPanel } +export default DeepResearchPanel diff --git a/src/renderer/src/components/GeminiInitializer.tsx b/src/renderer/src/components/GeminiInitializer.tsx new file mode 100644 index 0000000000..c87d717810 --- /dev/null +++ b/src/renderer/src/components/GeminiInitializer.tsx @@ -0,0 +1,35 @@ +import { RootState } from '@renderer/store' +import { updateProvider } from '@renderer/store/llm' +import { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +/** + * GeminiInitializer组件 + * 用于在应用启动时检查Gemini API的配置 + * 如果没有配置API密钥,则禁用Gemini API + */ +const GeminiInitializer = () => { + const dispatch = useDispatch() + const providers = useSelector((state: RootState) => state.llm.providers) + + useEffect(() => { + // 检查Gemini提供商 + const geminiProvider = providers.find((provider) => provider.id === 'gemini') + + // 如果Gemini提供商存在且已启用,但没有API密钥,则禁用它 + if (geminiProvider && geminiProvider.enabled && !geminiProvider.apiKey) { + dispatch( + updateProvider({ + ...geminiProvider, + enabled: false + }) + ) + console.log('Gemini API disabled due to missing API key') + } + }, [dispatch, providers]) + + // 这是一个初始化组件,不需要渲染任何UI + return null +} + +export default GeminiInitializer diff --git a/src/renderer/src/components/WebSearchInitializer.tsx b/src/renderer/src/components/WebSearchInitializer.tsx index 1d9ee19f13..e2987603db 100644 --- a/src/renderer/src/components/WebSearchInitializer.tsx +++ b/src/renderer/src/components/WebSearchInitializer.tsx @@ -1,19 +1,36 @@ +import { RootState } from '@renderer/store' +import { addWebSearchProvider } from '@renderer/store/websearch' +import { WebSearchProvider } from '@renderer/types' import { useEffect } from 'react' -import WebSearchService from '@renderer/services/WebSearchService' +import { useDispatch, useSelector } from 'react-redux' /** - * 初始化WebSearch服务的组件 - * 确保DeepSearch供应商被添加到列表中 + * WebSearchInitializer组件 + * 用于在应用启动时初始化WebSearchService + * 确保DeepSearch在应用启动时被正确设置 */ const WebSearchInitializer = () => { - useEffect(() => { - // 触发WebSearchService的初始化 - // 这将确保DeepSearch供应商被添加到列表中 - WebSearchService.getWebSearchProvider() - console.log('[WebSearchInitializer] 初始化WebSearch服务') - }, []) + const dispatch = useDispatch() + const providers = useSelector((state: RootState) => state.websearch.providers) - // 这个组件不渲染任何内容 + useEffect(() => { + // 检查是否已经存在DeepSearch提供商 + const hasDeepSearch = providers.some((provider) => provider.id === 'deep-search') + + // 如果不存在,添加DeepSearch提供商 + if (!hasDeepSearch) { + const deepSearchProvider: WebSearchProvider = { + id: 'deep-search', + name: 'DeepSearch', + usingBrowser: true, + contentLimit: 10000, + description: '多引擎深度搜索' + } + dispatch(addWebSearchProvider(deepSearchProvider)) + } + }, [dispatch, providers]) + + // 这是一个初始化组件,不需要渲染任何UI return null } diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 77b743e2c7..77df05c608 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -17,6 +17,7 @@ import { Languages, LayoutGrid, MessageSquareQuote, + Microscope, Moon, Palette, Settings, @@ -138,7 +139,8 @@ const MainMenus: FC = () => { minapp: , knowledge: , files: , - workspace: + workspace: , + deepresearch: } const pathMap = { @@ -149,7 +151,8 @@ const MainMenus: FC = () => { minapp: '/apps', knowledge: '/knowledge', files: '/files', - workspace: '/workspace' + workspace: '/workspace', + deepresearch: '/deepresearch' } return sidebarIcons.visible.map((icon) => { diff --git a/src/renderer/src/hooks/useWebSearchStore.ts b/src/renderer/src/hooks/useWebSearchStore.ts new file mode 100644 index 0000000000..26e4debb18 --- /dev/null +++ b/src/renderer/src/hooks/useWebSearchStore.ts @@ -0,0 +1,13 @@ +import { useAppSelector } from '@renderer/store' + +export function useWebSearchStore() { + const websearch = useAppSelector((state) => state.websearch) + const providers = useAppSelector((state) => state.websearch.providers) + const selectedProvider = useAppSelector((state) => state.websearch.defaultProvider) + + return { + websearch, + providers, + selectedProvider + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 65b19ffd4e..9749e3f82e 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -356,6 +356,44 @@ "docs": { "title": "Docs" }, + "deepresearch": { + "title": "Deep Research", + "description": "Deep Research provides comprehensive research reports through multiple rounds of search, analysis, and summarization.", + "engine_rotation": "Use different categories of search engines for each iteration: Chinese, International, Metasearch, and Academic", + "startResearch": "Start Deep Research", + "open": "Open Deep Research", + "open_success": "Opening Deep Research page", + "open_error": "Failed to open Deep Research page", + "history": "History", + "exportReport": "Export Report", + "maxIterations": "Max Iterations", + "researchLoading": "Conducting Deep Research", + "iteration": "Iteration", + "directAnswer": "Answer", + "keyInsights": "Key Insights", + "summary": "Research Summary", + "researchIterations": "Research Iterations", + "sources": "Sources", + "searchResults": "Search Results", + "analysis": "Analysis", + "followUpQueries": "Follow-up Queries", + "historyTitle": "Research History", + "load": "Load", + "export": "Export", + "noHistory": "No history records", + "date": "Date", + "iterationCount": "Iteration Count", + "tokenStats": "Token Statistics", + "input": "Input", + "output": "Output", + "total": "Total", + "error": { + "emptyQuery": "Please enter a research query", + "noProvider": "Please select a search provider", + "providerNotFound": "Selected search provider not found", + "researchFailed": "Error during research process" + } + }, "error": { "backup.file_format": "Backup file format error", "chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers", @@ -1659,7 +1697,15 @@ "overwrite": "Override search service", "overwrite_tooltip": "Force use search service instead of LLM", "apikey": "API key", - "free": "Free" + "free": "Free", + "deep_research": { + "title": "Deep Research", + "max_iterations": "Maximum Iterations", + "max_results_per_query": "Maximum Results Per Query", + "auto_summary": "Auto Summary", + "enable_query_optimization": "Enable Query Optimization", + "query_optimization_desc": "Use AI to analyze your question and generate more effective search queries" + } }, "quickPhrase": { "title": "Quick Phrases", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c7581908f4..18104c22fd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -358,6 +358,44 @@ "docs": { "title": "帮助文档" }, + "deepresearch": { + "title": "深度研究", + "description": "深度研究功能通过多轮搜索、分析和总结,为您提供全面的研究报告。", + "engine_rotation": "每次迭代使用不同类别的搜索引擎:中文、国际、元搜索和学术搜索", + "startResearch": "开始深度研究", + "open": "打开深度研究", + "open_success": "正在打开深度研究页面", + "open_error": "打开深度研究页面失败", + "history": "历史记录", + "exportReport": "导出报告", + "maxIterations": "最大迭代次数", + "researchLoading": "正在进行深度研究", + "iteration": "迭代", + "directAnswer": "问题回答", + "keyInsights": "关键见解", + "summary": "研究总结", + "researchIterations": "研究迭代", + "sources": "信息来源", + "searchResults": "搜索结果", + "analysis": "分析", + "followUpQueries": "后续查询", + "historyTitle": "历史研究记录", + "load": "加载", + "export": "导出", + "noHistory": "暂无历史记录", + "date": "日期", + "iterationCount": "迭代次数", + "tokenStats": "Token统计", + "input": "输入", + "output": "输出", + "total": "总计", + "error": { + "emptyQuery": "请输入研究查询", + "noProvider": "请选择搜索提供商", + "providerNotFound": "找不到选定的搜索提供商", + "researchFailed": "研究过程中出错" + } + }, "error": { "backup.file_format": "备份文件格式错误", "chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥", @@ -1766,7 +1804,15 @@ }, "title": "网络搜索", "apikey": "API 密钥", - "free": "免费" + "free": "免费", + "deep_research": { + "title": "深度研究", + "max_iterations": "最大迭代次数", + "max_results_per_query": "每次查询最大结果数", + "auto_summary": "自动生成摘要", + "enable_query_optimization": "启用查询优化", + "query_optimization_desc": "使用 AI 分析您的问题并生成更有效的搜索查询" + } }, "quickPhrase": { "title": "快捷短语", diff --git a/src/renderer/src/locales/en/deepresearch.json b/src/renderer/src/locales/en/deepresearch.json new file mode 100644 index 0000000000..a29c37dadb --- /dev/null +++ b/src/renderer/src/locales/en/deepresearch.json @@ -0,0 +1,34 @@ +{ + "title": "Deep Research", + "description": "Deep Research provides comprehensive research reports through multiple rounds of search, analysis, and summarization.", + "startResearch": "Start Deep Research", + "history": "History", + "exportReport": "Export Report", + "maxIterations": "Max Iterations", + "researchLoading": "Conducting Deep Research", + "iteration": "Iteration", + "directAnswer": "Answer", + "keyInsights": "Key Insights", + "summary": "Research Summary", + "researchIterations": "Research Iterations", + "sources": "Sources", + "searchResults": "Search Results", + "analysis": "Analysis", + "followUpQueries": "Follow-up Queries", + "historyTitle": "Research History", + "load": "Load", + "export": "Export", + "noHistory": "No history records", + "date": "Date", + "iterationCount": "Iteration Count", + "tokenStats": "Token Statistics", + "input": "Input", + "output": "Output", + "total": "Total", + "error": { + "emptyQuery": "Please enter a research query", + "noProvider": "Please select a search provider", + "providerNotFound": "Selected search provider not found", + "researchFailed": "Error during research process" + } +} diff --git a/src/renderer/src/locales/zh-CN/deepresearch.json b/src/renderer/src/locales/zh-CN/deepresearch.json new file mode 100644 index 0000000000..06707d1926 --- /dev/null +++ b/src/renderer/src/locales/zh-CN/deepresearch.json @@ -0,0 +1,34 @@ +{ + "title": "深度研究", + "description": "深度研究功能通过多轮搜索、分析和总结,为您提供全面的研究报告。", + "startResearch": "开始深度研究", + "history": "历史记录", + "exportReport": "导出报告", + "maxIterations": "最大迭代次数", + "researchLoading": "正在进行深度研究", + "iteration": "迭代", + "directAnswer": "问题回答", + "keyInsights": "关键见解", + "summary": "研究总结", + "researchIterations": "研究迭代", + "sources": "信息来源", + "searchResults": "搜索结果", + "analysis": "分析", + "followUpQueries": "后续查询", + "historyTitle": "历史研究记录", + "load": "加载", + "export": "导出", + "noHistory": "暂无历史记录", + "date": "日期", + "iterationCount": "迭代次数", + "tokenStats": "Token统计", + "input": "输入", + "output": "输出", + "total": "总计", + "error": { + "emptyQuery": "请输入研究查询", + "noProvider": "请选择搜索提供商", + "providerNotFound": "找不到选定的搜索提供商", + "researchFailed": "研究过程中出错" + } +} diff --git a/src/renderer/src/pages/deepresearch/DeepResearchPage.tsx b/src/renderer/src/pages/deepresearch/DeepResearchPage.tsx new file mode 100644 index 0000000000..4c1f07562c --- /dev/null +++ b/src/renderer/src/pages/deepresearch/DeepResearchPage.tsx @@ -0,0 +1,36 @@ +import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' +import React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import DeepResearchPanel from '../../components/DeepResearch' + +const DeepResearchPage: React.FC = () => { + const { t } = useTranslation() + + return ( + + + {t('deepresearch.title', 'Deep Research')} + + + + + + ) +} + +const Container = styled.div` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; +` + +const Content = styled.div` + flex: 1; + overflow: auto; + padding: 0; +` + +export default DeepResearchPage diff --git a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx index d504cbe95d..9e2e3d0575 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/DisplaySettings.tsx @@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import EnableDeepResearch from './EnableDeepResearch' import SidebarIconsManager from './SidebarIconsManager' const DisplaySettings: FC = () => { @@ -178,6 +179,8 @@ const DisplaySettings: FC = () => { setDisabledIcons={setDisabledIcons} /> + + {t('settings.display.custom.css')} diff --git a/src/renderer/src/pages/settings/DisplaySettings/EnableDeepResearch.tsx b/src/renderer/src/pages/settings/DisplaySettings/EnableDeepResearch.tsx new file mode 100644 index 0000000000..8bb302f6a6 --- /dev/null +++ b/src/renderer/src/pages/settings/DisplaySettings/EnableDeepResearch.tsx @@ -0,0 +1,65 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import { useSettings } from '@renderer/hooks/useSettings' +import { useAppDispatch } from '@renderer/store' +import { SidebarIcon, setSidebarIcons } from '@renderer/store/settings' +import { Button, message } from 'antd' +import { Microscope } from 'lucide-react' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const EnableDeepResearch: FC = () => { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { sidebarIcons } = useSettings() + const { theme: themeMode } = useTheme() + + const isDeepResearchEnabled = sidebarIcons.visible.includes('deepresearch') + + const handleEnableDeepResearch = () => { + if (!isDeepResearchEnabled) { + const newVisibleIcons: SidebarIcon[] = [...sidebarIcons.visible, 'deepresearch' as SidebarIcon] + dispatch(setSidebarIcons({ visible: newVisibleIcons })) + message.success(t('deepresearch.enable_success', '深度研究功能已启用,请查看侧边栏')) + } + } + + return ( + + + + + + {t('deepresearch.title', '深度研究')} + + + + + {t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')} + + {!isDeepResearchEnabled ? ( + + ) : ( + {t('deepresearch.already_enabled', '已启用')} + )} + + + ) +} + +const IconWrapper = styled.span` + margin-right: 8px; + display: inline-flex; + align-items: center; +` + +const EnabledText = styled.span` + color: var(--color-success); + font-weight: 500; +` + +export default EnableDeepResearch diff --git a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx index 2263651d60..a544eafd76 100644 --- a/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx +++ b/src/renderer/src/pages/settings/DisplaySettings/SidebarIconsManager.tsx @@ -10,7 +10,7 @@ import { import { useAppDispatch } from '@renderer/store' import { setSidebarIcons } from '@renderer/store/settings' import { message } from 'antd' -import { Folder, Languages, LayoutGrid, LibraryBig, MessageSquareQuote, Palette, Sparkle } from 'lucide-react' +import { Folder, FolderGit, Languages, LayoutGrid, LibraryBig, MessageSquareQuote, Microscope, Palette, Sparkle } from 'lucide-react' import { FC, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -115,7 +115,9 @@ const SidebarIconsManager: FC = ({ translate: , minapp: , knowledge: , - files: + files: , + workspace: , + deepresearch: }), [] ) diff --git a/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchSettings.tsx new file mode 100644 index 0000000000..728fb7fbf4 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchSettings.tsx @@ -0,0 +1,175 @@ +import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' +import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' +import { useTheme } from '@renderer/context/ThemeProvider' +import { getModelUniqId } from '@renderer/services/ModelService' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setDeepResearchConfig } from '@renderer/store/websearch' +import { Model } from '@renderer/types' +import { Button, InputNumber, Space, Switch } from 'antd' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const SubDescription = styled.div` + font-size: 12px; + color: #888; + margin-top: 4px; +` + +const DeepResearchSettings: FC = () => { + const { t } = useTranslation() + const { theme: themeMode } = useTheme() + const dispatch = useAppDispatch() + const navigate = useNavigate() + const providers = useAppSelector((state) => state.llm.providers) + const deepResearchConfig = useAppSelector((state) => state.websearch.deepResearchConfig) || { + maxIterations: 3, + maxResultsPerQuery: 20, + autoSummary: true, + enableQueryOptimization: true + } + + // 当前选择的模型 + const [selectedModel, setSelectedModel] = useState(null) + + // 初始化时,如果有保存的模型ID,则加载对应的模型 + useEffect(() => { + if (deepResearchConfig.modelId) { + const allModels = providers.flatMap((p) => p.models) + const model = allModels.find((m) => getModelUniqId(m) === deepResearchConfig.modelId) + if (model) { + setSelectedModel(model) + } + } + }, [deepResearchConfig.modelId, providers]) + + const handleMaxIterationsChange = (value: number | null) => { + if (value !== null) { + dispatch( + setDeepResearchConfig({ + ...deepResearchConfig, + maxIterations: value + }) + ) + } + } + + const handleMaxResultsPerQueryChange = (value: number | null) => { + if (value !== null) { + dispatch( + setDeepResearchConfig({ + ...deepResearchConfig, + maxResultsPerQuery: value + }) + ) + } + } + + const handleAutoSummaryChange = (checked: boolean) => { + dispatch( + setDeepResearchConfig({ + ...deepResearchConfig, + autoSummary: checked + }) + ) + } + + const handleQueryOptimizationChange = (checked: boolean) => { + dispatch( + setDeepResearchConfig({ + ...deepResearchConfig, + enableQueryOptimization: checked + }) + ) + } + + const handleOpenDeepResearch = () => { + navigate('/deepresearch') + } + + const handleSelectModel = async () => { + const model = await SelectModelPopup.show({ model: selectedModel || undefined }) + if (model) { + setSelectedModel(model) + dispatch( + setDeepResearchConfig({ + ...deepResearchConfig, + modelId: getModelUniqId(model) + }) + ) + } + } + + return ( + + {t('settings.websearch.deep_research.title')} + + + + + {t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')} + + {t('deepresearch.engine_rotation', '每次迭代使用不同类别的搜索引擎:中文、国际、元搜索和学术搜索')} + + + + + + + + + {t('settings.model.select', '选择模型')} + + + + + {t('settings.websearch.deep_research.max_iterations')} + + + + + {t('settings.websearch.deep_research.max_results_per_query')} + + + + + {t('settings.websearch.deep_research.auto_summary')} + + + + + + {t('settings.websearch.deep_research.enable_query_optimization', '启用查询优化')} + + {t( + 'settings.websearch.deep_research.query_optimization_desc', + '使用 AI 分析您的问题并生成更有效的搜索查询' + )} + + + + + + ) +} + +export default DeepResearchSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchShortcut.tsx b/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchShortcut.tsx new file mode 100644 index 0000000000..72386d0181 --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchShortcut.tsx @@ -0,0 +1,55 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import { modelGenerating } from '@renderer/hooks/useRuntime' +import { Button, message } from 'antd' +import { Microscope } from 'lucide-react' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const DeepResearchShortcut: FC = () => { + const { t } = useTranslation() + const navigate = useNavigate() + const { theme: themeMode } = useTheme() + + const handleOpenDeepResearch = async () => { + try { + await modelGenerating() + navigate('/deepresearch') + message.success(t('deepresearch.open_success', '正在打开深度研究页面')) + } catch (error) { + console.error('打开深度研究页面失败:', error) + message.error(t('deepresearch.open_error', '打开深度研究页面失败')) + } + } + + return ( + + + + + + {t('deepresearch.title', '深度研究')} + + + + + {t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')} + + + + + ) +} + +const IconWrapper = styled.span` + margin-right: 8px; + display: inline-flex; + align-items: center; +` + +export default DeepResearchShortcut diff --git a/src/renderer/src/pages/settings/WebSearchSettings/DeepSearchSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/DeepSearchSettings.tsx new file mode 100644 index 0000000000..2bec5b7c4b --- /dev/null +++ b/src/renderer/src/pages/settings/WebSearchSettings/DeepSearchSettings.tsx @@ -0,0 +1,228 @@ +import { useTheme } from '@renderer/context/ThemeProvider' +import { useAppDispatch, useAppSelector } from '@renderer/store' +import { setDeepSearchConfig } from '@renderer/store/websearch' +import { Checkbox, Space } from 'antd' +import { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const SubDescription = styled.div` + font-size: 12px; + color: #888; + margin-top: 4px; +` + +const DeepSearchSettings: FC = () => { + const { t } = useTranslation() + const { theme: themeMode } = useTheme() + const dispatch = useAppDispatch() + + // 从 store 获取 DeepSearch 配置 + const deepSearchConfig = useAppSelector((state) => state.websearch.deepSearchConfig) + + // 本地状态 - 使用 deepSearchConfig?.enabledEngines 作为初始值,如果不存在则使用默认值 + const [enabledEngines, setEnabledEngines] = useState(() => deepSearchConfig?.enabledEngines || { + // 中文搜索引擎 + baidu: true, + sogou: true, + '360': false, + yisou: false, + + // 国际搜索引擎 + bing: true, + duckduckgo: true, + brave: false, + qwant: false, + + // 元搜索引擎 + searx: true, + ecosia: false, + startpage: false, + mojeek: false, + + // 学术搜索引擎 + scholar: true, + semantic: false, + base: false, + cnki: false + }) + + // 当 deepSearchConfig.enabledEngines 的引用发生变化时更新本地状态 + useEffect(() => { + if (deepSearchConfig?.enabledEngines) { + // 比较当前状态和新状态,只有当它们不同时才更新 + const currentKeys = Object.keys(enabledEngines); + const newKeys = Object.keys(deepSearchConfig.enabledEngines); + + // 检查键是否相同 + if (currentKeys.length !== newKeys.length || + !currentKeys.every(key => newKeys.includes(key))) { + setEnabledEngines(deepSearchConfig.enabledEngines); + return; + } + + // 检查值是否相同 + let needsUpdate = false; + for (const key of currentKeys) { + if (enabledEngines[key] !== deepSearchConfig.enabledEngines[key]) { + needsUpdate = true; + break; + } + } + + if (needsUpdate) { + setEnabledEngines(deepSearchConfig.enabledEngines); + } + } + }, [deepSearchConfig?.enabledEngines]) + + // 处理搜索引擎选择变化 + const handleEngineChange = (engine: string, checked: boolean) => { + const newEnabledEngines = { + ...enabledEngines, + [engine]: checked + } + + setEnabledEngines(newEnabledEngines) + + // 更新 store + dispatch( + setDeepSearchConfig({ + enabledEngines: newEnabledEngines + }) + ) + } + + return ( + + {t('settings.websearch.deepsearch.title', 'DeepSearch 设置')} + + + + + {t('settings.websearch.deepsearch.description', '选择要在 DeepSearch 中使用的搜索引擎')} + + {t('settings.websearch.deepsearch.subdescription', '选择的搜索引擎将在 DeepSearch 中并行使用,不会影响 DeepResearch')} + + +
+ +
中文搜索引擎
+ handleEngineChange('baidu', e.target.checked)} + > + 百度 (Baidu) + + handleEngineChange('sogou', e.target.checked)} + > + 搜狗 (Sogou) + + handleEngineChange('360', e.target.checked)} + > + 360搜索 + + handleEngineChange('yisou', e.target.checked)} + > + 一搜 (Yisou) + +
+ + +
国际搜索引擎
+ handleEngineChange('bing', e.target.checked)} + > + 必应 (Bing) + + handleEngineChange('duckduckgo', e.target.checked)} + > + DuckDuckGo + + handleEngineChange('brave', e.target.checked)} + > + Brave Search + + handleEngineChange('qwant', e.target.checked)} + > + Qwant + +
+ + +
元搜索引擎
+ handleEngineChange('searx', e.target.checked)} + > + SearX + + handleEngineChange('ecosia', e.target.checked)} + > + Ecosia + + handleEngineChange('startpage', e.target.checked)} + > + Startpage + + handleEngineChange('mojeek', e.target.checked)} + > + Mojeek + +
+ + +
学术搜索引擎
+ handleEngineChange('scholar', e.target.checked)} + > + Google Scholar + + handleEngineChange('semantic', e.target.checked)} + > + Semantic Scholar + + handleEngineChange('base', e.target.checked)} + > + BASE + + handleEngineChange('cnki', e.target.checked)} + > + CNKI 知网 + +
+
+
+
+ ) +} + +export default DeepSearchSettings diff --git a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx index e00eb785e7..d5c6380b86 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx @@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import BasicSettings from './BasicSettings' import BlacklistSettings from './BlacklistSettings' +import DeepResearchSettings from './DeepResearchSettings' +import DeepSearchSettings from './DeepSearchSettings' import WebSearchProviderSetting from './WebSearchProviderSetting' const WebSearchSettings: FC = () => { @@ -57,6 +59,8 @@ const WebSearchSettings: FC = () => { )} + + ) } diff --git a/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts new file mode 100644 index 0000000000..e35e55387e --- /dev/null +++ b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts @@ -0,0 +1,777 @@ +import { WebSearchState } from '@renderer/store/websearch' +import { + ResearchIteration, + ResearchReport, + WebSearchProvider, + WebSearchResponse, + WebSearchResult +} from '@renderer/types' + +import BaseWebSearchProvider from './BaseWebSearchProvider' +import DeepSearchProvider from './DeepSearchProvider' + +/** + * 分析配置接口 + */ +interface AnalysisConfig { + maxIterations: number + maxResultsPerQuery: number + minConfidenceScore: number + autoSummary: boolean + modelId?: string + minOutputTokens?: number // 最小输出token数 + maxInputTokens?: number // 最大输入token数 +} + +// 使用从 types/index.ts 导入的 ResearchIteration 和 ResearchReport 类型 + +/** + * DeepResearchProvider 类 + * 提供深度研究功能,包括多轮搜索、分析和总结 + */ +class DeepResearchProvider extends BaseWebSearchProvider { + private deepSearchProvider: DeepSearchProvider + private analysisConfig: AnalysisConfig + + constructor(provider: WebSearchProvider) { + super(provider) + this.deepSearchProvider = new DeepSearchProvider(provider) + this.analysisConfig = { + maxIterations: 3, // 默认最大迭代次数 + maxResultsPerQuery: 50, // 每次查询的最大结果数 + minConfidenceScore: 0.6, // 最小可信度分数 + autoSummary: true, // 自动生成摘要 + minOutputTokens: 20000, // 最小输出20,000 tokens + maxInputTokens: 200000 // 最大输入200,000 tokens + } + } + + // 实现 BaseWebSearchProvider 的抽象方法 + public async search(query: string, websearch: WebSearchState): Promise { + // 调用 research 方法并将结果转换为 WebSearchResponse 格式 + const researchResults = await this.research(query, websearch) + + // 从研究结果中提取搜索结果 + const allResults = researchResults.iterations.flatMap((iter) => iter.results) + + // 返回标准的 WebSearchResponse 格式 + return { + query, + results: allResults + } + } + + /** + * 优化查询 + * @param query 用户原始查询 + * @returns 优化后的查询 + */ + private async optimizeQuery(query: string): Promise { + try { + console.log(`[DeepResearch] 正在优化查询: "${query}"`) + + // 使用模型优化查询 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const prompt = `你是一个搜索优化专家,负责将用户的问题转化为最有效的搜索查询。 + +用户问题: "${query}" + +请分析这个问题,并生成一个更有效的搜索查询。你的查询应该: +1. 提取关键概念和术语 +2. 去除不必要的虚词和介词 +3. 增加相关的同义词或专业术语(如果适用) +4. 保持简洁明确,不超过 10 个关键词 + +只返回优化后的查询词,不要添加任何解释或其他文本。` + + const optimizedQuery = await fetchGenerate({ + prompt, + content: ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId + }) + + // 如果优化失败,返回原始查询 + if (!optimizedQuery || optimizedQuery.trim() === '') { + return query + } + + console.log(`[DeepResearch] 查询优化结果: "${optimizedQuery}"`) + return optimizedQuery.trim() + } catch (error) { + console.error('[DeepResearch] 查询优化失败:', error) + return query // 出错时返回原始查询 + } + } + + /** + * 执行深度研究 + * @param query 初始查询 + * @param websearch WebSearch状态 + * @param progressCallback 进度回调函数 + * @returns 研究报告 + */ + public async research( + query: string, + websearch?: WebSearchState, + progressCallback?: (iteration: number, status: string, percent: number) => void + ): Promise { + // 确保 websearch 存在 + const webSearchState: WebSearchState = websearch || { + defaultProvider: '', + providers: [], + maxResults: 10, + excludeDomains: [], + searchWithTime: false, + subscribeSources: [], + enhanceMode: true, + overwrite: false, + deepResearchConfig: { + maxIterations: this.analysisConfig.maxIterations, + maxResultsPerQuery: this.analysisConfig.maxResultsPerQuery, + autoSummary: this.analysisConfig.autoSummary || true + } + } + console.log(`[DeepResearch] 开始深度研究: "${query}"`) + + // 根据配置决定是否优化查询 + let optimizedQuery = query + if (webSearchState.deepResearchConfig?.enableQueryOptimization !== false) { + console.log(`[DeepResearch] 启用查询优化`) + optimizedQuery = await this.optimizeQuery(query) + } else { + console.log(`[DeepResearch] 未启用查询优化`) + } + + const report: ResearchReport = { + originalQuery: query, + iterations: [], + summary: '', + directAnswer: '', // 初始化为空字符串 + keyInsights: [], + sources: [], + tokenUsage: { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0 + } + } + + let currentQuery = optimizedQuery + let iterationCount = 0 + const allSources = new Set() + + // 定义搜索引擎类别列表 + const engineCategories = ['chinese', 'international', 'meta', 'academic'] + + // 迭代研究过程 + while (iterationCount < this.analysisConfig.maxIterations) { + // 调用进度回调 + if (progressCallback) { + const percent = Math.round((iterationCount / this.analysisConfig.maxIterations) * 100) + progressCallback(iterationCount + 1, `迭代 ${iterationCount + 1}: ${currentQuery}`, percent) + } + + console.log(`[DeepResearch] 迭代 ${iterationCount + 1}: "${currentQuery}"`) + + // 根据当前迭代选择搜索引擎类别 + const categoryIndex = iterationCount % engineCategories.length + const currentCategory = engineCategories[categoryIndex] + console.log(`[DeepResearch] 这一迭代使用 ${currentCategory} 类别的搜索引擎`) + + // 1. 使用DeepSearch获取当前查询的结果,指定搜索引擎类别 + if (progressCallback) { + progressCallback( + iterationCount + 1, + `正在搜索: ${currentQuery}`, + Math.round((iterationCount / this.analysisConfig.maxIterations) * 100) + ) + } + const searchResponse = await this.deepSearchProvider.search(currentQuery, webSearchState, currentCategory) + + // 限制结果数量 + const limitedResults = searchResponse.results.slice(0, this.analysisConfig.maxResultsPerQuery) + + // 2. 分析搜索结果 + if (progressCallback) { + progressCallback( + iterationCount + 1, + `正在分析 ${limitedResults.length} 个结果...`, + Math.round((iterationCount / this.analysisConfig.maxIterations) * 100) + ) + } + const analysis = await this.analyzeResults(limitedResults, currentQuery, report) + + // 3. 生成后续查询 + if (progressCallback) { + progressCallback( + iterationCount + 1, + `正在生成后续查询...`, + Math.round((iterationCount / this.analysisConfig.maxIterations) * 100) + ) + } + const followUpQueries = await this.generateFollowUpQueries(analysis, currentQuery, report.iterations) + + // 4. 记录这次迭代 + const iteration: ResearchIteration = { + query: currentQuery, + results: limitedResults, + analysis, + followUpQueries + } + + report.iterations.push(iteration) + + // 5. 收集源 + limitedResults.forEach((result) => { + if (result.url) { + allSources.add(result.url) + } + }) + + // 6. 检查是否继续迭代 + if (followUpQueries.length === 0) { + console.log(`[DeepResearch] 没有更多的后续查询,结束迭代`) + if (progressCallback) { + progressCallback( + iterationCount + 1, + `迭代完成,没有更多后续查询`, + Math.round(((iterationCount + 1) / this.analysisConfig.maxIterations) * 100) + ) + } + break + } + + // 7. 更新查询并继续 + currentQuery = followUpQueries[0] // 使用第一个后续查询 + iterationCount++ + } + + // 生成最终总结 + if (progressCallback) { + progressCallback(iterationCount, `正在生成研究总结...`, 70) + } + report.summary = await this.generateSummary(report.iterations, report) + + if (progressCallback) { + progressCallback(iterationCount, `正在提取关键见解...`, 80) + } + report.keyInsights = await this.extractKeyInsights(report.iterations, report) + + if (progressCallback) { + progressCallback(iterationCount, `正在生成问题回答...`, 90) + } + report.directAnswer = await this.generateDirectAnswer(query, report.summary, report.keyInsights, report) + + report.sources = Array.from(allSources) + + if (progressCallback) { + progressCallback(iterationCount, `研究完成`, 100) + } + + console.log(`[DeepResearch] 完成深度研究,共 ${report.iterations.length} 次迭代`) + return report + } + + /** + * 分析搜索结果 + * @param results 搜索结果 + * @param query 当前查询 + * @returns 分析文本 + */ + private async analyzeResults(results: WebSearchResult[], query: string, report?: ResearchReport): Promise { + if (results.length === 0) { + return `没有找到关于"${query}"的相关信息。` + } + + try { + console.log(`[DeepResearch] 分析 ${results.length} 个结果`) + + // 提取关键信息 + const contentSummaries = results + .map((result, index) => { + const content = result.content || '无内容' + // 提取前300个字符作为摘要 + const summary = content.length > 300 ? content.substring(0, 300) + '...' : content + + return `[${index + 1}] ${result.title}\n${summary}\n来源: ${result.url}\n` + }) + .join('\n') + + // 使用模型分析内容 + const analysis = await this.analyzeWithModel(contentSummaries, query, report) + + return analysis + } catch (error: any) { + console.error('[DeepResearch] 分析结果时出错:', error) + return `分析过程中出现错误: ${error?.message || '未知错误'}` + } + } + + /** + * 使用项目中的模型分析搜索结果 + */ + private async analyzeWithModel(contentSummaries: string, query: string, report?: ResearchReport): Promise { + try { + console.log(`[DeepResearch] 使用模型分析搜索结果`) + + // 分析提示词 + const prompt = `你是一个高级研究分析师,负责深入分析搜索结果并提取全面、详细的见解。 +请对以下关于"${query}"的搜索结果进行彻底分析,并提供以下内容: + +1. 主要发现(详细列出5-8个要点,每个要点至少包含3-4句解释) +2. 争议点或不同观点(详细分析各方观点及其依据) +3. 技术细节和实现方法(如适用) +4. 历史背景和发展脉络(如适用) +5. 最新进展和趋势 +6. 专家共识和最佳实践 +7. 需要进一步研究的领域(列出3-5个方向,并解释为什么这些方向值得探索) + +请使用学术性、分析性的语言,提供深入的分析而非简单总结。确保你的分析: +- 包含具体的事实、数据和例子 +- 引用搜索结果中的具体信息源 +- 区分已确认的事实和推测性内容 +- 指出信息的可靠性和局限性 + +搜索结果内容: +${contentSummaries}` + + // 检查内容是否为空 + if (!contentSummaries || contentSummaries.trim() === '') { + return `没有找到关于"${query}"的有效内容可供分析。` + } + + // 限制输入token数量 + let trimmedContent = contentSummaries + const maxInputTokens = this.analysisConfig.maxInputTokens || 200000 + const inputTokens = this.estimateTokens(prompt + trimmedContent) + + if (inputTokens > maxInputTokens) { + console.log(`[DeepResearch] 输入内容超过最大token限制(${inputTokens} > ${maxInputTokens}),进行裁剪`) + // 计算需要保留的比例 + const ratio = maxInputTokens / inputTokens + const contentTokens = this.estimateTokens(trimmedContent) + const targetContentTokens = Math.floor(contentTokens * ratio) - 1000 // 留出一些空间给提示词 + + // 按比例裁剪内容 + const contentLines = trimmedContent.split('\n') + let currentTokens = 0 + const truncatedLines: string[] = [] + + for (const line of contentLines) { + const lineTokens = this.estimateTokens(line) + if (currentTokens + lineTokens <= targetContentTokens) { + truncatedLines.push(line) + currentTokens += lineTokens + } else { + break + } + } + + trimmedContent = truncatedLines.join('\n') + console.log(`[DeepResearch] 内容已裁剪至约 ${this.estimateTokens(trimmedContent)} tokens`) + } + + // 使用项目中的 fetchGenerate 函数调用模型 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const analysis = await fetchGenerate({ + prompt, + content: trimmedContent || ' ', // 使用裁剪后的内容 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + // 更新token统计 + if (report?.tokenUsage) { + report.tokenUsage.inputTokens += this.estimateTokens(prompt + trimmedContent) + report.tokenUsage.outputTokens += this.estimateTokens(analysis || '') + report.tokenUsage.totalTokens = report.tokenUsage.inputTokens + report.tokenUsage.outputTokens + } + + return analysis || `分析失败,无法获取结果。` + } catch (error: any) { + console.error('[DeepResearch] 模型分析失败:', error) + return `分析过程中出现错误: ${error?.message || '未知错误'}` + } + } + + /** + * 生成后续查询 + * @param analysis 当前分析 + * @param currentQuery 当前查询 + * @param previousIterations 之前的迭代 + * @returns 后续查询列表 + */ + private async generateFollowUpQueries( + analysis: string, + currentQuery: string, + previousIterations: ResearchIteration[] + ): Promise { + try { + // 避免重复查询 + const previousQueries = new Set(previousIterations.map((i) => i.query)) + previousQueries.add(currentQuery) + + // 使用模型生成后续查询 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const prompt = `你是一个研究助手,负责生成后续查询。 +基于以下关于"${currentQuery}"的分析,生成 2-3 个后续查询,以深入探索该主题。 + +分析内容: +${analysis} + +请仅返回查询列表,每行一个,不要添加编号或其他标记。确保查询简洁、具体且与原始查询"${currentQuery}"相关。` + + const result = await fetchGenerate({ + prompt, + content: ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + if (!result) { + return [] + } + + // 处理生成的查询 + const candidateQueries = result + .split('\n') + .map((q) => q.trim()) + .filter((q) => q.length > 0) + + // 过滤掉已经查询过的 + const newQueries = candidateQueries.filter((q) => !previousQueries.has(q)) + + // 限制查询数量 + return newQueries.slice(0, 3) + } catch (error: any) { + console.error('[DeepResearch] 生成后续查询失败:', error) + return [] + } + } + + /** + * 生成研究总结 + * @param iterations 所有迭代 + * @returns 总结文本 + */ + private async generateSummary(iterations: ResearchIteration[], report?: ResearchReport): Promise { + if (iterations.length === 0) { + return '没有足够的研究数据来生成总结。' + } + + try { + const mainQuery = iterations[0].query + + // 收集所有迭代的分析和查询 + const iterationsData = iterations + .map((iter, index) => { + return `迭代 ${index + 1}:\n查询: ${iter.query}\n分析:\n${iter.analysis}\n` + }) + .join('\n---\n\n') + + // 使用模型生成总结 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const prompt = `你是一个高级学术研究分析师,负责生成深入、全面的研究总结。 +请基于以下关于"${mainQuery}"的多轮研究迭代,生成一份学术水准的综合研究报告。 + +报告应包含以下部分,每部分都应详细、深入,并包含具体例证: + +1. 摘要(简要总结整个研究的主要发现) + +2. 背景与历史背景 + - 该主题的起源与发展过程 + - 关键里程碑与转折点 + +3. 主要发现(详细列出至少8-10个要点) + - 每个发现应有充分的说明和例证 + - 引用具体数据、研究或信息源 + +4. 争议点与不同观点 + - 详细分析各方立场及其依据 + - 客观评估各种观点的合理性和限制 + +5. 技术细节与实现方法(如适用) + - 当前技术实现的详细分析 + - 各种方法的比较与评估 + +6. 当前领域状态 + - 最新研究进展与突破 + - 主要参与者与机构 + - 当前面临的挑战与障碍 + +7. 专家共识与最佳实践 + - 行业公认的标准与方法 + - 成功案例与经验教训 + +8. 未来发展趋势 + - 预测的发展方向与变革 + - 潜在的机遇与风险 + +9. 研究局限性 + - 当前研究的不足与局限 + - 数据或方法的可靠性考量 + +10. 建议的未来研究方向 + - 具体的研究问题与方法建议 + - 为什么这些方向值得探索 + +请使用学术性、分析性的语言,提供深入的分析而非简单总结。确保你的分析: +- 包含具体的事实、数据和例子 +- 引用搜索结果中的具体信息源 +- 区分已确认的事实和推测性内容 +- 指出信息的可靠性和局限性 + +研究迭代数据: +${iterationsData}` + + // 确保内容不为空 + if (!iterationsData || iterationsData.trim() === '') { + return `没有足够的数据来生成关于"${mainQuery}"的研究总结。` + } + + // 限制输入token数量 + let trimmedData = iterationsData + const maxInputTokens = this.analysisConfig.maxInputTokens || 200000 + const inputTokens = this.estimateTokens(prompt + trimmedData) + + if (inputTokens > maxInputTokens) { + console.log(`[DeepResearch] 总结输入超过最大token限制(${inputTokens} > ${maxInputTokens}),进行裁剪`) + const ratio = maxInputTokens / inputTokens + const contentTokens = this.estimateTokens(trimmedData) + const targetContentTokens = Math.floor(contentTokens * ratio) - 1000 + + const contentLines = trimmedData.split('\n') + let currentTokens = 0 + const truncatedLines: string[] = [] + + for (const line of contentLines) { + const lineTokens = this.estimateTokens(line) + if (currentTokens + lineTokens <= targetContentTokens) { + truncatedLines.push(line) + currentTokens += lineTokens + } else { + break + } + } + + trimmedData = truncatedLines.join('\n') + console.log(`[DeepResearch] 总结内容已裁剪至约 ${this.estimateTokens(trimmedData)} tokens`) + } + + const summary = await fetchGenerate({ + prompt, + content: trimmedData || ' ', + modelId: this.analysisConfig.modelId + }) + + // 更新token统计 + if (report?.tokenUsage) { + report.tokenUsage.inputTokens += this.estimateTokens(prompt + trimmedData) + report.tokenUsage.outputTokens += this.estimateTokens(summary || '') + report.tokenUsage.totalTokens = report.tokenUsage.inputTokens + report.tokenUsage.outputTokens + } + + return summary || `无法生成关于 "${mainQuery}" 的研究总结。` + } catch (error: any) { + console.error('[DeepResearch] 生成研究总结失败:', error) + return `生成研究总结时出错: ${error?.message || '未知错误'}` + } + } + + /** + * 生成对原始问题的直接回答 + * @param originalQuery 原始查询 + * @param summary 研究总结 + * @param keyInsights 关键见解 + * @returns 直接回答文本 + */ + private async generateDirectAnswer( + originalQuery: string, + summary: string, + keyInsights: string[], + report?: ResearchReport + ): Promise { + try { + console.log(`[DeepResearch] 正在生成对原始问题的直接回答`) + + // 使用模型生成直接回答 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const prompt = `你是一个专业的问题解答专家,负责提供清晰、准确、全面的回答。 + +用户原始问题: "${originalQuery}" + +基于以下研究总结和关键见解,请直接回答用户的问题。你的回答应该: + +1. 直接回应用户的原始问题,而不是提供一般性的信息 +2. 简洁明确,但包含充分的细节和深度 +3. 如果问题有多个方面,请全面考虑每个方面 +4. 如果有不同观点,请客观地呈现各种观点 +5. 如果问题有具体答案,请直接提供答案 +6. 如果问题没有单一答案,请提供最佳建议或解决方案 + +请在回答中使用二级标题和项目符号来组织内容,使其易于阅读。回答应在500-1000字左右,不要过长。 + +研究总结: +${summary} + +关键见解: +${keyInsights.join('\n')}` + + // 确保内容不为空 + if (!summary || summary.trim() === '') { + return `没有足够的数据来生成关于"${originalQuery}"的直接回答。` + } + + const directAnswer = await fetchGenerate({ + prompt, + content: ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + // 更新token统计 + if (report?.tokenUsage) { + report.tokenUsage.inputTokens += this.estimateTokens(prompt) + report.tokenUsage.outputTokens += this.estimateTokens(directAnswer || '') + report.tokenUsage.totalTokens = report.tokenUsage.inputTokens + report.tokenUsage.outputTokens + } + + return directAnswer || `无法生成关于 "${originalQuery}" 的直接回答。` + } catch (error: any) { + console.error('[DeepResearch] 生成直接回答失败:', error) + return `生成直接回答时出错: ${error?.message || '未知错误'}` + } + } + + /** + * 提取关键见解 + * @param iterations 所有迭代 + * @returns 关键见解列表 + */ + private async extractKeyInsights(iterations: ResearchIteration[], report?: ResearchReport): Promise { + if (iterations.length === 0) { + return ['没有足够的研究数据来提取关键见解。'] + } + + try { + const mainQuery = iterations[0].query + + // 收集所有迭代的分析 + const allAnalyses = iterations.map((iter) => iter.analysis).join('\n\n') + + // 使用模型提取关键见解 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const prompt = `你是一个高级研究分析师,负责提取全面、深入的关键见解。 +请从以下关于"${mainQuery}"的研究分析中,提取 10-20 条最重要的关键见解。 + +这些见解应该是研究中最有价值、最有洞察力的发现,能够帮助读者全面理解该主题的核心要点。确保见解涵盖不同的角度和方面,包括: + +- 核心概念和定义 +- 历史背景和发展脉络 +- 技术细节和实现方法 +- 争议点和不同观点 +- 最新进展和突破 +- 实际应用和案例 +- 挑战和限制 +- 未来趋势和发展方向 + +请仅返回见解列表,每行一条,不要添加编号或其他标记。确保每条见解简洁、清晰、有洞察力。 + +研究分析内容: +${allAnalyses}` + + // 确保内容不为空 + if (!allAnalyses || allAnalyses.trim() === '') { + return [`关于${mainQuery}的研究数据不足,无法提取有意义的见解。`, `需要更多的搜索结果来全面分析${mainQuery}。`] + } + + // 限制输入token数量 + let trimmedAnalyses = allAnalyses + const maxInputTokens = this.analysisConfig.maxInputTokens || 200000 + const inputTokens = this.estimateTokens(prompt + trimmedAnalyses) + + if (inputTokens > maxInputTokens) { + console.log(`[DeepResearch] 关键见解输入超过最大token限制(${inputTokens} > ${maxInputTokens}),进行裁剪`) + const ratio = maxInputTokens / inputTokens + const contentTokens = this.estimateTokens(trimmedAnalyses) + const targetContentTokens = Math.floor(contentTokens * ratio) - 1000 + + const contentLines = trimmedAnalyses.split('\n') + let currentTokens = 0 + const truncatedLines: string[] = [] + + for (const line of contentLines) { + const lineTokens = this.estimateTokens(line) + if (currentTokens + lineTokens <= targetContentTokens) { + truncatedLines.push(line) + currentTokens += lineTokens + } else { + break + } + } + + trimmedAnalyses = truncatedLines.join('\n') + console.log(`[DeepResearch] 关键见解内容已裁剪至约 ${this.estimateTokens(trimmedAnalyses)} tokens`) + } + + const result = await fetchGenerate({ + prompt, + content: trimmedAnalyses || ' ', + modelId: this.analysisConfig.modelId + }) + + // 更新token统计 + if (report?.tokenUsage) { + report.tokenUsage.inputTokens += this.estimateTokens(prompt + trimmedAnalyses) + report.tokenUsage.outputTokens += this.estimateTokens(result || '') + report.tokenUsage.totalTokens = report.tokenUsage.inputTokens + report.tokenUsage.outputTokens + } + + if (!result) { + return [ + `${mainQuery}在多个领域都有重要应用。`, + `关于${mainQuery}的研究在近年来呈上升趋势。`, + `${mainQuery}的最佳实践尚未达成共识。`, + `${mainQuery}的未来发展前景广阔。` + ] + } + + // 处理生成的见解 + return result + .split('\n') + .map((insight) => insight.trim()) + .filter((insight) => insight.length > 0) + } catch (error: any) { + console.error('[DeepResearch] 提取关键见解失败:', error) + return [ + `${iterations[0].query}在多个领域都有重要应用。`, + `关于${iterations[0].query}的研究在近年来呈上升趋势。`, + `${iterations[0].query}的最佳实践尚未达成共识。`, + `${iterations[0].query}的未来发展前景广阔。` + ] + } + } + + /** + * 估算文本的token数量 + * @param text 要估算的文本 + * @returns 估算的token数量 + */ + private estimateTokens(text: string): number { + // 简单估算:英文大约每4个字符为1个token,中文大约每1.5个字符为1个token + const englishChars = text.replace(/[\u4e00-\u9fa5]/g, '').length + const chineseChars = text.length - englishChars + + return Math.ceil(englishChars / 4 + chineseChars / 1.5) + } + + /** + * 设置分析配置 + * @param config 配置对象 + */ + public setAnalysisConfig(config: Partial): void { + this.analysisConfig = { + ...this.analysisConfig, + ...config + } + } +} + +export default DeepResearchProvider diff --git a/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts index b9e139cc73..93f6ea0e94 100644 --- a/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts @@ -15,16 +15,136 @@ import BaseWebSearchProvider from './BaseWebSearchProvider' export default class DeepSearchProvider extends BaseWebSearchProvider { // 定义默认的搜索引擎URLs private searchEngines = [ - { name: 'Baidu', url: 'https://www.baidu.com/s?wd=%s' }, - { name: 'Bing', url: 'https://cn.bing.com/search?q=%s&ensearch=1' }, - { name: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=%s&t=h_' }, - { name: 'Sogou', url: 'https://www.sogou.com/web?query=%s' }, + // 中文搜索引擎 + { name: 'Baidu', url: 'https://www.baidu.com/s?wd=%s', category: 'chinese' }, + { name: 'Sogou', url: 'https://www.sogou.com/web?query=%s', category: 'chinese' }, + { name: '360', url: 'https://www.so.com/s?q=%s', category: 'chinese' }, + { name: 'Yisou', url: 'https://yisou.com/search?q=%s', category: 'chinese' }, + + // 国际搜索引擎 + { name: 'Bing', url: 'https://cn.bing.com/search?q=%s&ensearch=1', category: 'international' }, + { name: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=%s&t=h_', category: 'international' }, + { name: 'Brave', url: 'https://search.brave.com/search?q=%s', category: 'international' }, + { name: 'Qwant', url: 'https://www.qwant.com/?q=%s', category: 'international' }, + + // 元搜索引擎 { name: 'SearX', - url: 'https://searx.tiekoetter.com/search?q=%s&categories=general&language=auto&time_range=&safesearch=0&theme=simple' - } + url: 'https://searx.tiekoetter.com/search?q=%s&categories=general&language=auto', + category: 'meta' + }, + { name: 'Ecosia', url: 'https://www.ecosia.org/search?q=%s', category: 'meta' }, + { name: 'Startpage', url: 'https://www.startpage.com/do/search?q=%s', category: 'meta' }, + { name: 'Mojeek', url: 'https://www.mojeek.com/search?q=%s', category: 'meta' }, + + // 学术搜索引擎 + { name: 'Scholar', url: 'https://scholar.google.com/scholar?q=%s', category: 'academic' }, + { name: 'Semantic', url: 'https://www.semanticscholar.org/search?q=%s', category: 'academic' }, + { name: 'BASE', url: 'https://www.base-search.net/Search/Results?lookfor=%s', category: 'academic' }, + { name: 'CNKI', url: 'https://kns.cnki.net/kns/brief/Default_Result.aspx?code=SCDB&kw=%s', category: 'academic' } ] + // 定义URL过滤规则 + private urlFilters = { + // 排除的域名(增强版) + excludedDomains: [ + // 账户相关 + 'login', + 'signin', + 'signup', + 'register', + 'account', + 'checkout', + + // 广告相关 + 'ads', + 'ad.', + 'adv.', + 'advertisement', + 'sponsor', + 'tracking', + 'promotion', + 'marketing', + 'banner', + 'popup', + + // 购物相关 + 'cart', + 'shop', + 'store', + 'buy', + 'price', + 'deal', + 'coupon', + 'discount', + + // 社交媒体评论区 + 'comment', + 'comments', + 'forum', + 'bbs' + ], + // 优先的域名(相关性更高) + priorityDomains: [ + 'github.com/augment', + 'augmentcode.com', + 'augment.dev', + 'github.com', + 'stackoverflow.com', + 'dev.to', + 'medium.com', + 'docs.github.com', + 'npmjs.com', + 'pypi.org', + 'microsoft.com/en-us/learn', + 'developer.mozilla.org', + 'w3schools.com', + 'reactjs.org', + 'vuejs.org', + 'angular.io', + 'tensorflow.org', + 'pytorch.org', + 'kubernetes.io', + 'docker.com', + 'aws.amazon.com/documentation', + 'cloud.google.com/docs', + 'azure.microsoft.com/en-us/documentation' + ], + // 排除的文件类型 + excludedFileTypes: [ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.bmp', + '.svg', + '.webp', + '.mp3', + '.mp4', + '.avi', + '.mov', + '.wmv', + '.flv', + '.wav', + '.ogg', + '.zip', + '.rar', + '.7z', + '.tar', + '.gz', + '.exe', + '.dmg', + '.apk', + '.pdf', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.ppt', + '.pptx' + ] + } + // 分析模型配置 private analyzeConfig = { enabled: true, // 是否启用预分析 @@ -37,7 +157,7 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { // 不再强制要求provider.url,因为我们有默认的搜索引擎 } - public async search(query: string, websearch: WebSearchState): Promise { + public async search(query: string, websearch: WebSearchState, engineCategory?: string): Promise { try { if (!query.trim()) { throw new Error('Search query cannot be empty') @@ -49,8 +169,58 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { // 存储所有搜索引擎的结果 const allItems: Array<{ title: string; url: string; source: string }> = [] - // 并行搜索所有引擎 - const searchPromises = this.searchEngines.map(async (engine) => { + // 根据类别筛选搜索引擎 + let enginesToUse = this.searchEngines + + // 如果是 DeepResearch 调用(指定了类别),则按类别筛选搜索引擎 + if (engineCategory) { + enginesToUse = this.searchEngines.filter((engine) => engine.category === engineCategory) + // 如果该类别没有搜索引擎,则使用所有搜索引擎 + if (enginesToUse.length === 0) { + enginesToUse = this.searchEngines + } + } + // 如果是普通 DeepSearch 调用(没有指定类别),则根据用户配置筛选搜索引擎 + else if (websearch.deepSearchConfig?.enabledEngines) { + const enabledEngines = websearch.deepSearchConfig.enabledEngines + enginesToUse = this.searchEngines.filter((engine) => { + // 中文搜索引擎 + if (engine.name === 'Baidu' && enabledEngines.baidu === false) return false + if (engine.name === 'Sogou' && enabledEngines.sogou === false) return false + if (engine.name === '360' && enabledEngines['360'] === false) return false + if (engine.name === 'Yisou' && enabledEngines.yisou === false) return false + + // 国际搜索引擎 + if (engine.name === 'Bing' && enabledEngines.bing === false) return false + if (engine.name === 'DuckDuckGo' && enabledEngines.duckduckgo === false) return false + if (engine.name === 'Brave' && enabledEngines.brave === false) return false + if (engine.name === 'Qwant' && enabledEngines.qwant === false) return false + + // 元搜索引擎 + if (engine.name === 'SearX' && enabledEngines.searx === false) return false + if (engine.name === 'Ecosia' && enabledEngines.ecosia === false) return false + if (engine.name === 'Startpage' && enabledEngines.startpage === false) return false + if (engine.name === 'Mojeek' && enabledEngines.mojeek === false) return false + + // 学术搜索引擎 + if (engine.name === 'Scholar' && enabledEngines.scholar === false) return false + if (engine.name === 'Semantic' && enabledEngines.semantic === false) return false + if (engine.name === 'BASE' && enabledEngines.base === false) return false + if (engine.name === 'CNKI' && enabledEngines.cnki === false) return false + + return true + }) + + // 如果没有启用任何搜索引擎,则至少使用百度 + if (enginesToUse.length === 0) { + enginesToUse = this.searchEngines.filter((engine) => engine.name === 'Baidu') + } + } + + console.log(`[DeepSearch] 使用${engineCategory || '所有'}类别的搜索引擎,共 ${enginesToUse.length} 个`) + + // 并行搜索选定的引擎 + const searchPromises = enginesToUse.map(async (engine) => { try { const uid = `deep-search-${engine.name.toLowerCase()}-${nanoid()}` const url = engine.url.replace('%s', encodeURIComponent(cleanedQuery)) @@ -185,6 +355,93 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { } } + /** + * 检测内容是否包含乱码或无意义内容 + * @param content 要检测的内容 + * @returns 如果包含乱码或无意义内容返回true,否则返回false + */ + private containsGarbage(content: string): boolean { + if (!content) return true + + // 检测内容长度 + if (content.length < 50) return true // 内容过短 + + // 检测乱码字符比例 + // 使用安全的方式检测乱码字符 + // 手动检测不可打印字符而不使用正则表达式 + let nonReadableCharsCount = 0 + for (let i = 0; i < content.length; i++) { + const charCode = content.charCodeAt(i) + // 检测控制字符和特殊字符 + if ( + (charCode >= 0 && charCode <= 8) || + charCode === 11 || + charCode === 12 || + (charCode >= 14 && charCode <= 31) || + (charCode >= 127 && charCode <= 159) || + charCode === 0xfffd || + charCode === 0xfffe || + charCode === 0xffff + ) { + nonReadableCharsCount++ + } + } + if (nonReadableCharsCount > content.length * 0.05) { + return true // 乱码字符超过5% + } + + // 检测重复模式 + const repeatedPatterns = content.match(/(.{10,})\1{3,}/g) + if (repeatedPatterns && repeatedPatterns.length > 0) { + return true // 存在多次重复的长模式 + } + + // 检测广告关键词 + const adKeywords = [ + 'advertisement', + 'sponsored', + 'promotion', + 'discount', + 'sale', + 'buy now', + 'limited time', + 'special offer', + 'click here', + 'best price', + 'free shipping', + '广告', + '促销', + '特惠', + '打折', + '限时', + '点击购买', + '立即购买' + ] + + const contentLower = content.toLowerCase() + const adKeywordCount = adKeywords.filter((keyword) => contentLower.includes(keyword)).length + + if (adKeywordCount >= 3) { + return true // 包含多个广告关键词 + } + + // 检测内容多样性(字符类型比例) + const letters = content.match(/[a-zA-Z]/g)?.length || 0 + const digits = content.match(/\d/g)?.length || 0 + const spaces = content.match(/\s/g)?.length || 0 + const punctuation = content.match(/[.,;:!?]/g)?.length || 0 + + // 如果内容几乎只有一种字符类型,可能是乱码 + const totalChars = content.length + const mainCharType = Math.max(letters, digits, spaces, punctuation) + + if (mainCharType / totalChars > 0.9) { + return true // 单一字符类型超过90% + } + + return false + } + /** * 分析搜索结果,提取摘要和关键词 * @param results 搜索结果 @@ -198,56 +455,306 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const batchSize = this.analyzeConfig.batchSize const analyzedResults: AnalyzedResult[] = [...results] // 复制原始结果 - // 简单的分析逻辑:提取前几句作为摘要 + // 预处理查询,提取重要关键词 + const queryWords = query.toLowerCase().split(/\s+/) + + // 过滤掉常见的停用词 + const stopWords = new Set([ + 'a', + 'an', + 'the', + 'and', + 'or', + 'but', + 'is', + 'are', + 'was', + 'were', + 'be', + 'been', + 'being', + 'in', + 'on', + 'at', + 'to', + 'for', + 'with', + 'by', + 'about', + 'against', + 'between', + 'into', + 'through', + 'during', + 'before', + 'after', + 'above', + 'below', + 'from', + 'up', + 'down', + 'of', + 'off', + 'over', + 'under', + 'again', + 'further', + 'then', + 'once', + 'here', + 'there', + 'when', + 'where', + 'why', + 'how', + 'all', + 'any', + 'both', + 'each', + 'few', + 'more', + 'most', + 'other', + 'some', + 'such', + 'no', + 'nor', + 'not', + 'only', + 'own', + 'same', + 'so', + 'than', + 'too', + 'very', + 'can', + 'will', + 'just', + 'should', + 'now' + ]) + + // 提取重要关键词,并为每个词分配权重 + const keywordWeights = new Map() + queryWords.forEach((word, index) => { + if (word.length > 2 && !stopWords.has(word)) { + // 根据词的位置分配权重,前面的词权重更高 + const positionWeight = 1 - (index / queryWords.length) * 0.5 // 位置权重范围:0.5-1.0 + // 根据词的长度分配权重,更长的词权重更高 + const lengthWeight = Math.min(1, word.length / 10) // 长度权重最高为1 + // 组合权重 + const weight = positionWeight * 0.7 + lengthWeight * 0.3 + keywordWeights.set(word, weight) + } + }) + + // 如果没有提取到关键词,使用原始查询词 + if (keywordWeights.size === 0) { + queryWords.forEach((word) => { + if (word.length > 2) { + keywordWeights.set(word, 1.0) + } + }) + } + + // 分析每个结果 for (let i = 0; i < results.length; i++) { const result = results[i] if (result.content === noContent) continue try { - // 提取摘要(简单实现,取前300个字符) + // 提取摘要(改进实现,尝试找到包含关键词的最相关段落) const maxLength = this.analyzeConfig.maxSummaryLength - let summary = result.content.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim() + const content = result.content.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim() - if (summary.length > maxLength) { - // 截取到最后一个完整的句子 - summary = summary.substring(0, maxLength) + // 将内容分成句子 + const sentences = content.split(/(?<=[.!?])\s+/) + + // 为每个句子评分 + const sentenceScores = sentences.map((sentence) => { + const sentenceLower = sentence.toLowerCase() + let score = 0 + + // 根据句子中包含的关键词计算分数 + for (const [keyword, weight] of keywordWeights.entries()) { + if (sentenceLower.includes(keyword)) { + score += weight + + // 如果关键词在句子开头,给予额外加分 + if (sentenceLower.indexOf(keyword) < 20) { + score += weight * 0.5 + } + } + } + + return { sentence, score } + }) + + // 按分数排序句子 + sentenceScores.sort((a, b) => b.score - a.score) + + // 选择最相关的句子作为摘要 + let summary = '' + let currentLength = 0 + + // 首先添加得分最高的句子 + if (sentenceScores.length > 0 && sentenceScores[0].score > 0) { + summary = sentenceScores[0].sentence + currentLength = summary.length + } + + // 如果还有空间,添加更多相关句子 + for (let j = 1; j < sentenceScores.length && currentLength < maxLength; j++) { + if (sentenceScores[j].score > 0) { + const nextSentence = sentenceScores[j].sentence + if (currentLength + nextSentence.length + 1 <= maxLength) { + summary += ' ' + nextSentence + currentLength += nextSentence.length + 1 + } + } + } + + // 如果没有找到相关句子,回退到简单摘要提取 + if (summary.length === 0) { + summary = content.substring(0, maxLength) const lastPeriod = summary.lastIndexOf('.') if (lastPeriod > maxLength * 0.7) { - // 至少要有总长度的70% summary = summary.substring(0, lastPeriod + 1) } summary += '...' + } else if (summary.length < content.length) { + summary += '...' } - // 提取关键词(简单实现,基于查询词拆分) - const keywords = query - .split(/\s+/) - .filter((word) => word.length > 2 && result.content.toLowerCase().includes(word.toLowerCase())) + // 提取关键词(改进实现) + const contentLower = result.content.toLowerCase() + const keywordScores = new Map() - // 计算相关性评分(简单实现,基于关键词出现频率) - let relevanceScore = 0 - if (keywords.length > 0) { - const contentLower = result.content.toLowerCase() - for (const word of keywords) { - const wordLower = word.toLowerCase() - // 计算关键词出现的次数 - let count = 0 - let pos = contentLower.indexOf(wordLower) - while (pos !== -1) { - count++ - pos = contentLower.indexOf(wordLower, pos + 1) - } - relevanceScore += count + // 从内容中提取潜在关键词 + const contentWords = contentLower.split(/\W+/).filter((word) => word.length > 3 && !stopWords.has(word)) + + // 计算词频 + const wordFrequency = new Map() + contentWords.forEach((word) => { + wordFrequency.set(word, (wordFrequency.get(word) || 0) + 1) + }) + + // 为每个词评分 + for (const [word, freq] of wordFrequency.entries()) { + // 基础分数是词频 + let score = freq + + // 如果是查询关键词,增加分数 + if (keywordWeights.has(word)) { + score += freq * keywordWeights.get(word)! * 3 } - // 标准化评分,范围为0-1 - relevanceScore = Math.min(1, relevanceScore / (contentLower.length / 100)) + + // 如果在标题中出现,增加分数 + if (result.title.toLowerCase().includes(word)) { + score += 5 + } + + keywordScores.set(word, score) } + // 添加查询关键词(如果内容中包含) + for (const [keyword, weight] of keywordWeights.entries()) { + if (contentLower.includes(keyword) && !keywordScores.has(keyword)) { + keywordScores.set(keyword, weight * 3) + } + } + + // 选择得分最高的关键词 + const sortedKeywords = Array.from(keywordScores.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map((entry) => entry[0]) + + // 计算相关性评分(改进实现) + let relevanceScore = 0 + + // 1. 基于关键词匹配度的评分 + let keywordMatchScore = 0 + + for (const [keyword, weight] of keywordWeights.entries()) { + // 计算关键词出现的次数 + let count = 0 + let pos = contentLower.indexOf(keyword) + while (pos !== -1) { + count++ + pos = contentLower.indexOf(keyword, pos + 1) + } + + if (count > 0) { + // 权重 * 出现次数 * 归一化因子 + keywordMatchScore += (weight * Math.min(10, count)) / 10 + } + } + + // 归一化关键词匹配分数 + if (keywordWeights.size > 0) { + keywordMatchScore = keywordMatchScore / keywordWeights.size + } + + // 2. 基于标题相关性的评分 + let titleScore = 0 + const titleLower = result.title.toLowerCase() + + for (const [keyword, weight] of keywordWeights.entries()) { + if (titleLower.includes(keyword)) { + titleScore += weight + } + } + + // 归一化标题分数 + if (keywordWeights.size > 0) { + titleScore = titleScore / keywordWeights.size + } + + // 3. 基于内容长度的评分(适中长度最佳) + const contentLength = result.content.length + const lengthScore = Math.min(1, contentLength / 2000) * (contentLength < 50000 ? 1 : 0.5) + + // 4. 基于URL的评分(官方网站、知名网站加分) + let urlScore = 0 + const url = result.url.toLowerCase() + + // 首先检查是否有预先计算的优先级分数 + if (result.meta && result.meta.priorityScore) { + // 使用预先计算的分数 + urlScore = result.meta.priorityScore + } else { + // 如果没有预先计算的分数,使用基于域名的评分 + // 检查是否是官方网站或知名网站 + if (url.includes('github.com/augment') || url.includes('augmentcode.com') || url.includes('augment.dev')) { + urlScore = 1.0 // 官方网站最高分 + } else if ( + url.includes('github.com') || + url.includes('stackoverflow.com') || + url.includes('medium.com') || + url.includes('dev.to') + ) { + urlScore = 0.8 // 知名技术网站高分 + } else if (!url.includes('login') && !url.includes('signup') && !url.includes('register')) { + urlScore = 0.5 // 普通网站中等分 + } + } + + // 组合所有评分因素,调整权重以提高URL质量的重要性 + relevanceScore = + keywordMatchScore * 0.4 + // 关键词匹配度占40% + titleScore * 0.3 + // 标题相关性占30% + lengthScore * 0.05 + // 内容长度占5% + urlScore * 0.25 // URL质量占25%,增加了权重 + + // 确保分数在0-1范围内 + relevanceScore = Math.min(1, Math.max(0, relevanceScore)) + // 更新分析结果 analyzedResults[i] = { ...analyzedResults[i], summary, - keywords, + keywords: sortedKeywords, relevanceScore } @@ -267,16 +774,31 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { return scoreB - scoreA }) - console.log(`[DeepSearch] 完成分析 ${results.length} 个结果`) - return analyzedResults + // 过滤掉明显不相关的结果和乱码内容 + const filteredResults = analyzedResults.filter((result) => { + // 检查相关性分数 + const score = (result as AnalyzedResult).relevanceScore || 0 + if (score <= 0.05) return false // 相关性分数过低 + + // 检查是否包含乱码或广告 + if (this.containsGarbage(result.content)) { + console.log(`[DeepSearch] 过滤乱码或广告内容: ${result.title}`) + return false + } + + return true + }) + + console.log(`[DeepSearch] 完成分析 ${results.length} 个结果,过滤后剩余 ${filteredResults.length} 个结果`) + return filteredResults } /** * 解析搜索结果页面中的URL * 默认实现,子类可以覆盖此方法以适应不同的搜索引擎 */ - protected parseValidUrls(htmlContent: string): Array<{ title: string; url: string }> { - const results: Array<{ title: string; url: string }> = [] + protected parseValidUrls(htmlContent: string): Array<{ title: string; url: string; meta?: Record }> { + const results: Array<{ title: string; url: string; meta?: Record }> = [] try { // 通用解析逻辑,查找所有链接 @@ -288,7 +810,11 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { ...doc.querySelectorAll('#content_left .result h3 a'), ...doc.querySelectorAll('#content_left .c-container h3 a'), ...doc.querySelectorAll('#content_left .c-container a.c-title'), - ...doc.querySelectorAll('#content_left a[data-click]') + ...doc.querySelectorAll('#content_left a[data-click]'), + // 添加更多选择器来适应百度的变化 + ...doc.querySelectorAll('#content_left .result-op a[href*="http"]'), + ...doc.querySelectorAll('#content_left .c-container .t a'), + ...doc.querySelectorAll('#content_left .c-container .c-title-text') ] // 尝试解析Bing搜索结果 - 使用多个选择器来获取更多结果 @@ -296,7 +822,11 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { ...doc.querySelectorAll('.b_algo h2 a'), ...doc.querySelectorAll('.b_algo a.tilk'), ...doc.querySelectorAll('.b_algo a.b_title'), - ...doc.querySelectorAll('.b_results a.b_restorLink') + ...doc.querySelectorAll('.b_results a.b_restorLink'), + // 添加更多选择器来适应Bing的变化 + ...doc.querySelectorAll('.b_algo .b_caption a'), + ...doc.querySelectorAll('.b_algo .b_attribution cite'), + ...doc.querySelectorAll('.b_algo .b_deep a') ] // 尝试解析DuckDuckGo搜索结果 - 使用多个选择器来获取更多结果 @@ -368,19 +898,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉搜索引擎内部链接和重复链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !uniqueUrls.has(url) - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -399,19 +927,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉搜索引擎内部链接和重复链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !uniqueUrls.has(url) - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -430,21 +956,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉搜索引擎内部链接和重复链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !url.includes('sogou.com/web') && - !url.includes('duckduckgo.com/?q=') && - !uniqueUrls.has(url) - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -465,21 +987,25 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 更宽松的过滤条件 + // 使用过滤方法检查URL,但使用更宽松的条件 if ( url && (url.startsWith('http') || url.startsWith('https')) && - !url.includes('sogou.com/web') && + !url.includes('sogou.com/web') && // 仍然过滤掉搜索引擎内部链接 !url.includes('javascript:') && !url.includes('mailto:') && !url.includes('tel:') && !uniqueUrls.has(url) && title.trim().length > 0 ) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + meta: { priorityScore } }) } } catch (error) { @@ -501,22 +1027,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉搜索引擎内部链接和重复链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !url.includes('sogou.com/web') && - !url.includes('duckduckgo.com/?q=') && - !url.includes('searx.tiekoetter.com/search') && - !uniqueUrls.has(url) - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -547,10 +1068,14 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { !uniqueUrls.has(url) && title.trim().length > 0 ) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + meta: { priorityScore } }) } } catch (error) { @@ -600,10 +1125,14 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { !uniqueUrls.has(url) && title.trim().length > 0 ) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + meta: { priorityScore } }) } } catch (error) { @@ -617,20 +1146,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉搜索引擎内部链接和重复链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !url.includes('duckduckgo.com/?q=') && - !uniqueUrls.has(url) - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim() || url, - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -661,10 +1187,14 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { !url.includes('searx.tiekoetter.com/search') && !uniqueUrls.has(url) ) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: url, - url: url + url: url, + meta: { priorityScore } }) } } @@ -684,31 +1214,17 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const url = (link as HTMLAnchorElement).href const title = link.textContent || url - // 过滤掉无效链接和搜索引擎内部链接 - if ( - url && - (url.startsWith('http') || url.startsWith('https')) && - !url.includes('google.com/search') && - !url.includes('bing.com/search') && - !url.includes('baidu.com/s?') && - !url.includes('duckduckgo.com/?q=') && - !url.includes('sogou.com/web') && - !url.includes('searx.tiekoetter.com/search') && - !uniqueUrls.has(url) && - // 过滤掉常见的无用链接 - !url.includes('javascript:') && - !url.includes('mailto:') && - !url.includes('tel:') && - !url.includes('login') && - !url.includes('register') && - !url.includes('signup') && - !url.includes('signin') && - title.trim().length > 0 - ) { + // 使用过滤方法检查URL + if (url && !this.shouldFilterUrl(url) && !uniqueUrls.has(url) && title.trim().length > 0) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + uniqueUrls.add(url) results.push({ title: title.trim(), - url: url + url: url, + // 将优先级分数作为元数据保存 + meta: { priorityScore } }) } } catch (error) { @@ -725,6 +1241,81 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { return results } + /** + * 检查URL是否应该被过滤掉 + * @param url 要检查的URL + * @returns 如果应该过滤掉返回true,否则返回false + */ + private shouldFilterUrl(url: string): boolean { + if (!url || !url.startsWith('http')) { + return true + } + + const urlLower = url.toLowerCase() + + // 检查是否是搜索引擎内部链接 + if ( + urlLower.includes('google.com/search') || + urlLower.includes('bing.com/search') || + urlLower.includes('baidu.com/s?') || + urlLower.includes('sogou.com/web') || + urlLower.includes('duckduckgo.com/?q=') || + urlLower.includes('searx.tiekoetter.com/search') + ) { + return true + } + + // 检查是否包含排除的域名 + for (const domain of this.urlFilters.excludedDomains) { + if (urlLower.includes(domain)) { + return true + } + } + + // 检查是否是排除的文件类型 + for (const fileType of this.urlFilters.excludedFileTypes) { + if (urlLower.endsWith(fileType)) { + return true + } + } + + // 检查是否是常见的无用链接 + if ( + urlLower.includes('javascript:') || + urlLower.includes('mailto:') || + urlLower.includes('tel:') || + urlLower.includes('about:') || + urlLower.includes('chrome:') || + urlLower.includes('file:') + ) { + return true + } + + return false + } + + /** + * 计算URL的优先级分数 + * @param url 要计算的URL + * @returns 优先级分数,范围从0到1,越高越优先 + */ + private getUrlPriorityScore(url: string): number { + if (!url) return 0 + + const urlLower = url.toLowerCase() + + // 检查是否是优先域名 + for (let i = 0; i < this.urlFilters.priorityDomains.length; i++) { + const domain = this.urlFilters.priorityDomains[i] + if (urlLower.includes(domain)) { + // 根据域名在数组中的位置计算分数,前面的域名分数更高 + return 1 - (i / this.urlFilters.priorityDomains.length) * 0.5 + } + } + + return 0 + } + /** * 深度抓取内容 * 不仅抓取搜索结果页面,还会抓取页面中的链接 @@ -848,8 +1439,13 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { while ((match = markdownLinkRegex.exec(markdown)) !== null) { const url = match[2] - if (url && (url.startsWith('http') || url.startsWith('https'))) { - urls.add(url) + if (url && (url.startsWith('http') || url.startsWith('https')) && !this.shouldFilterUrl(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + // 优先级较高的URL更有可能被添加 + if (priorityScore > 0.3 || Math.random() < 0.7) { + urls.add(url) + } } } @@ -857,8 +1453,13 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { const urlRegex = /(https?:\/\/[^\s]+)/g while ((match = urlRegex.exec(markdown)) !== null) { const url = match[1] - if (url) { - urls.add(url) + if (url && !this.shouldFilterUrl(url)) { + // 计算URL优先级分数 + const priorityScore = this.getUrlPriorityScore(url) + // 优先级较高的URL更有可能被添加 + if (priorityScore > 0.3 || Math.random() < 0.5) { + urls.add(url) + } } } diff --git a/src/renderer/src/providers/WebSearchProvider/WebSearchProviderFactory.ts b/src/renderer/src/providers/WebSearchProvider/WebSearchProviderFactory.ts index ebe3c345c3..b5fb34799a 100644 --- a/src/renderer/src/providers/WebSearchProvider/WebSearchProviderFactory.ts +++ b/src/renderer/src/providers/WebSearchProvider/WebSearchProviderFactory.ts @@ -2,6 +2,7 @@ import { WebSearchProvider } from '@renderer/types' import BaseWebSearchProvider from './BaseWebSearchProvider' import DefaultProvider from './DefaultProvider' +import DeepResearchProvider from './DeepResearchProvider' import DeepSearchProvider from './DeepSearchProvider' import ExaProvider from './ExaProvider' import LocalBaiduProvider from './LocalBaiduProvider' @@ -27,6 +28,8 @@ export default class WebSearchProviderFactory { return new LocalBingProvider(provider) case 'deep-search': return new DeepSearchProvider(provider) + case 'deep-research': + return new DeepResearchProvider(provider) default: return new DefaultProvider(provider) } diff --git a/src/renderer/src/router/RouterConfig.tsx b/src/renderer/src/router/RouterConfig.tsx index a1f03a7586..272727b84a 100644 --- a/src/renderer/src/router/RouterConfig.tsx +++ b/src/renderer/src/router/RouterConfig.tsx @@ -1,6 +1,7 @@ import { createHashRouter, HashRouter, Route, Routes } from 'react-router-dom' import AgentsPage from '@renderer/pages/agents/AgentsPage' import AppsPage from '@renderer/pages/apps/AppsPage' +import DeepResearchPage from '@renderer/pages/deepresearch/DeepResearchPage' import FilesPage from '@renderer/pages/files/FilesPage' import HomePage from '@renderer/pages/home/HomePage' import KnowledgePage from '@renderer/pages/knowledge/KnowledgePage' @@ -46,6 +47,10 @@ export const router = createHashRouter( path: '/workspace', element: }, + { + path: '/deepresearch', + element: + }, { path: '/settings/*', element: @@ -80,6 +85,7 @@ export const RouterComponent = ({ children }: { children?: React.ReactNode }) => } /> } /> } /> + } /> } /> {children} diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 4978e3050d..4e2d1e0d5f 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -17,6 +17,7 @@ export type SidebarIcon = | 'files' | 'projects' | 'workspace' + | 'deepresearch' export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'assistants', @@ -26,7 +27,8 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'minapp', 'knowledge', 'files', - 'workspace' + 'workspace', + 'deepresearch' ] export interface NutstoreSyncRuntime extends WebDAVSyncState {} @@ -191,6 +193,12 @@ export interface SettingsState { siyuan: boolean docx: boolean } + // DeepResearch 设置 + enableDeepResearch: boolean + deepResearchShortcut: string + deepResearchMaxDepth: number + deepResearchMaxUrls: number + deepResearchTimeLimit: number } export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid' @@ -344,7 +352,13 @@ export const initialState: SettingsState = { obsidian: true, siyuan: true, docx: true - } + }, + // DeepResearch 设置 + enableDeepResearch: true, + deepResearchShortcut: 'Alt+D', + deepResearchMaxDepth: 3, + deepResearchMaxUrls: 30, + deepResearchTimeLimit: 120 } const settingsSlice = createSlice({ @@ -802,6 +816,22 @@ const settingsSlice = createSlice({ setEnableBackspaceDeleteModel: (state, action: PayloadAction) => { state.enableBackspaceDeleteModel = action.payload }, + // DeepResearch 设置 + setEnableDeepResearch: (state, action: PayloadAction) => { + state.enableDeepResearch = action.payload + }, + setDeepResearchShortcut: (state, action: PayloadAction) => { + state.deepResearchShortcut = action.payload + }, + setDeepResearchMaxDepth: (state, action: PayloadAction) => { + state.deepResearchMaxDepth = action.payload + }, + setDeepResearchMaxUrls: (state, action: PayloadAction) => { + state.deepResearchMaxUrls = action.payload + }, + setDeepResearchTimeLimit: (state, action: PayloadAction) => { + state.deepResearchTimeLimit = action.payload + }, // PDF设置相关的action setPdfSettings: ( state, @@ -942,7 +972,13 @@ export const { setLastPlayedMessageId, setSkipNextAutoTTS, setEnableBackspaceDeleteModel, - setUsePromptForToolCalling + setUsePromptForToolCalling, + // DeepResearch 设置 + setEnableDeepResearch, + setDeepResearchShortcut, + setDeepResearchMaxDepth, + setDeepResearchMaxUrls, + setDeepResearchTimeLimit } = settingsSlice.actions // PDF设置相关的action diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 1f285d384d..566266396a 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -24,6 +24,41 @@ export interface WebSearchState { enhanceMode: boolean // 是否覆盖服务商搜索 overwrite: boolean + // 深度研究配置 + deepResearchConfig?: { + maxIterations?: number + maxResultsPerQuery?: number + autoSummary?: boolean + enableQueryOptimization?: boolean + } + // DeepSearch 配置 + deepSearchConfig?: { + enabledEngines?: { + // 中文搜索引擎 + baidu?: boolean + sogou?: boolean + '360'?: boolean + yisou?: boolean + + // 国际搜索引擎 + bing?: boolean + duckduckgo?: boolean + brave?: boolean + qwant?: boolean + + // 元搜索引擎 + searx?: boolean + ecosia?: boolean + startpage?: boolean + mojeek?: boolean + + // 学术搜索引擎 + scholar?: boolean + semantic?: boolean + base?: boolean + cnki?: boolean + } + } } const initialState: WebSearchState = { @@ -64,6 +99,12 @@ const initialState: WebSearchState = { name: 'DeepSearch (多引擎)', description: '使用Baidu、Bing、DuckDuckGo、搜狗和SearX进行深度搜索', contentLimit: 10000 + }, + { + id: 'deep-research', + name: 'DeepResearch (深度研究)', + description: '使用多轮搜索、分析和总结进行深度研究', + contentLimit: 30000 } ], searchWithTime: true, @@ -71,7 +112,34 @@ const initialState: WebSearchState = { excludeDomains: [], subscribeSources: [], enhanceMode: true, - overwrite: false + overwrite: false, + deepSearchConfig: { + enabledEngines: { + // 中文搜索引擎 + baidu: true, + sogou: true, + '360': false, + yisou: false, + + // 国际搜索引擎 + bing: true, + duckduckgo: true, + brave: false, + qwant: false, + + // 元搜索引擎 + searx: true, + ecosia: false, + startpage: false, + mojeek: false, + + // 学术搜索引擎 + scholar: true, + semantic: false, + base: false, + cnki: false + } + } } export const defaultWebSearchProviders = initialState.providers @@ -145,6 +213,39 @@ const websearchSlice = createSlice({ // Add the new provider to the array state.providers.push(action.payload) } + }, + setDeepResearchConfig: ( + state, + action: PayloadAction<{ + maxIterations?: number + maxResultsPerQuery?: number + autoSummary?: boolean + enableQueryOptimization?: boolean + modelId?: string + }> + ) => { + state.deepResearchConfig = { + ...state.deepResearchConfig, + ...action.payload + } + }, + setDeepSearchConfig: ( + state, + action: PayloadAction<{ + enabledEngines?: { + baidu?: boolean + bing?: boolean + duckduckgo?: boolean + sogou?: boolean + searx?: boolean + scholar?: boolean + } + }> + ) => { + state.deepSearchConfig = { + ...state.deepSearchConfig, + ...action.payload + } } } }) @@ -163,7 +264,9 @@ export const { setSubscribeSources, setEnhanceMode, setOverwrite, - addWebSearchProvider + addWebSearchProvider, + setDeepResearchConfig, + setDeepSearchConfig } = websearchSlice.actions export default websearchSlice.reducer diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index ebcbb25224..3e9b928ae1 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -363,6 +363,8 @@ export type SidebarIcon = | 'knowledge' | 'files' | 'projects' + | 'workspace' + | 'deepresearch' export type WebSearchProvider = { id: string @@ -374,6 +376,7 @@ export type WebSearchProvider = { contentLimit?: number usingBrowser?: boolean description?: string + category?: string } export type WebSearchResponse = { @@ -389,6 +392,31 @@ export type WebSearchResult = { summary?: string keywords?: string[] relevanceScore?: number + meta?: { + priorityScore?: number + [key: string]: any + } +} + +export interface ResearchIteration { + query: string + results: WebSearchResult[] + analysis: string + followUpQueries: string[] +} + +export interface ResearchReport { + originalQuery: string + iterations: ResearchIteration[] + summary: string + directAnswer: string + keyInsights: string[] + sources: string[] + tokenUsage?: { + inputTokens: number + outputTokens: number + totalTokens: number + } } export type KnowledgeReference = { diff --git a/yarn.lock b/yarn.lock index d56c3008f0..6f976b15c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3562,6 +3562,407 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api-logs@npm:0.57.2": + version: 0.57.2 + resolution: "@opentelemetry/api-logs@npm:0.57.2" + dependencies: + "@opentelemetry/api": "npm:^1.3.0" + checksum: 10c0/1e514d3fd4ca68e7e8b008794a95ee0562a5d9e1d3ebb02647b245afaa6c2d72cc14e99e3ea47a1d1007f8a965c62bfb6170e1aa26756230bea063cfde2898bf + languageName: node + linkType: hard + +"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add + languageName: node + linkType: hard + +"@opentelemetry/context-async-hooks@npm:^1.30.1": + version: 1.30.1 + resolution: "@opentelemetry/context-async-hooks@npm:1.30.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/3e8114d360060a5225226d2fcd8df08cd542246003790a7f011c0774bc60b8a931f46f4c6673f3977a7d9bba717de6ee028cae51b752c2567053d7f46ed3eba3 + languageName: node + linkType: hard + +"@opentelemetry/core@npm:1.30.1, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.26.0, @opentelemetry/core@npm:^1.30.1, @opentelemetry/core@npm:^1.8.0": + version: 1.30.1 + resolution: "@opentelemetry/core@npm:1.30.1" + dependencies: + "@opentelemetry/semantic-conventions": "npm:1.28.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/4c25ba50a6137c2ba9ca563fb269378f3c9ca6fd1b3f15dbb6eff78eebf5656f281997cbb7be8e51c01649fd6ad091083fcd8a42dd9b5dfac907dc06d7cfa092 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-amqplib@npm:^0.46.1": + version: 0.46.1 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.46.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/4a8b870ccaa64cfd200663ec14385aca7eeb7146124d82e566f3d48678f237c9a56661ae3401345fe0dce5c56366ae02a312dc7905eb4fd6e073df2cface30fb + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-connect@npm:0.43.1": + version: 0.43.1 + resolution: "@opentelemetry/instrumentation-connect@npm:0.43.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/connect": "npm:3.4.38" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a7e2629fbfa775f2d1a6b2c9387e27809db16177cf6de89159017d7353c270c6c84d81550c58ccc51ea72c2304b1fcb911499440451d8df6954cc1f4e654eb64 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-dataloader@npm:0.16.1": + version: 0.16.1 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.16.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/83bd0267672cc3e8709401e1f107612aed3bb72faedfed76fe25e174b19c41f65d503bc3a666ba0872bbef8c31adcefb8884982f785fa3b0df28eec40b6578aa + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-express@npm:0.47.1": + version: 0.47.1 + resolution: "@opentelemetry/instrumentation-express@npm:0.47.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/eca448eb088857c7c0c7d0a1875b9e20a990b23e2f64355d2e645618d3f5c038efb9d605009a6d8fa1e05243d0ccef14b9aa1effffee693fd071de3cc39ad3d1 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fastify@npm:0.44.2": + version: 0.44.2 + resolution: "@opentelemetry/instrumentation-fastify@npm:0.44.2" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2306bbf9b59e5d29002b9c14493de92817e268c92587b285a0a4bbeece4a3f791db8e0d1be0e22ad9f6a97075071aa5c3269cf2d219e378480dc1ceafbf2927c + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fs@npm:0.19.1": + version: 0.19.1 + resolution: "@opentelemetry/instrumentation-fs@npm:0.19.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8bf714658c0fcc34ba7db4c28af3196690f756a9b4fb6d1b6cab59938a7b5c1e40e834c518b39085e744915c0c384ca6d997a8a97901955732acf3af0cba6e7f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-generic-pool@npm:0.43.1": + version: 0.43.1 + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.43.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bdc95146d1f6f5dcf5922af8161c4954b9feeb505a01c5e61b1246ed67909dc1f6e72ad067839f085a4977e863246e7e4b468c814cf4104f35fcc20fb570eac2 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-graphql@npm:0.47.1": + version: 0.47.1 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.47.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/d5cfeb668b5ea4e4d97d8433c642457ac9f7f2023278a84a183b4c4c2cc43bbae3eac916ff7176ef8492661877560b519663c52eb2fad0a8a1f00718a0449aa6 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-hapi@npm:0.45.2": + version: 0.45.2 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.45.2" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bb491327ce86d8f1f7e2a12621a00dbf921e1fc3e9b64f975fc23e443d92bcd6ef779b34349214871763d459650da219c5e23bb1fdd1bc261fa0f92190521b2e + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-http@npm:0.57.2": + version: 0.57.2 + resolution: "@opentelemetry/instrumentation-http@npm:0.57.2" + dependencies: + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/instrumentation": "npm:0.57.2" + "@opentelemetry/semantic-conventions": "npm:1.28.0" + forwarded-parse: "npm:2.1.2" + semver: "npm:^7.5.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/b95a1b61cddabd32358fa565a4fcf5c17e8340907b171dcdf2a104533c9afdee821efa7b82dabb3123318dcc66272b0a7b8c37c44fc87e593cb8138a7a63fc23 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-ioredis@npm:0.47.1": + version: 0.47.1 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.47.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/redis-common": "npm:^0.36.2" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/ec741778041cecc133a143292d66631c99311bf098db8f03276a48b87fe18826eec4513e4de70bb555ef50268db6520442e9a2f7752f7ea9b5a3e8363fecb8c9 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-kafkajs@npm:0.7.1": + version: 0.7.1 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.7.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/440a479ec65414da706f15b5c2ff82235ea8b11701e811ae235af5e8f01bee7e639223243e9f18550ce55cc94b8cdaa8a72297ded55f1c7993f1d95488c2b02e + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-knex@npm:0.44.1": + version: 0.44.1 + resolution: "@opentelemetry/instrumentation-knex@npm:0.44.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/75dcbda2c412cc448ac95238899d92846bda14bb21a1c9e9bc0c51fd48dcedb6064c2a8ab9e53d112945748d50513ecda13afbc4c0f24a884674d2a485f0efcd + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-koa@npm:0.47.1": + version: 0.47.1 + resolution: "@opentelemetry/instrumentation-koa@npm:0.47.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a1c5433da1265f1f8da3e46ebe085a3ddba3e16f43c5f44bd41082a0839f6bdf9a6a737b80b0d2f2a05d1ef2c23e2b0a4f7e55858bf1e32570b4c150c69135bc + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-lru-memoizer@npm:0.44.1": + version: 0.44.1 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.44.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/5728d0b6ed560ba8426546ab30ef251cbe9f25a130abc8bea0d7635b51cc29fbade4d00c7b1869fa0543fe54891799483fe0f6fb4073d1bf5d12dbdd543aaae5 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongodb@npm:0.52.0": + version: 0.52.0 + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.52.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/dcd072a296369a6b254a809e3708e5f9842ac9f8c61700bfa2014872fa6e6ca65adfa5efdbf9021df57e749dea2cddd828351e73cb581370b8b97693c06df7e8 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongoose@npm:0.46.1": + version: 0.46.1 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.46.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/69378d41df172c2edb8b36042e751936837bb1cbee11ee72a3d1608c6d7f609d79beec2020b25de72086553ad9d85347642c4066e0b4e96d442513b29ac4f0aa + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql2@npm:0.45.2": + version: 0.45.2 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.45.2" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.40.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8ac62064b32facfddd7d47ba0bce9689d2277ba4ef74348655faffe818522c919654c5bf1a5fac211a75f2093fbd588a14cba278c353da2f60d1919d58d419aa + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql@npm:0.45.1": + version: 0.45.1 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.45.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/mysql": "npm:2.15.26" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/c820a6929fe2e010dacb8962d40fdb8c9ac95c265efc74f478eadc021b2a3add9ce8d303c4bda20af01327564f487c9e052e710d9e975d7f17a5918d802d7ae4 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pg@npm:0.51.1": + version: 0.51.1 + resolution: "@opentelemetry/instrumentation-pg@npm:0.51.1" + dependencies: + "@opentelemetry/core": "npm:^1.26.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.40.1" + "@types/pg": "npm:8.6.1" + "@types/pg-pool": "npm:2.0.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fff3dcc092b959601a20c20e19c27d39d6386e6bc2b7014c1be5a5e22c0e275bf9980dad758b1f7824b1448a6178e13938b6bb2da53095f410fbb4d248b5ede6 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-redis-4@npm:0.46.1": + version: 0.46.1 + resolution: "@opentelemetry/instrumentation-redis-4@npm:0.46.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/redis-common": "npm:^0.36.2" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/df0bdd865e254c9b4c0339ce5aabf3698d99b8ab8cf8ea1aa57ffa13620f2193fda247ed43ec4ccc6edadab1ffec5cc263038ab6f3c9e96ef000ee232b9181f8 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-tedious@npm:0.18.1": + version: 0.18.1 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.18.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.57.1" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/tedious": "npm:^4.0.14" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fda9ac4dc89998a2cf739a70f06b1d6eebf98fe22713dc3fbca4a1119dc289d83c91ada4a3cea37f39a34c69978ae21ff9b599c27beaee128879b993677696dc + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-undici@npm:0.10.1": + version: 0.10.1 + resolution: "@opentelemetry/instrumentation-undici@npm:0.10.1" + dependencies: + "@opentelemetry/core": "npm:^1.8.0" + "@opentelemetry/instrumentation": "npm:^0.57.1" + peerDependencies: + "@opentelemetry/api": ^1.7.0 + checksum: 10c0/3958f291d14f2f7bb5e3b957487444ffee449d8ea76c973ca09b9669258d37e98b3797c7167190c5038802529cc6b539bdf6efc7887398ec9b53d3ba51c90bda + languageName: node + linkType: hard + +"@opentelemetry/instrumentation@npm:0.57.2, @opentelemetry/instrumentation@npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0, @opentelemetry/instrumentation@npm:^0.57.1, @opentelemetry/instrumentation@npm:^0.57.2": + version: 0.57.2 + resolution: "@opentelemetry/instrumentation@npm:0.57.2" + dependencies: + "@opentelemetry/api-logs": "npm:0.57.2" + "@types/shimmer": "npm:^1.2.0" + import-in-the-middle: "npm:^1.8.1" + require-in-the-middle: "npm:^7.1.1" + semver: "npm:^7.5.2" + shimmer: "npm:^1.2.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/79ca65b66357665d19f89da7027da25ea1c6b55ecdacb0a99534923743c80deb9282870db563de8ae284b13e7e0aab8413efa1937f199deeaef069e07c7e4875 + languageName: node + linkType: hard + +"@opentelemetry/redis-common@npm:^0.36.2": + version: 0.36.2 + resolution: "@opentelemetry/redis-common@npm:0.36.2" + checksum: 10c0/4cb831628551b9f13dca8d65897e300ff7be0e256b77f455a26fb053bbdfc7997b27d066ab1402ca929e7ac77598e0d593f91762d8af9f798c19ba1524e9d078 + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:1.30.1, @opentelemetry/resources@npm:^1.30.1": + version: 1.30.1 + resolution: "@opentelemetry/resources@npm:1.30.1" + dependencies: + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/semantic-conventions": "npm:1.28.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/688e73258283c80662bfa9a858aaf73bf3b832a18d96e546d0dddfa6dcec556cdfa087a1d0df643435293406009e4122d7fb7eeea69aa87b539d3bab756fba74 + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:^1.30.1": + version: 1.30.1 + resolution: "@opentelemetry/sdk-trace-base@npm:1.30.1" + dependencies: + "@opentelemetry/core": "npm:1.30.1" + "@opentelemetry/resources": "npm:1.30.1" + "@opentelemetry/semantic-conventions": "npm:1.28.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/77019dc3efaeceb41b4c54dd83b92f0ccd81ecceca544cbbe8e0aee4b2c8727724bdb9dcecfe00622c16d60946ae4beb69a5c0e7d85c4bc7ef425bd84f8b970c + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.28.0": + version: 1.28.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.28.0" + checksum: 10c0/deb8a0f744198071e70fea27143cf7c9f7ecb7e4d7b619488c917834ea09b31543c1c2bcea4ec5f3cf68797f0ef3549609c14e859013d9376400ac1499c2b9cb + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.30.0": + version: 1.32.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.32.0" + checksum: 10c0/977c93225490f2456e8bb13b90a8627861207eb5eb4771d7565c2321be883ec711c1701485451f9e10b8d2a724525496c0e4441b43190a7a550bcf7c73f681cd + languageName: node + linkType: hard + +"@opentelemetry/sql-common@npm:^0.40.1": + version: 0.40.1 + resolution: "@opentelemetry/sql-common@npm:0.40.1" + dependencies: + "@opentelemetry/core": "npm:^1.1.0" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + checksum: 10c0/60a70358f0c94f610e2995333e96b406626d67d03d38ed03b15a3461ad0f8d64afbf6275cca7cb58fe955ecdce832f3ffc9b73f9d88503bba5d2a620bbd6d351 + languageName: node + linkType: hard + "@parcel/watcher-android-arm64@npm:2.5.1": version: 2.5.1 resolution: "@parcel/watcher-android-arm64@npm:2.5.1" @@ -3745,6 +4146,17 @@ __metadata: languageName: node linkType: hard +"@prisma/instrumentation@npm:6.5.0": + version: 6.5.0 + resolution: "@prisma/instrumentation@npm:6.5.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + peerDependencies: + "@opentelemetry/api": ^1.8 + checksum: 10c0/b9223ccc437d0f1f82f8ea8ce50befed18d5337b1079f7a652a0c4e6d9645bcae3ccaf124cc90b94d336b47f195f4bf7b1c4513fac3cb07a8952df576c5b891b + languageName: node + linkType: hard + "@rc-component/async-validator@npm:^5.0.3": version: 5.0.4 resolution: "@rc-component/async-validator@npm:5.0.4" @@ -4183,6 +4595,82 @@ __metadata: languageName: node linkType: hard +"@sentry-internal/browser-utils@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry-internal/browser-utils@npm:9.11.0" + dependencies: + "@sentry/core": "npm:9.11.0" + checksum: 10c0/83b163e8590b0bd64b843321c6b1172ce6cf0bdc7d97fc2aa88b57cf0d388d52bead2272b64d6b2657a31c0dbce89d660279812dbfc047dc55d02c1bbffd49d3 + languageName: node + linkType: hard + +"@sentry-internal/browser-utils@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry-internal/browser-utils@npm:9.14.0" + dependencies: + "@sentry/core": "npm:9.14.0" + checksum: 10c0/4c5907b210ba1cc4add98294146348baedcee67c5339d61fc8c8a5e57add478292f72cef8dc82064aa197381fae089926b9de4a9915bc86ac81ea09b3496633f + languageName: node + linkType: hard + +"@sentry-internal/feedback@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry-internal/feedback@npm:9.11.0" + dependencies: + "@sentry/core": "npm:9.11.0" + checksum: 10c0/5b3a091c30b8536b1836089a86721942d49beebc7e77887986c52e58ed49772de51b5376e33278ade1f8c48e66cc9ba0d8b5fd0d9d66dc3ced60028002c8ac50 + languageName: node + linkType: hard + +"@sentry-internal/feedback@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry-internal/feedback@npm:9.14.0" + dependencies: + "@sentry/core": "npm:9.14.0" + checksum: 10c0/5a78d458c76e95772938aeb88da00260007b7a1b6b51e0ca60bdf17f848df3828daba8b658021d0e842f7365c0d13e648ce26a0d4057880c1735e4644dabecb3 + languageName: node + linkType: hard + +"@sentry-internal/replay-canvas@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry-internal/replay-canvas@npm:9.11.0" + dependencies: + "@sentry-internal/replay": "npm:9.11.0" + "@sentry/core": "npm:9.11.0" + checksum: 10c0/c43959d73dde11def09745ae2d1ea195f5ad97160f9d4982118ff7dace798a999abcc042209acf03b0008a414f114c5005dd7867eef62a2e922401b4367630f0 + languageName: node + linkType: hard + +"@sentry-internal/replay-canvas@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry-internal/replay-canvas@npm:9.14.0" + dependencies: + "@sentry-internal/replay": "npm:9.14.0" + "@sentry/core": "npm:9.14.0" + checksum: 10c0/73d8b6d56677f393969e74aaf780377d06b7bbc56f7e8c94482b23697a9449bf362bd2f56cf9b9243ac228082a75a02e92f283db395ba0fd424f1f16c4a1e29b + languageName: node + linkType: hard + +"@sentry-internal/replay@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry-internal/replay@npm:9.11.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.11.0" + "@sentry/core": "npm:9.11.0" + checksum: 10c0/568eb8ba32710a8bb9e8fb4566c3365672f437f661c13da5ffcba6f55932e23774031388f545ddc517ed8a37759048a9b57ce400d2d8c62ff1146b41ce92d9f5 + languageName: node + linkType: hard + +"@sentry-internal/replay@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry-internal/replay@npm:9.14.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.14.0" + "@sentry/core": "npm:9.14.0" + checksum: 10c0/c6c455bfbc3ea9280878fa4c49d56b61658b04355265487bbd1156d2c628b02dbf60aec950d1adfafd1e4f48aad50cbe3e003231ba0b0456ad572a9cf63a0cef + languageName: node + linkType: hard + "@sentry/babel-plugin-component-annotate@npm:3.3.1": version: 3.3.1 resolution: "@sentry/babel-plugin-component-annotate@npm:3.3.1" @@ -4190,6 +4678,32 @@ __metadata: languageName: node linkType: hard +"@sentry/browser@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry/browser@npm:9.11.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.11.0" + "@sentry-internal/feedback": "npm:9.11.0" + "@sentry-internal/replay": "npm:9.11.0" + "@sentry-internal/replay-canvas": "npm:9.11.0" + "@sentry/core": "npm:9.11.0" + checksum: 10c0/6e5563aea64dc3c3f07eca213d1d9fd540a8d2ba10cf9d86d0cda1c3833fbe940813860213adbe221c848bc935d3304aa6abd8aae72225158723fc7defbe137c + languageName: node + linkType: hard + +"@sentry/browser@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry/browser@npm:9.14.0" + dependencies: + "@sentry-internal/browser-utils": "npm:9.14.0" + "@sentry-internal/feedback": "npm:9.14.0" + "@sentry-internal/replay": "npm:9.14.0" + "@sentry-internal/replay-canvas": "npm:9.14.0" + "@sentry/core": "npm:9.14.0" + checksum: 10c0/4ac0d0e236052668c630429b0ec4dc9ca6576ec9042295d7ab5cababe4f9ecedc4ce53b86d2b17cadb7639f5b5a6d4286279fe1a6d96851167601494f2478f45 + languageName: node + linkType: hard + "@sentry/bundler-plugin-core@npm:3.3.1": version: 3.3.1 resolution: "@sentry/bundler-plugin-core@npm:3.3.1" @@ -4292,6 +4806,103 @@ __metadata: languageName: node linkType: hard +"@sentry/core@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry/core@npm:9.11.0" + checksum: 10c0/566aaaad16afb34ada9d9a42b0bc486e127413ce1b4e5a883c5424e04b95d8855e060267b18e8e2a16ee05d3708aab6191ad855de0a150877249c58e5d64fce6 + languageName: node + linkType: hard + +"@sentry/core@npm:9.14.0": + version: 9.14.0 + resolution: "@sentry/core@npm:9.14.0" + checksum: 10c0/9008e876d6f1b863852380013466f7dfaed19f864aa840d8cc3b65f22f2347abf0a02d7048cdd0ab746f0648931f375748891b68103d69e48e54ead11e199e1d + languageName: node + linkType: hard + +"@sentry/electron@npm:^6.5.0": + version: 6.5.0 + resolution: "@sentry/electron@npm:6.5.0" + dependencies: + "@sentry/browser": "npm:9.11.0" + "@sentry/core": "npm:9.11.0" + "@sentry/node": "npm:9.11.0" + deepmerge: "npm:4.3.1" + checksum: 10c0/f1fdbf41d49a864086e9fd9af3c0c807e16b1ada67f18cf8825697de6df8e89f41d23974fef12e90fb502eb63a9c8b8ccda16145ad92eede076bb99a26e57485 + languageName: node + linkType: hard + +"@sentry/node@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry/node@npm:9.11.0" + dependencies: + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/context-async-hooks": "npm:^1.30.1" + "@opentelemetry/core": "npm:^1.30.1" + "@opentelemetry/instrumentation": "npm:^0.57.2" + "@opentelemetry/instrumentation-amqplib": "npm:^0.46.1" + "@opentelemetry/instrumentation-connect": "npm:0.43.1" + "@opentelemetry/instrumentation-dataloader": "npm:0.16.1" + "@opentelemetry/instrumentation-express": "npm:0.47.1" + "@opentelemetry/instrumentation-fastify": "npm:0.44.2" + "@opentelemetry/instrumentation-fs": "npm:0.19.1" + "@opentelemetry/instrumentation-generic-pool": "npm:0.43.1" + "@opentelemetry/instrumentation-graphql": "npm:0.47.1" + "@opentelemetry/instrumentation-hapi": "npm:0.45.2" + "@opentelemetry/instrumentation-http": "npm:0.57.2" + "@opentelemetry/instrumentation-ioredis": "npm:0.47.1" + "@opentelemetry/instrumentation-kafkajs": "npm:0.7.1" + "@opentelemetry/instrumentation-knex": "npm:0.44.1" + "@opentelemetry/instrumentation-koa": "npm:0.47.1" + "@opentelemetry/instrumentation-lru-memoizer": "npm:0.44.1" + "@opentelemetry/instrumentation-mongodb": "npm:0.52.0" + "@opentelemetry/instrumentation-mongoose": "npm:0.46.1" + "@opentelemetry/instrumentation-mysql": "npm:0.45.1" + "@opentelemetry/instrumentation-mysql2": "npm:0.45.2" + "@opentelemetry/instrumentation-pg": "npm:0.51.1" + "@opentelemetry/instrumentation-redis-4": "npm:0.46.1" + "@opentelemetry/instrumentation-tedious": "npm:0.18.1" + "@opentelemetry/instrumentation-undici": "npm:0.10.1" + "@opentelemetry/resources": "npm:^1.30.1" + "@opentelemetry/sdk-trace-base": "npm:^1.30.1" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + "@prisma/instrumentation": "npm:6.5.0" + "@sentry/core": "npm:9.11.0" + "@sentry/opentelemetry": "npm:9.11.0" + import-in-the-middle: "npm:^1.13.0" + checksum: 10c0/171f2a90e1b59aa78e5a5220366a6786fa0fa11deb1909442117bec21d323edb585bb248f1756a089befcb461b4e0976800d2582b51996f60dc625fdf964169c + languageName: node + linkType: hard + +"@sentry/opentelemetry@npm:9.11.0": + version: 9.11.0 + resolution: "@sentry/opentelemetry@npm:9.11.0" + dependencies: + "@sentry/core": "npm:9.11.0" + peerDependencies: + "@opentelemetry/api": ^1.9.0 + "@opentelemetry/context-async-hooks": ^1.30.1 + "@opentelemetry/core": ^1.30.1 + "@opentelemetry/instrumentation": ^0.57.1 + "@opentelemetry/sdk-trace-base": ^1.30.1 + "@opentelemetry/semantic-conventions": ^1.28.0 + checksum: 10c0/4e8c504b1b04fda5d3c6e0909908d2b41f3ee91fe0cd7a8fe2422fd0561d12480f199e49b638a98054b4dcd70d8ba7997b5aee06b50035dbdb720f5153ab9ee7 + languageName: node + linkType: hard + +"@sentry/react@npm:^9.14.0": + version: 9.14.0 + resolution: "@sentry/react@npm:9.14.0" + dependencies: + "@sentry/browser": "npm:9.14.0" + "@sentry/core": "npm:9.14.0" + hoist-non-react-statics: "npm:^3.3.2" + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + checksum: 10c0/ca59d97adc4e3d255c721f9447f354ad44f23a873d7ae4d96abbcde58fa116b1ee618ff6a9af58bc7b1916f8c24c0909d6ce2c15dfa98678dff98525ee61b306 + languageName: node + linkType: hard + "@sentry/vite-plugin@npm:^3.3.1": version: 3.3.1 resolution: "@sentry/vite-plugin@npm:3.3.1" @@ -4718,6 +5329,15 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:3.4.38": + version: 3.4.38 + resolution: "@types/connect@npm:3.4.38" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/2e1cdba2c410f25649e77856505cd60223250fa12dff7a503e492208dbfdd25f62859918f28aba95315251fd1f5e1ffbfca1e25e73037189ab85dd3f8d0a148c + languageName: node + linkType: hard + "@types/d3-array@npm:*": version: 3.2.1 resolution: "@types/d3-array@npm:3.2.1" @@ -5209,6 +5829,15 @@ __metadata: languageName: node linkType: hard +"@types/mysql@npm:2.15.26": + version: 2.15.26 + resolution: "@types/mysql@npm:2.15.26" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/3cf279e7db05d56c0544532a4380b9079f579092379a04c8138bd5cf88dda5b31208ac2d23ce7dbf4e3a3f43aaeed44e72f9f19f726518f308efe95a7435619a + languageName: node + linkType: hard + "@types/node-fetch@npm:^2.5.10, @types/node-fetch@npm:^2.6.4": version: 2.6.12 resolution: "@types/node-fetch@npm:2.6.12" @@ -5267,6 +5896,37 @@ __metadata: languageName: node linkType: hard +"@types/pg-pool@npm:2.0.6": + version: 2.0.6 + resolution: "@types/pg-pool@npm:2.0.6" + dependencies: + "@types/pg": "npm:*" + checksum: 10c0/41965d4d0b677c54ce45d36add760e496d356b78019cb062d124af40287cf6b0fd4d86e3b0085f443856c185983a60c8b0795ff76d15683e2a93c62f5ac0125f + languageName: node + linkType: hard + +"@types/pg@npm:*": + version: 8.11.13 + resolution: "@types/pg@npm:8.11.13" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^4.0.1" + checksum: 10c0/a111989a223f21ff864e35150474409e9659766603e4f7a51f89ffc173292adcd895c1551f792aaf1cf94afe99e4bd1dbf7c740252c1dca2a5038f1fd2c6e0bd + languageName: node + linkType: hard + +"@types/pg@npm:8.6.1": + version: 8.6.1 + resolution: "@types/pg@npm:8.6.1" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^2.2.0" + checksum: 10c0/8d16660c9a4f050d6d5e391c59f9a62e9d377a2a6a7eb5865f8828082dbdfeab700fd707e585f42d67b29e796b32863aea5bd6d5cbb8ceda2d598da5d0c61693 + languageName: node + linkType: hard + "@types/plist@npm:^3.0.1": version: 3.0.5 resolution: "@types/plist@npm:3.0.5" @@ -5364,6 +6024,13 @@ __metadata: languageName: node linkType: hard +"@types/shimmer@npm:^1.2.0": + version: 1.2.0 + resolution: "@types/shimmer@npm:1.2.0" + checksum: 10c0/6f7bfe1b55601cfc3ae713fc74a03341f3834253b8b91cb2add926d5949e4a63f7e666f59c2a6e40a883a5f9e2f3e3af10f9d3aed9b60fced0bda87659e58d8d + languageName: node + linkType: hard + "@types/stylis@npm:4.2.5": version: 4.2.5 resolution: "@types/stylis@npm:4.2.5" @@ -5371,6 +6038,15 @@ __metadata: languageName: node linkType: hard +"@types/tedious@npm:^4.0.14": + version: 4.0.14 + resolution: "@types/tedious@npm:4.0.14" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d2914f8e9b5b998e4275ec5f0130cba1c2fb47e75616b5c125a65ef6c1db2f1dc3f978c7900693856a15d72bbb4f4e94f805537a4ecb6dc126c64415d31c0590 + languageName: node + linkType: hard + "@types/tinycolor2@npm:^1": version: 1.4.6 resolution: "@types/tinycolor2@npm:1.4.6" @@ -5795,6 +6471,8 @@ __metadata: "@mozilla/readability": "npm:^0.6.0" "@notionhq/client": "npm:^2.2.15" "@reduxjs/toolkit": "npm:^2.2.5" + "@sentry/electron": "npm:^6.5.0" + "@sentry/react": "npm:^9.14.0" "@sentry/vite-plugin": "npm:^3.3.1" "@shikijs/markdown-it": "npm:^3.2.2" "@strongtz/win32-arm64-msvc": "npm:^0.4.7" @@ -5961,6 +6639,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-attributes@npm:^1.9.5": + version: 1.9.5 + resolution: "acorn-import-attributes@npm:1.9.5" + peerDependencies: + acorn: ^8 + checksum: 10c0/5926eaaead2326d5a86f322ff1b617b0f698aa61dc719a5baa0e9d955c9885cc71febac3fb5bacff71bbf2c4f9c12db2056883c68c53eb962c048b952e1e013d + languageName: node + linkType: hard + "acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -7281,6 +7968,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.2.2": + version: 1.4.3 + resolution: "cjs-module-lexer@npm:1.4.3" + checksum: 10c0/076b3af85adc4d65dbdab1b5b240fe5b45d44fcf0ef9d429044dd94d19be5589376805c44fb2d4b3e684e5fe6a9b7cf3e426476a6507c45283c5fc6ff95240be + languageName: node + linkType: hard + "classcat@npm:^5.0.3": version: 5.0.5 resolution: "classcat@npm:5.0.5" @@ -8414,7 +9108,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.3.1": +"deepmerge@npm:4.3.1, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -10524,6 +11218,13 @@ __metadata: languageName: node linkType: hard +"forwarded-parse@npm:2.1.2": + version: 2.1.2 + resolution: "forwarded-parse@npm:2.1.2" + checksum: 10c0/0c6b4c631775f272b4475e935108635495e8a5b261d1b4a5caef31c47c5a0b04134adc564e655aadfef366a02647fa3ae90a1d3ac19929f3ade47f9bed53036a + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -11861,6 +12562,18 @@ __metadata: languageName: node linkType: hard +"import-in-the-middle@npm:^1.13.0, import-in-the-middle@npm:^1.8.1": + version: 1.13.1 + resolution: "import-in-the-middle@npm:1.13.1" + dependencies: + acorn: "npm:^8.14.0" + acorn-import-attributes: "npm:^1.9.5" + cjs-module-lexer: "npm:^1.2.2" + module-details-from-path: "npm:^1.0.3" + checksum: 10c0/4ef05a924c37ff718dd08654927c90d470d92fd9425d646b0d423aaddc89655848debd14761bcb6efa4f57870d63ff38109bab31ca8a1d9d5df2e7d84d2649cf + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -14712,6 +15425,13 @@ __metadata: languageName: node linkType: hard +"module-details-from-path@npm:^1.0.3": + version: 1.0.3 + resolution: "module-details-from-path@npm:1.0.3" + checksum: 10c0/3d881f3410c142e4c2b1307835a2862ba04e5b3ec6e90655614a0ee2c4b299b4c1d117fb525d2435bf436990026f18d338a197b54ad6bd36252f465c336ff423 + languageName: node + linkType: hard + "monaco-editor@npm:^0.52.2": version: 0.52.2 resolution: "monaco-editor@npm:0.52.2" @@ -15190,6 +15910,13 @@ __metadata: languageName: node linkType: hard +"obuf@npm:~1.1.2": + version: 1.1.2 + resolution: "obuf@npm:1.1.2" + checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81 + languageName: node + linkType: hard + "office-text-extractor@npm:^3.0.3": version: 3.0.3 resolution: "office-text-extractor@npm:3.0.3" @@ -16023,6 +16750,55 @@ __metadata: languageName: node linkType: hard +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 + languageName: node + linkType: hard + +"pg-numeric@npm:1.0.2": + version: 1.0.2 + resolution: "pg-numeric@npm:1.0.2" + checksum: 10c0/43dd9884e7b52c79ddc28d2d282d7475fce8bba13452d33c04ceb2e0a65f561edf6699694e8e1c832ff9093770496363183c950dd29608e1bdd98f344b25bca9 + languageName: node + linkType: hard + +"pg-protocol@npm:*": + version: 1.9.5 + resolution: "pg-protocol@npm:1.9.5" + checksum: 10c0/5cb3444cf973adadd22ee9ea26bb1674f0d980ef8f9c66d426bbe67fc9cb5f0ca4a204bf7e432b3ab2ab59ac8227f4b18ab3b2e64eaed537e037e916991c7319 + languageName: node + linkType: hard + +"pg-types@npm:^2.2.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: "npm:1.0.1" + postgres-array: "npm:~2.0.0" + postgres-bytea: "npm:~1.0.0" + postgres-date: "npm:~1.0.4" + postgres-interval: "npm:^1.1.0" + checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 + languageName: node + linkType: hard + +"pg-types@npm:^4.0.1": + version: 4.0.2 + resolution: "pg-types@npm:4.0.2" + dependencies: + pg-int8: "npm:1.0.1" + pg-numeric: "npm:1.0.2" + postgres-array: "npm:~3.0.1" + postgres-bytea: "npm:~3.0.0" + postgres-date: "npm:~2.1.0" + postgres-interval: "npm:^3.0.0" + postgres-range: "npm:^1.1.1" + checksum: 10c0/780fccda2f3fa2a34e85a72e8e7dadb7d88fbe71ce88f126cb3313f333ad836d02488ec4ff3d94d0c1e5846f735d6e6c6281f8059e6b8919d2180429acaec3e2 + languageName: node + linkType: hard + "phantomjs-prebuilt@npm:^2.1.14": version: 2.1.16 resolution: "phantomjs-prebuilt@npm:2.1.16" @@ -16213,6 +16989,73 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 + languageName: node + linkType: hard + +"postgres-array@npm:~3.0.1": + version: 3.0.4 + resolution: "postgres-array@npm:3.0.4" + checksum: 10c0/47f3e648da512bacdd6a5ed55cf770605ec271330789faeece0fd13805a49f376d6e5c9e0e353377be11a9545e727dceaa2473566c505432bf06366ccd04c6b2 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.0 + resolution: "postgres-bytea@npm:1.0.0" + checksum: 10c0/febf2364b8a8953695cac159eeb94542ead5886792a9627b97e33f6b5bb6e263bc0706ab47ec221516e79fbd6b2452d668841830fb3b49ec6c0fc29be61892ce + languageName: node + linkType: hard + +"postgres-bytea@npm:~3.0.0": + version: 3.0.0 + resolution: "postgres-bytea@npm:3.0.0" + dependencies: + obuf: "npm:~1.1.2" + checksum: 10c0/41c79cc48aa730c5ba3eda6ab989a940034f07a1f57b8f2777dce56f1b8cca16c5870582932b5b10cc605048aef9b6157e06253c871b4717cafc6d00f55376aa + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 + languageName: node + linkType: hard + +"postgres-date@npm:~2.1.0": + version: 2.1.0 + resolution: "postgres-date@npm:2.1.0" + checksum: 10c0/00a7472c10788f6b0d08d24108bf1eb80858de1bd6317740198a564918ea4a69b80c98148167b92ae688abd606483020d0de0dd3a36f3ea9a3e26bbeef3464f4 + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: "npm:^4.0.0" + checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 + languageName: node + linkType: hard + +"postgres-interval@npm:^3.0.0": + version: 3.0.0 + resolution: "postgres-interval@npm:3.0.0" + checksum: 10c0/8b570b30ea37c685e26d136d34460f246f98935a1533defc4b53bb05ee23ae3dc7475b718ec7ea607a57894d8c6b4f1adf67ca9cc83a75bdacffd427d5c68de8 + languageName: node + linkType: hard + +"postgres-range@npm:^1.1.1": + version: 1.1.4 + resolution: "postgres-range@npm:1.1.4" + checksum: 10c0/254494ef81df208e0adeae6b66ce394aba37914ea14c7ece55a45fb6691b7db04bee74c825380a47c887a9f87158fd3d86f758f9cc60b76d3a38ce5aca7912e8 + languageName: node + linkType: hard + "prebuild-install@npm:^5.3.5": version: 5.3.6 resolution: "prebuild-install@npm:5.3.6" @@ -17792,6 +18635,17 @@ __metadata: languageName: node linkType: hard +"require-in-the-middle@npm:^7.1.1": + version: 7.5.2 + resolution: "require-in-the-middle@npm:7.5.2" + dependencies: + debug: "npm:^4.3.5" + module-details-from-path: "npm:^1.0.3" + resolve: "npm:^1.22.8" + checksum: 10c0/43a2dac5520e39d13c413650895715e102d6802e6cc6ff322017bd948f12a9657fe28435f7cbbcba437b167f02e192ac7af29fa35cabd5d0c375d071c0605e01 + languageName: node + linkType: hard + "require-main-filename@npm:^1.0.1": version: 1.0.1 resolution: "require-main-filename@npm:1.0.1" @@ -17843,7 +18697,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0": +"resolve@npm:^1.10.0, resolve@npm:^1.22.8": version: 1.22.10 resolution: "resolve@npm:1.22.10" dependencies: @@ -17856,7 +18710,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": version: 1.22.10 resolution: "resolve@patch:resolve@npm%3A1.22.10#optional!builtin::version=1.22.10&hash=c3c19d" dependencies: @@ -18327,7 +19181,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": +"semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.6.0, semver@npm:^7.6.3": version: 7.7.1 resolution: "semver@npm:7.7.1" bin: @@ -18465,6 +19319,13 @@ __metadata: languageName: node linkType: hard +"shimmer@npm:^1.2.1": + version: 1.2.1 + resolution: "shimmer@npm:1.2.1" + checksum: 10c0/ae8b27c389db2a00acfc8da90240f11577685a8f3e40008f826a3bea8b4f3b3ecd305c26be024b4a0fd3b123d132c1569d6e238097960a9a543b6c60760fb46a + languageName: node + linkType: hard + "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0"