Compare commits

...

7 Commits

Author SHA1 Message Date
1600822305
41ef576d0a 6 2025-04-24 04:24:42 +08:00
1600822305
4d605005a7 升级了下筛选 2025-04-24 01:41:10 +08:00
1600822305
d2019a32aa Merge branch 'deepsearch-2' of https://github.com/CherryHQ/cherry-studio into deepsearch-2 2025-04-24 01:35:57 +08:00
1600822305
05d110e4af 升级了下筛选 2025-04-24 01:34:35 +08:00
1600822305
e2a140a99a Delete index.html 2025-04-24 01:12:31 +08:00
1600822305
d33e16fa81 deepsearch 2025-04-24 01:04:27 +08:00
1600822305
1b2d15f2e8 Add files via upload 2025-04-24 00:49:47 +08:00
29 changed files with 3932 additions and 10 deletions

View File

@@ -159,7 +159,7 @@ class McpService {
} else if (server.type === 'sse') {
const options: SSEClientTransportOptions = {
eventSourceInit: {
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} }),
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} })
},
requestInit: {
headers: server.headers || {}

View File

@@ -6,7 +6,9 @@ import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import GeminiInitializer from './components/GeminiInitializer'
import TopViewContainer from './components/TopView'
import WebSearchInitializer from './components/WebSearchInitializer'
import AntdProvider from './context/AntdProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { SyntaxHighlighterProvider } from './context/SyntaxHighlighterProvider'
@@ -14,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'
@@ -29,6 +32,8 @@ function App(): React.ReactElement {
<AntdProvider>
<SyntaxHighlighterProvider>
<PersistGate loading={null} persistor={persistor}>
<GeminiInitializer />
<WebSearchInitializer />
<TopViewContainer>
<HashRouter>
<NavigationHandler />
@@ -41,6 +46,7 @@ function App(): React.ReactElement {
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/deepresearch" element={<DeepResearchPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>

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,3 @@
import DeepResearchPanel from './DeepResearchPanel'
export { 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

@@ -0,0 +1,37 @@
import { RootState } from '@renderer/store'
import { addWebSearchProvider } from '@renderer/store/websearch'
import { WebSearchProvider } from '@renderer/types'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
/**
* WebSearchInitializer组件
* 用于在应用启动时初始化WebSearchService
* 确保DeepSearch在应用启动时被正确设置
*/
const WebSearchInitializer = () => {
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
}
export default WebSearchInitializer

View File

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

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

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

View File

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

View File

@@ -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": "搜尋包含日期",

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

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

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

View File

@@ -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<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状态
* @returns 研究报告
*/
public async research(query: string, websearch?: WebSearchState): Promise<ResearchReport> {
// 确保 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<string>()
// 定义搜索引擎类别列表
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<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)
return analysis
} catch (error: any) {
console.error('[DeepResearch] 分析结果时出错:', error)
return `分析过程中出现错误: ${error?.message || '未知错误'}`
}
}
/**
* 使用项目中的模型分析搜索结果
*/
private async analyzeWithModel(contentSummaries: string, query: string): Promise<string> {
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<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[]): 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. 该领域的当前状态
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<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}"的研究分析中,提取 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<AnalysisConfig>): void {
this.analysisConfig = {
...this.analysisConfig,
...config
}
}
}

View File

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

View File

@@ -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<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: [],
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-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<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
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { WebSearchProvider } from '@renderer/types'
import BaseWebSearchProvider from './BaseWebSearchProvider'
import DeepSearchProvider from './DeepSearchProvider'
import DefaultProvider from './DefaultProvider'
import ExaProvider from './ExaProvider'
import LocalBaiduProvider from './LocalBaiduProvider'
@@ -18,6 +19,8 @@ export default class WebSearchProviderFactory {
return new SearxngProvider(provider)
case 'exa':
return new ExaProvider(provider)
case 'deep-search':
return new DeepSearchProvider(provider)
case 'local-google':
return new LocalGoogleProvider(provider)
case 'local-baidu':

View File

@@ -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<string> {
const model = getDefaultModel()
export async function fetchGenerate({
prompt,
content,
modelId
}: {
prompt: string
content: string
modelId?: string
}): Promise<string> {
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 ''
}
}

View File

@@ -45,6 +45,11 @@ class WebSearchService {
return provider.apiHost !== ''
}
// DeepSearch提供商不需要API密钥或主机
if (provider.id === 'deep-search') {
return true
}
return false
}

View File

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

View File

@@ -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 = {
@@ -43,6 +53,13 @@ const initialState: WebSearchState = {
name: 'Exa',
apiKey: ''
},
{
id: 'deep-search',
name: 'DeepSearch',
usingBrowser: true,
contentLimit: 10000,
description: '多引擎深度搜索'
},
{
id: 'local-google',
name: 'Google',
@@ -60,10 +77,16 @@ const initialState: WebSearchState = {
}
],
searchWithTime: true,
maxResults: 5,
maxResults: 10,
excludeDomains: [],
subscribeSources: [],
overwrite: false
overwrite: false,
deepResearchConfig: {
maxIterations: 3,
maxResultsPerQuery: 5,
autoSummary: true,
enableQueryOptimization: true
}
}
export const defaultWebSearchProviders = initialState.providers
@@ -137,6 +160,9 @@ const websearchSlice = createSlice({
},
setContentLimit: (state, action: PayloadAction<number>) => {
state.contentLimit = action.payload
},
setDeepResearchConfig: (state, action: PayloadAction<DeepResearchConfig>) => {
state.deepResearchConfig = action.payload
}
}
})
@@ -155,7 +181,8 @@ export const {
setSubscribeSources,
setOverwrite,
addWebSearchProvider,
setContentLimit
setContentLimit,
setDeepResearchConfig
} = websearchSlice.actions
export default websearchSlice.reducer

View File

@@ -351,6 +351,7 @@ export type WebSearchProvider = {
basicAuthPassword?: string
contentLimit?: number
usingBrowser?: boolean
description?: string
}
export type WebSearchResponse = {
@@ -362,6 +363,24 @@ export type WebSearchResult = {
title: string
content: string
url: string
source?: string
meta?: Record<string, any> // 添加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 = {
@@ -512,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
}
}