This commit is contained in:
1600822305 2025-04-24 06:38:47 +08:00
parent d4b2d53118
commit 68541a769e
29 changed files with 3914 additions and 156 deletions

View File

@ -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",

View File

@ -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 {
<PersistGate loading={null} persistor={persistor}>
<MemoryProvider>
<DeepClaudeProvider />
<GeminiInitializer />
<PDFSettingsInitializer />
<WebSearchInitializer />
<WorkspaceInitializer />

View File

@ -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;
}

View File

@ -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<ResearchReport | null>(null)
const [error, setError] = useState<string | null>(null)
const [maxIterations, setMaxIterations] = useState(3)
const [historyVisible, setHistoryVisible] = useState(false)
const [history, setHistory] = useState<ResearchHistory[]>([])
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<HTMLInputElement>) => {
setQuery(e.target.value)
}
const handleMaxIterationsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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) => (
<List.Item>
<Card
title={
<a href={result.url} target="_blank" rel="noopener noreferrer">
{result.title}
</a>
}
size="small"
style={{ width: '100%', wordBreak: 'break-word', overflowWrap: 'break-word' }}>
<Paragraph ellipsis={{ rows: 3 }}>
{result.content ? result.content.substring(0, 200) + '...' : '无内容'}
</Paragraph>
<Text type="secondary" style={{ wordBreak: 'break-word', overflowWrap: 'break-word', display: 'block' }}>
: {result.url}
</Text>
</Card>
</List.Item>
)
const renderIteration = (iteration: ResearchIteration, index: number) => (
<Panel
header={
<Space>
<FileSearchOutlined />
<span>
{index + 1}: {iteration.query}
</span>
</Space>
}
key={index}>
<Title level={5}></Title>
<List dataSource={iteration.results} renderItem={renderResultItem} grid={{ gutter: 16, column: 1 }} />
<Divider />
<Title level={5}></Title>
<Card>
<ReactMarkdown remarkPlugins={[remarkGfm]} className="markdown">
{iteration.analysis}
</ReactMarkdown>
</Card>
<Divider />
<Title level={5}></Title>
<Space wrap>
{iteration.followUpQueries.map((q, i) => (
<Tag color="blue" key={i}>
{q}
</Tag>
))}
</Space>
</Panel>
)
const renderReport = () => {
if (!report) return null
return (
<div>
<Card>
<Title level={3}>
<ExperimentOutlined /> : {report.originalQuery}
</Title>
{report.tokenUsage && (
<div className="token-stats">
Token统计: 输入 {report.tokenUsage.inputTokens.toLocaleString()} | {' '}
{report.tokenUsage.outputTokens.toLocaleString()} | {report.tokenUsage.totalTokens.toLocaleString()}
</div>
)}
<Divider />
<Title level={4} className="direct-answer-title">
</Title>
<Card className="direct-answer-card">
<ReactMarkdown remarkPlugins={[remarkGfm]} className="markdown">
{report.directAnswer}
</ReactMarkdown>
</Card>
<Divider />
<Title level={4}>
<BulbOutlined />
</Title>
<List
dataSource={report.keyInsights}
renderItem={(item) => (
<List.Item>
<Text>{item}</Text>
</List.Item>
)}
/>
<Divider />
<Title level={4}></Title>
<Card>
<ReactMarkdown remarkPlugins={[remarkGfm]} className="markdown">
{report.summary}
</ReactMarkdown>
</Card>
<Divider />
<Title level={4}></Title>
<Collapse>{report.iterations.map((iteration, index) => renderIteration(iteration, index))}</Collapse>
<Divider />
<Title level={4}>
<LinkOutlined />
</Title>
<List
dataSource={report.sources}
renderItem={(source) => (
<List.Item>
<a href={source} target="_blank" rel="noopener noreferrer" className="source-link">
{source}
</a>
</List.Item>
)}
/>
</Card>
</div>
)
}
// 渲染历史记录对话框
const renderHistoryModal = () => (
<Modal
title={
<div>
<HistoryOutlined />
</div>
}
open={historyVisible}
onCancel={() => setHistoryVisible(false)}
footer={null}
width={800}>
<List
dataSource={history}
renderItem={(item) => (
<List.Item
actions={[
<Button key="load" type="link" onClick={() => loadFromHistory(item)}>
</Button>,
<Button key="export" type="link" onClick={() => exportToMarkdown(item.report)}>
</Button>
]}>
<List.Item.Meta
title={item.query}
description={
<div>
<div>: {item.date}</div>
<div>: {item.report.iterations.length}</div>
</div>
}
/>
</List.Item>
)}
locale={{ emptyText: '暂无历史记录' }}
/>
</Modal>
)
return (
<div className="deep-research-container">
<Title level={3}>
<ExperimentOutlined />
</Title>
<Paragraph></Paragraph>
<Space direction="vertical" style={{ width: '100%', marginBottom: '20px' }}>
<Input
placeholder="输入研究主题或问题"
value={query}
onChange={handleQueryChange}
prefix={<SearchOutlined />}
size="large"
/>
<Space>
<Text>:</Text>
<Input type="number" value={maxIterations} onChange={handleMaxIterationsChange} style={{ width: '60px' }} />
<Button
type="primary"
icon={<ExperimentOutlined />}
onClick={startResearch}
loading={isResearching}
disabled={!query.trim() || !selectedProvider}>
</Button>
<Button icon={<HistoryOutlined />} onClick={() => setHistoryVisible(true)} disabled={isResearching}>
</Button>
{report && (
<Button icon={<DownloadOutlined />} onClick={() => exportToMarkdown(report)} disabled={isResearching}>
</Button>
)}
</Space>
</Space>
{error && <div className="error-message">{error}</div>}
{isResearching && (
<div className="research-loading">
<Spin size="large" />
<div className="loading-status">
<div>: {progressStatus}</div>
<div className="iteration-info">
{currentIteration}/{maxIterations}
</div>
<div className="progress-container">
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${progressPercent}%` }} />
</div>
<div className="progress-percentage">{progressPercent}%</div>
</div>
</div>
</div>
)}
{report && renderReport()}
{/* 渲染历史记录对话框 */}
{renderHistoryModal()}
</div>
)
}
export default DeepResearchPanel

View File

@ -0,0 +1,4 @@
import DeepResearchPanel from './DeepResearchPanel'
export { DeepResearchPanel }
export default DeepResearchPanel

View File

@ -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

View File

@ -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
}

View File

@ -17,6 +17,7 @@ import {
Languages,
LayoutGrid,
MessageSquareQuote,
Microscope,
Moon,
Palette,
Settings,
@ -138,7 +139,8 @@ const MainMenus: FC = () => {
minapp: <LayoutGrid size={18} className="icon" />,
knowledge: <FileSearch size={18} className="icon" />,
files: <Folder size={17} className="icon" />,
workspace: <FolderGit size={17} className="icon" />
workspace: <FolderGit size={17} className="icon" />,
deepresearch: <Microscope size={18} className="icon" />
}
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) => {

View File

@ -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
}
}

View File

@ -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",

View File

@ -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": "快捷短语",

View File

@ -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"
}
}

View File

@ -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": "研究过程中出错"
}
}

View File

@ -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 (
<Container>
<Navbar>
<NavbarCenter>{t('deepresearch.title', 'Deep Research')}</NavbarCenter>
</Navbar>
<Content>
<DeepResearchPanel />
</Content>
</Container>
)
}
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

View File

@ -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}
/>
</SettingGroup>
<EnableDeepResearch />
<SettingGroup theme={theme}>
<SettingTitle>
{t('settings.display.custom.css')}

View File

@ -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 (
<SettingGroup theme={themeMode}>
<SettingTitle>
<IconWrapper>
<Microscope size={18} />
</IconWrapper>
{t('deepresearch.title', '深度研究')}
</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
{t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')}
</SettingRowTitle>
{!isDeepResearchEnabled ? (
<Button type="primary" onClick={handleEnableDeepResearch}>
{t('deepresearch.enable', '启用深度研究')}
</Button>
) : (
<EnabledText>{t('deepresearch.already_enabled', '已启用')}</EnabledText>
)}
</SettingRow>
</SettingGroup>
)
}
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

View File

@ -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<SidebarIconsManagerProps> = ({
translate: <Languages size={16} />,
minapp: <LayoutGrid size={16} />,
knowledge: <LibraryBig size={16} />,
files: <Folder size={15} />
files: <Folder size={15} />,
workspace: <FolderGit size={15} />,
deepresearch: <Microscope size={16} />
}),
[]
)

View File

@ -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<Model | null>(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 (
<SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.websearch.deep_research.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
{t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')}
<SubDescription>
{t('deepresearch.engine_rotation', '每次迭代使用不同类别的搜索引擎:中文、国际、元搜索和学术搜索')}
</SubDescription>
</SettingRowTitle>
<Button type="primary" onClick={handleOpenDeepResearch}>
{t('deepresearch.open', '打开深度研究')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.model.select', '选择模型')}</SettingRowTitle>
<Button onClick={handleSelectModel}>
{selectedModel ? (
<Space>
<ModelAvatar model={selectedModel} size={20} />
<span>{selectedModel.name}</span>
</Space>
) : (
t('settings.model.select_model', '选择模型')
)}
</Button>
</SettingRow>
<SettingRow>
<SettingRowTitle>{t('settings.websearch.deep_research.max_iterations')}</SettingRowTitle>
<InputNumber min={1} max={10} value={deepResearchConfig.maxIterations} onChange={handleMaxIterationsChange} />
</SettingRow>
<SettingRow>
<SettingRowTitle>{t('settings.websearch.deep_research.max_results_per_query')}</SettingRowTitle>
<InputNumber
min={1}
max={50}
value={deepResearchConfig.maxResultsPerQuery}
onChange={handleMaxResultsPerQueryChange}
/>
</SettingRow>
<SettingRow>
<SettingRowTitle>{t('settings.websearch.deep_research.auto_summary')}</SettingRowTitle>
<Switch checked={deepResearchConfig.autoSummary} onChange={handleAutoSummaryChange} />
</SettingRow>
<SettingRow>
<SettingRowTitle>
{t('settings.websearch.deep_research.enable_query_optimization', '启用查询优化')}
<SubDescription>
{t(
'settings.websearch.deep_research.query_optimization_desc',
'使用 AI 分析您的问题并生成更有效的搜索查询'
)}
</SubDescription>
</SettingRowTitle>
<Switch checked={deepResearchConfig.enableQueryOptimization} onChange={handleQueryOptimizationChange} />
</SettingRow>
</SettingGroup>
)
}
export default DeepResearchSettings

View File

@ -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 (
<SettingGroup theme={themeMode}>
<SettingTitle>
<IconWrapper>
<Microscope size={18} />
</IconWrapper>
{t('deepresearch.title', '深度研究')}
</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
{t('deepresearch.description', '通过多轮搜索、分析和总结,提供全面的研究报告')}
</SettingRowTitle>
<Button type="primary" onClick={handleOpenDeepResearch}>
{t('deepresearch.open', '打开深度研究')}
</Button>
</SettingRow>
</SettingGroup>
)
}
const IconWrapper = styled.span`
margin-right: 8px;
display: inline-flex;
align-items: center;
`
export default DeepResearchShortcut

View File

@ -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 (
<SettingGroup theme={themeMode}>
<SettingTitle>{t('settings.websearch.deepsearch.title', 'DeepSearch 设置')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
{t('settings.websearch.deepsearch.description', '选择要在 DeepSearch 中使用的搜索引擎')}
<SubDescription>
{t('settings.websearch.deepsearch.subdescription', '选择的搜索引擎将在 DeepSearch 中并行使用,不会影响 DeepResearch')}
</SubDescription>
</SettingRowTitle>
<div style={{ display: 'flex', flexDirection: 'row', gap: '20px' }}>
<Space direction="vertical">
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}></div>
<Checkbox
checked={enabledEngines.baidu}
onChange={(e) => handleEngineChange('baidu', e.target.checked)}
>
(Baidu)
</Checkbox>
<Checkbox
checked={enabledEngines.sogou}
onChange={(e) => handleEngineChange('sogou', e.target.checked)}
>
(Sogou)
</Checkbox>
<Checkbox
checked={enabledEngines['360']}
onChange={(e) => handleEngineChange('360', e.target.checked)}
>
360
</Checkbox>
<Checkbox
checked={enabledEngines.yisou}
onChange={(e) => handleEngineChange('yisou', e.target.checked)}
>
(Yisou)
</Checkbox>
</Space>
<Space direction="vertical">
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}></div>
<Checkbox
checked={enabledEngines.bing}
onChange={(e) => handleEngineChange('bing', e.target.checked)}
>
(Bing)
</Checkbox>
<Checkbox
checked={enabledEngines.duckduckgo}
onChange={(e) => handleEngineChange('duckduckgo', e.target.checked)}
>
DuckDuckGo
</Checkbox>
<Checkbox
checked={enabledEngines.brave}
onChange={(e) => handleEngineChange('brave', e.target.checked)}
>
Brave Search
</Checkbox>
<Checkbox
checked={enabledEngines.qwant}
onChange={(e) => handleEngineChange('qwant', e.target.checked)}
>
Qwant
</Checkbox>
</Space>
<Space direction="vertical">
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}></div>
<Checkbox
checked={enabledEngines.searx}
onChange={(e) => handleEngineChange('searx', e.target.checked)}
>
SearX
</Checkbox>
<Checkbox
checked={enabledEngines.ecosia}
onChange={(e) => handleEngineChange('ecosia', e.target.checked)}
>
Ecosia
</Checkbox>
<Checkbox
checked={enabledEngines.startpage}
onChange={(e) => handleEngineChange('startpage', e.target.checked)}
>
Startpage
</Checkbox>
<Checkbox
checked={enabledEngines.mojeek}
onChange={(e) => handleEngineChange('mojeek', e.target.checked)}
>
Mojeek
</Checkbox>
</Space>
<Space direction="vertical">
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}></div>
<Checkbox
checked={enabledEngines.scholar}
onChange={(e) => handleEngineChange('scholar', e.target.checked)}
>
Google Scholar
</Checkbox>
<Checkbox
checked={enabledEngines.semantic}
onChange={(e) => handleEngineChange('semantic', e.target.checked)}
>
Semantic Scholar
</Checkbox>
<Checkbox
checked={enabledEngines.base}
onChange={(e) => handleEngineChange('base', e.target.checked)}
>
BASE
</Checkbox>
<Checkbox
checked={enabledEngines.cnki}
onChange={(e) => handleEngineChange('cnki', e.target.checked)}
>
CNKI
</Checkbox>
</Space>
</div>
</SettingRow>
</SettingGroup>
)
}
export default DeepSearchSettings

View File

@ -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 = () => {
)}
<BasicSettings />
<BlacklistSettings />
<DeepSearchSettings />
<DeepResearchSettings />
</SettingContainer>
)
}

View File

@ -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<WebSearchResponse> {
// 调用 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<string> {
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<ResearchReport> {
// 确保 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<string>()
// 定义搜索引擎类别列表
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<string> {
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<string> {
try {
console.log(`[DeepResearch] 使用模型分析搜索结果`)
// 分析提示词
const prompt = `你是一个高级研究分析师,负责深入分析搜索结果并提取全面、详细的见解。
"${query}"
1. 5-83-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<string[]> {
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<string> {
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<string> {
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<string[]> {
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<AnalysisConfig>): void {
this.analysisConfig = {
...this.analysisConfig,
...config
}
}
}
export default DeepResearchProvider

View File

@ -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)
}

View File

@ -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: <WorkspacePage />
},
{
path: '/deepresearch',
element: <DeepResearchPage />
},
{
path: '/settings/*',
element: <SettingsPage />
@ -80,6 +85,7 @@ export const RouterComponent = ({ children }: { children?: React.ReactNode }) =>
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/workspace" element={<WorkspacePage />} />
<Route path="/deepresearch" element={<DeepResearchPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
{children}

View File

@ -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<boolean>) => {
state.enableBackspaceDeleteModel = action.payload
},
// DeepResearch 设置
setEnableDeepResearch: (state, action: PayloadAction<boolean>) => {
state.enableDeepResearch = action.payload
},
setDeepResearchShortcut: (state, action: PayloadAction<string>) => {
state.deepResearchShortcut = action.payload
},
setDeepResearchMaxDepth: (state, action: PayloadAction<number>) => {
state.deepResearchMaxDepth = action.payload
},
setDeepResearchMaxUrls: (state, action: PayloadAction<number>) => {
state.deepResearchMaxUrls = action.payload
},
setDeepResearchTimeLimit: (state, action: PayloadAction<number>) => {
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

View File

@ -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

View File

@ -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 = {

869
yarn.lock

File diff suppressed because it is too large Load Diff