diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 9b7c3a209a..136de78bd6 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -16,6 +16,7 @@ import { ThemeProvider } from './context/ThemeProvider' import NavigationHandler from './handler/NavigationHandler' import AgentsPage from './pages/agents/AgentsPage' import AppsPage from './pages/apps/AppsPage' +import DeepResearchPage from './pages/deepresearch/DeepResearchPage' import FilesPage from './pages/files/FilesPage' import HomePage from './pages/home/HomePage' import KnowledgePage from './pages/knowledge/KnowledgePage' @@ -45,6 +46,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..29863c338b --- /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..2fcc7b8898 --- /dev/null +++ b/src/renderer/src/components/DeepResearch/index.ts @@ -0,0 +1,3 @@ +import DeepResearchPanel from './DeepResearchPanel' + +export { DeepResearchPanel } diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 198e49a43b..8e92da9918 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, @@ -136,7 +137,8 @@ const MainMenus: FC = () => { translate: , minapp: , knowledge: , - files: + files: , + deepresearch: } const pathMap = { @@ -146,7 +148,8 @@ const MainMenus: FC = () => { translate: '/translate', minapp: '/apps', knowledge: '/knowledge', - files: '/files' + files: '/files', + 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 2bada24b26..2f94af7f0c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1,5 +1,29 @@ { "translation": { + "deepresearch": { + "title": "Deep Research", + "description": "Provides comprehensive research reports through multiple rounds of search, analysis, and summarization", + "start": "Start Deep Research", + "query": { + "placeholder": "Enter research topic or question", + "empty": "Please enter a research query" + }, + "max_iterations": "Maximum Iterations", + "researching": "Conducting deep research, this may take a few minutes...", + "report": { + "title": "Deep Research Report", + "key_insights": "Key Insights", + "summary": "Research Summary", + "iterations": "Research Iterations", + "sources": "Information Sources" + }, + "iteration": { + "title": "Iteration", + "search_results": "Search Results", + "analysis": "Analysis", + "follow_up_queries": "Follow-up Queries" + } + }, "agents": { "add.button": "Add to Assistant", "add.knowledge_base": "Knowledge Base", @@ -1337,6 +1361,12 @@ "tray.show": "Show Tray Icon", "tray.title": "Tray", "websearch": { + "deep_research": { + "title": "Deep Research Settings", + "max_iterations": "Maximum Iterations", + "max_results_per_query": "Maximum Results Per Query", + "auto_summary": "Auto Summary" + }, "blacklist": "Blacklist", "blacklist_description": "Results from the following websites will not appear in search results", "blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8da18e72f4..8734808b65 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1,5 +1,31 @@ { "translation": { + "deepresearch": { + "title": "深度研究", + "description": "通过多轮搜索、分析和总结,提供全面的研究报告", + "start": "开始深度研究", + "query": { + "placeholder": "输入研究主题或问题", + "empty": "请输入研究查询" + }, + "max_iterations": "最大迭代次数", + "researching": "正在进行深度研究,这可能需要几分钟时间...", + "report": { + "title": "深度研究报告", + "key_insights": "关键见解", + "summary": "研究总结", + "iterations": "研究迭代", + "sources": "信息来源" + }, + "iteration": { + "title": "迭代", + "search_results": "搜索结果", + "analysis": "分析", + "follow_up_queries": "后续查询" + }, + "engine_rotation": "每次迭代使用不同类别的搜索引擎:中文、国际、元搜索和学术搜索", + "open": "打开深度研究" + }, "agents": { "add.button": "添加到助手", "add.knowledge_base": "知识库", @@ -1337,6 +1363,12 @@ "tray.show": "显示托盘图标", "tray.title": "托盘", "websearch": { + "deep_research": { + "title": "深度研究设置", + "max_iterations": "最大迭代次数", + "max_results_per_query": "每次查询的最大结果数", + "auto_summary": "自动生成摘要" + }, "blacklist": "黑名单", "blacklist_description": "在搜索结果中不会出现以下网站的结果", "blacklist_tooltip": "请使用以下格式(换行分隔)\n匹配模式: *://*.example.com/*\n正则表达式: /example\\.(net|org)/", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 0524918471..4e960f76c1 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1,5 +1,29 @@ { "translation": { + "deepresearch": { + "title": "深度研究", + "description": "通過多輪搜索、分析和總結,提供全面的研究報告", + "start": "開始深度研究", + "query": { + "placeholder": "輸入研究主題或問題", + "empty": "請輸入研究查詢" + }, + "max_iterations": "最大迭代次數", + "researching": "正在進行深度研究,這可能需要幾分鐘時間...", + "report": { + "title": "深度研究報告", + "key_insights": "關鍵見解", + "summary": "研究總結", + "iterations": "研究迭代", + "sources": "信息來源" + }, + "iteration": { + "title": "迭代", + "search_results": "搜索結果", + "analysis": "分析", + "follow_up_queries": "後續查詢" + } + }, "agents": { "add.button": "新增到助手", "add.knowledge_base": "知識庫", @@ -1336,6 +1360,12 @@ "tray.show": "顯示系统匣圖示", "tray.title": "系统匣", "websearch": { + "deep_research": { + "title": "深度研究設置", + "max_iterations": "最大迭代次數", + "max_results_per_query": "每次查詢的最大結果數", + "auto_summary": "自動生成摘要" + }, "check_success": "驗證成功", "get_api_key": "點選這裡取得金鑰", "search_with_time": "搜尋包含日期", diff --git a/src/renderer/src/pages/deepresearch/DeepResearchPage.tsx b/src/renderer/src/pages/deepresearch/DeepResearchPage.tsx new file mode 100644 index 0000000000..2fa950e8b7 --- /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 c1811862ee..c16969875e 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,7 @@ 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/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/DeepResearchSettings.tsx.new b/src/renderer/src/pages/settings/WebSearchSettings/DeepResearchSettings.tsx.new new file mode 100644 index 0000000000..e69de29bb2 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/index.tsx b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx index e00eb785e7..18c6c96f30 100644 --- a/src/renderer/src/pages/settings/WebSearchSettings/index.tsx +++ b/src/renderer/src/pages/settings/WebSearchSettings/index.tsx @@ -9,6 +9,7 @@ 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 WebSearchProviderSetting from './WebSearchProviderSetting' const WebSearchSettings: FC = () => { @@ -56,6 +57,7 @@ const WebSearchSettings: FC = () => { )} + ) diff --git a/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.new.ts.bak b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.new.ts.bak new file mode 100644 index 0000000000..a82e7a0cb6 --- /dev/null +++ b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.new.ts.bak @@ -0,0 +1,492 @@ +import { WebSearchProvider } from '@renderer/providers/WebSearchProvider/WebSearchProvider' +import { WebSearchResult } from '@renderer/types' + +// 临时定义WebSearchState类型,因为它在@renderer/types中不存在 +interface WebSearchState { + defaultProvider: string + providers: any[] + maxResults: number + excludeDomains: string[] + searchWithTime: boolean + subscribeSources: string[] + overwrite: boolean + deepResearchConfig?: { + maxIterations?: number + maxResultsPerQuery?: number + autoSummary?: boolean + enableQueryOptimization?: boolean + } +} + +// 临时定义DeepSearchProvider类 +class DeepSearchProvider { + constructor(provider: WebSearchProvider) {} + + async search(query: string, webSearchState?: WebSearchState, category?: string): Promise<{ results: WebSearchResult[] }> { + return { results: [] } + } +} + +/** + * 分析配置接口 + */ +interface AnalysisConfig { + maxIterations: number + maxResultsPerQuery: number + minConfidenceScore: number + autoSummary: boolean + modelId?: string +} + +/** + * 研究迭代接口 + */ +interface ResearchIteration { + query: string + results: WebSearchResult[] + analysis: string + followUpQueries: string[] +} + +/** + * 研究报告接口 + */ +export interface ResearchReport { + originalQuery: string + iterations: ResearchIteration[] + summary: string + keyInsights: string[] + sources: string[] +} + +/** + * DeepResearchProvider 类 + * 提供深度研究功能,包括多轮搜索、分析和总结 + */ +export class DeepResearchProvider { + private deepSearchProvider: DeepSearchProvider + private analysisConfig: AnalysisConfig + constructor(provider: WebSearchProvider) { + this.deepSearchProvider = new DeepSearchProvider(provider) + this.analysisConfig = { + maxIterations: 3, // 默认最大迭代次数 + maxResultsPerQuery: 20, // 每次查询的最大结果数 + minConfidenceScore: 0.6, // 最小可信度分数 + autoSummary: true // 自动生成摘要 + } + } + + /** + * 优化查询 + * @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状态 + * @returns 研究报告 + */ + public async research(query: string, websearch?: WebSearchState): Promise { + // 确保 websearch 存在 + const webSearchState: WebSearchState = websearch || { + defaultProvider: '', + providers: [], + maxResults: 10, + excludeDomains: [], + searchWithTime: false, + subscribeSources: [], + 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: '', + keyInsights: [], + sources: [] + } + + let currentQuery = optimizedQuery + let iterationCount = 0 + const allSources = new Set() + + // 定义搜索引擎类别列表 + const engineCategories = ['chinese', 'international', 'meta', 'academic'] + + // 迭代研究过程 + while (iterationCount < this.analysisConfig.maxIterations) { + console.log(`[DeepResearch] 迭代 ${iterationCount + 1}: "${currentQuery}"`) + + // 根据当前迭代选择搜索引擎类别 + const categoryIndex = iterationCount % engineCategories.length + const currentCategory = engineCategories[categoryIndex] + console.log(`[DeepResearch] 这一迭代使用 ${currentCategory} 类别的搜索引擎`) + + // 1. 使用DeepSearch获取当前查询的结果,指定搜索引擎类别 + const searchResponse = await this.deepSearchProvider.search(currentQuery, webSearchState, currentCategory) + + // 限制结果数量 + const limitedResults = searchResponse.results.slice(0, this.analysisConfig.maxResultsPerQuery) + + // 2. 分析搜索结果 + const analysis = await this.analyzeResults(limitedResults, currentQuery) + + // 3. 生成后续查询 + 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] 没有更多的后续查询,结束迭代`) + break + } + + // 7. 更新查询并继续 + currentQuery = followUpQueries[0] // 使用第一个后续查询 + iterationCount++ + } + + // 生成最终总结 + report.summary = await this.generateSummary(report.iterations) + report.keyInsights = await this.extractKeyInsights(report.iterations) + report.sources = Array.from(allSources) + + console.log(`[DeepResearch] 完成深度研究,共 ${report.iterations.length} 次迭代`) + return report + } + + /** + * 分析搜索结果 + * @param results 搜索结果 + * @param query 当前查询 + * @returns 分析文本 + */ + private async analyzeResults(results: WebSearchResult[], query: string): 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) + + return analysis + } catch (error: any) { + console.error('[DeepResearch] 分析结果时出错:', error) + return `分析过程中出现错误: ${error?.message || '未知错误'}` + } + } + + /** + * 使用项目中的模型分析搜索结果 + */ + private async analyzeWithModel(contentSummaries: string, query: string): 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}"的有效内容可供分析。` + } + + // 使用项目中的 fetchGenerate 函数调用模型 + const { fetchGenerate } = await import('@renderer/services/ApiService') + const analysis = await fetchGenerate({ + prompt, + content: contentSummaries || ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + 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[]): 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. 该领域的当前状态 +4. 未来发展趋势 + +请使用学术性、客观的语言,并确保总结涵盖所有迭代中的重要发现。 + +研究迭代数据: +${iterationsData}` + + // 确保内容不为空 + if (!iterationsData || iterationsData.trim() === '') { + return `没有足够的数据来生成关于"${mainQuery}"的研究总结。` + } + + const summary = await fetchGenerate({ + prompt, + content: iterationsData || ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + return summary || `无法生成关于 "${mainQuery}" 的研究总结。` + } catch (error: any) { + console.error('[DeepResearch] 生成研究总结失败:', error) + return `生成研究总结时出错: ${error?.message || '未知错误'}` + } + } + + /** + * 提取关键见解 + * @param iterations 所有迭代 + * @returns 关键见解列表 + */ + private async extractKeyInsights(iterations: ResearchIteration[]): 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}"的研究分析中,提取 4-6 条最重要的关键见解。 + +这些见解应该是研究中最有价值、最有洞察力的发现,能够帮助读者快速理解该主题的核心要点。 + +请仅返回见解列表,每行一条,不要添加编号或其他标记。确保每条见解简洁、清晰、有洞察力。 + +研究分析内容: +${allAnalyses}` + + // 确保内容不为空 + if (!allAnalyses || allAnalyses.trim() === '') { + return [`关于${mainQuery}的研究数据不足,无法提取有意义的见解。`, `需要更多的搜索结果来全面分析${mainQuery}。`] + } + + const result = await fetchGenerate({ + prompt, + content: allAnalyses || ' ', // 确保内容不为空 + modelId: this.analysisConfig.modelId // 使用指定的模型 + }) + + 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}的未来发展前景广阔。` + ] + } + } + + /** + * 设置分析配置 + * @param config 配置对象 + */ + public setAnalysisConfig(config: Partial): void { + this.analysisConfig = { + ...this.analysisConfig, + ...config + } + } +} diff --git a/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.summary.ts b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.summary.ts new file mode 100644 index 0000000000..07bcf5b723 --- /dev/null +++ b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.summary.ts @@ -0,0 +1,54 @@ +export const getSummaryPrompt = (mainQuery: string, iterationsData: string): string => { + return `你是一个高级学术研究分析师,负责生成深入、全面的研究总结。 +请基于以下关于"${mainQuery}"的多轮研究迭代,生成一份学术水准的综合研究报告。 + +报告应包含以下部分,每部分都应详细、深入,并包含具体例证: + +1. 摘要(简要总结整个研究的主要发现) + +2. 背景与历史背景 + - 该主题的起源与发展过程 + - 关键里程碑与转折点 + +3. 主要发现(详细列出至少8-10个要点) + - 每个发现应有充分的说明和例证 + - 引用具体数据、研究或信息源 + +4. 争议点与不同观点 + - 详细分析各方立场及其依据 + - 客观评估各种观点的合理性和限制 + +5. 技术细节与实现方法(如适用) + - 当前技术实现的详细分析 + - 各种方法的比较与评估 + +6. 当前领域状态 + - 最新研究进展与突破 + - 主要参与者与机构 + - 当前面临的挑战与障碍 + +7. 专家共识与最佳实践 + - 行业公认的标准与方法 + - 成功案例与经验教训 + +8. 未来发展趋势 + - 预测的发展方向与变革 + - 潜在的机遇与风险 + +9. 研究局限性 + - 当前研究的不足与局限 + - 数据或方法的可靠性考量 + +10. 建议的未来研究方向 + - 具体的研究问题与方法建议 + - 为什么这些方向值得探索 + +请使用学术性、分析性的语言,提供深入的分析而非简单总结。确保你的分析: +- 包含具体的事实、数据和例子 +- 引用搜索结果中的具体信息源 +- 区分已确认的事实和推测性内容 +- 指出信息的可靠性和局限性 + +研究迭代数据: +${iterationsData}` +} diff --git a/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts new file mode 100644 index 0000000000..2f60dba8fd --- /dev/null +++ b/src/renderer/src/providers/WebSearchProvider/DeepResearchProvider.ts @@ -0,0 +1,775 @@ +import { WebSearchState } from '@renderer/store/websearch' +import { WebSearchProvider, WebSearchResult } from '@renderer/types' + +import DeepSearchProvider from './DeepSearchProvider' + +/** + * 分析配置接口 + */ +interface AnalysisConfig { + maxIterations: number + maxResultsPerQuery: number + minConfidenceScore: number + autoSummary: boolean + modelId?: string + minOutputTokens?: number // 最小输出token数 + maxInputTokens?: number // 最大输入token数 +} + +/** + * 研究迭代接口 + */ +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 + } +} + +/** + * DeepResearchProvider 类 + * 提供深度研究功能,包括多轮搜索、分析和总结 + */ +export class DeepResearchProvider { + private deepSearchProvider: DeepSearchProvider + private analysisConfig: AnalysisConfig + constructor(provider: WebSearchProvider) { + 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 + } + } + + /** + * 优化查询 + * @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: [], + 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 + } + } +} diff --git a/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts b/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts index 72d40c820a..70fbea4083 100644 --- a/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts +++ b/src/renderer/src/providers/WebSearchProvider/DeepSearchProvider.ts @@ -15,60 +15,74 @@ 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', - 'download', - 'shop', - 'store', - 'buy', - 'cart', 'checkout', + + // 广告相关 'ads', + 'ad.', + 'adv.', 'advertisement', 'sponsor', 'tracking', - 'facebook.com', - 'twitter.com', - 'instagram.com', - 'pinterest.com', - 'youtube.com/channel', - 'youtube.com/user', - 'youtube.com/c/', - 'tiktok.com', - 'douyin.com', - 'weibo.com', - 'zhihu.com/question', - 'baike.baidu.com', - 'wiki.com', - 'wikipedia.org/wiki/Help:', - 'wikipedia.org/wiki/Wikipedia:', - 'wikipedia.org/wiki/Template:', - 'wikipedia.org/wiki/File:', - 'wikipedia.org/wiki/Category:', - 'amazon.com/s', - 'amazon.cn/s', - 'taobao.com/search', - 'jd.com/search', - 'tmall.com/search', - 'ebay.com/sch', - 'aliexpress.com/wholesale' + 'promotion', + 'marketing', + 'banner', + 'popup', + + // 购物相关 + 'cart', + 'shop', + 'store', + 'buy', + 'price', + 'deal', + 'coupon', + 'discount', + + // 社交媒体评论区 + 'comment', + 'comments', + 'forum', + 'bbs' ], // 优先的域名(相关性更高) priorityDomains: [ @@ -143,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') @@ -155,8 +169,22 @@ 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 + + // 如果指定了类别,则只使用该类别的搜索引擎 + if (engineCategory) { + enginesToUse = this.searchEngines.filter((engine) => engine.category === engineCategory) + // 如果该类别没有搜索引擎,则使用所有搜索引擎 + if (enginesToUse.length === 0) { + enginesToUse = this.searchEngines + } + } + + 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)) @@ -291,6 +319,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 搜索结果 @@ -623,10 +738,19 @@ export default class DeepSearchProvider extends BaseWebSearchProvider { return scoreB - scoreA }) - // 过滤掉明显不相关的结果,提高阈值以只保留更相关的结果 + // 过滤掉明显不相关的结果和乱码内容 const filteredResults = analyzedResults.filter((result) => { + // 检查相关性分数 const score = (result as AnalyzedResult).relevanceScore || 0 - return score > 0.2 // 提高阈值到 0.2,只保留相关性分数较高的结果 + 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} 个结果`) diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts index 14c408e5bf..10b2496b85 100644 --- a/src/renderer/src/services/ApiService.ts +++ b/src/renderer/src/services/ApiService.ts @@ -6,6 +6,7 @@ import { } from '@renderer/config/models' import { SEARCH_SUMMARY_PROMPT } from '@renderer/config/prompts' import i18n from '@renderer/i18n' +import { getModelUniqId } from '@renderer/services/ModelService' import store from '@renderer/store' import { setGenerating } from '@renderer/store/runtime' import { @@ -391,8 +392,27 @@ export async function fetchSearchSummary({ messages, assistant }: { messages: Me } } -export async function fetchGenerate({ prompt, content }: { prompt: string; content: string }): Promise { - const model = getDefaultModel() +export async function fetchGenerate({ + prompt, + content, + modelId +}: { + prompt: string + content: string + modelId?: string +}): Promise { + let model = getDefaultModel() + + // 如果提供了指定的模型 ID,尝试使用该模型 + if (modelId) { + const { providers } = store.getState().llm + const allModels = providers.flatMap((p) => p.models) + const specifiedModel = allModels.find((m) => getModelUniqId(m) === modelId) + if (specifiedModel) { + model = specifiedModel + } + } + const provider = getProviderByModel(model) if (!hasApiKey(provider)) { @@ -404,6 +424,7 @@ export async function fetchGenerate({ prompt, content }: { prompt: string; conte try { return await AI.generateText({ prompt, content }) } catch (error: any) { + console.error('Generate text error:', error) return '' } } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index c03433abf4..0d5d692836 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -7,7 +7,15 @@ import { WebDAVSyncState } from './backup' export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' -export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files' +export type SidebarIcon = + | 'assistants' + | 'agents' + | 'paintings' + | 'translate' + | 'minapp' + | 'knowledge' + | 'files' + | 'deepresearch' export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'assistants', @@ -16,7 +24,8 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [ 'translate', 'minapp', 'knowledge', - 'files' + 'files', + 'deepresearch' ] export interface NutstoreSyncRuntime extends WebDAVSyncState {} diff --git a/src/renderer/src/store/websearch.ts b/src/renderer/src/store/websearch.ts index 64029c7ff0..d82cecd846 100644 --- a/src/renderer/src/store/websearch.ts +++ b/src/renderer/src/store/websearch.ts @@ -7,6 +7,14 @@ export interface SubscribeSource { blacklist?: string[] // 存储从该订阅源获取的黑名单 } +export interface DeepResearchConfig { + maxIterations: number + maxResultsPerQuery: number + autoSummary: boolean + modelId?: string + enableQueryOptimization?: boolean +} + export interface WebSearchState { // 默认搜索提供商的ID defaultProvider: string @@ -23,6 +31,8 @@ export interface WebSearchState { // 是否覆盖服务商搜索 overwrite: boolean contentLimit?: number + // Deep Research 配置 + deepResearchConfig: DeepResearchConfig } const initialState: WebSearchState = { @@ -70,7 +80,13 @@ const initialState: WebSearchState = { maxResults: 10, excludeDomains: [], subscribeSources: [], - overwrite: false + overwrite: false, + deepResearchConfig: { + maxIterations: 3, + maxResultsPerQuery: 5, + autoSummary: true, + enableQueryOptimization: true + } } export const defaultWebSearchProviders = initialState.providers @@ -144,6 +160,9 @@ const websearchSlice = createSlice({ }, setContentLimit: (state, action: PayloadAction) => { state.contentLimit = action.payload + }, + setDeepResearchConfig: (state, action: PayloadAction) => { + state.deepResearchConfig = action.payload } } }) @@ -162,7 +181,8 @@ export const { setSubscribeSources, setOverwrite, addWebSearchProvider, - setContentLimit + setContentLimit, + setDeepResearchConfig } = websearchSlice.actions export default websearchSlice.reducer diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 3d19f64706..8ed9dba409 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -367,6 +367,22 @@ export type WebSearchResult = { meta?: Record // 添加meta字段用于存储元数据 } +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[] +} + export type KnowledgeReference = { id: number content: string @@ -515,3 +531,25 @@ export interface Citation { } export type MathEngine = 'KaTeX' | 'MathJax' | 'none' + +// Deep Research 类型定义 +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 + } +}