This commit is contained in:
1600822305 2025-04-24 04:24:42 +08:00
parent 4d605005a7
commit 41ef576d0a
24 changed files with 2570 additions and 50 deletions

View File

@ -16,6 +16,7 @@ import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import DeepResearchPage from './pages/deepresearch/DeepResearchPage'
import FilesPage from './pages/files/FilesPage'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
@ -45,6 +46,7 @@ function App(): React.ReactElement {
<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

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

View File

@ -15,60 +15,74 @@ import BaseWebSearchProvider from './BaseWebSearchProvider'
export default class DeepSearchProvider extends BaseWebSearchProvider {
// 定义默认的搜索引擎URLs
private searchEngines = [
{ name: 'Baidu', url: 'https://www.baidu.com/s?wd=%s' },
{ name: 'Bing', url: 'https://cn.bing.com/search?q=%s&ensearch=1' },
{ name: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=%s&t=h_' },
{ name: 'Sogou', url: 'https://www.sogou.com/web?query=%s' },
// 中文搜索引擎
{ name: 'Baidu', url: 'https://www.baidu.com/s?wd=%s', category: 'chinese' },
{ name: 'Sogou', url: 'https://www.sogou.com/web?query=%s', category: 'chinese' },
{ name: '360', url: 'https://www.so.com/s?q=%s', category: 'chinese' },
{ name: 'Yisou', url: 'https://yisou.com/search?q=%s', category: 'chinese' },
// 国际搜索引擎
{ name: 'Bing', url: 'https://cn.bing.com/search?q=%s&ensearch=1', category: 'international' },
{ name: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=%s&t=h_', category: 'international' },
{ name: 'Brave', url: 'https://search.brave.com/search?q=%s', category: 'international' },
{ name: 'Qwant', url: 'https://www.qwant.com/?q=%s', category: 'international' },
// 元搜索引擎
{
name: 'SearX',
url: 'https://searx.tiekoetter.com/search?q=%s&categories=general&language=auto&time_range=&safesearch=0&theme=simple'
}
url: 'https://searx.tiekoetter.com/search?q=%s&categories=general&language=auto',
category: 'meta'
},
{ name: 'Ecosia', url: 'https://www.ecosia.org/search?q=%s', category: 'meta' },
{ name: 'Startpage', url: 'https://www.startpage.com/do/search?q=%s', category: 'meta' },
{ name: 'Mojeek', url: 'https://www.mojeek.com/search?q=%s', category: 'meta' },
// 学术搜索引擎
{ name: 'Scholar', url: 'https://scholar.google.com/scholar?q=%s', category: 'academic' },
{ name: 'Semantic', url: 'https://www.semanticscholar.org/search?q=%s', category: 'academic' },
{ name: 'BASE', url: 'https://www.base-search.net/Search/Results?lookfor=%s', category: 'academic' },
{ name: 'CNKI', url: 'https://kns.cnki.net/kns/brief/Default_Result.aspx?code=SCDB&kw=%s', category: 'academic' }
]
// 定义URL过滤规则
private urlFilters = {
// 排除的域名
// 排除的域名(增强版)
excludedDomains: [
// 账户相关
'login',
'signin',
'signup',
'register',
'account',
'download',
'shop',
'store',
'buy',
'cart',
'checkout',
// 广告相关
'ads',
'ad.',
'adv.',
'advertisement',
'sponsor',
'tracking',
'facebook.com',
'twitter.com',
'instagram.com',
'pinterest.com',
'youtube.com/channel',
'youtube.com/user',
'youtube.com/c/',
'tiktok.com',
'douyin.com',
'weibo.com',
'zhihu.com/question',
'baike.baidu.com',
'wiki.com',
'wikipedia.org/wiki/Help:',
'wikipedia.org/wiki/Wikipedia:',
'wikipedia.org/wiki/Template:',
'wikipedia.org/wiki/File:',
'wikipedia.org/wiki/Category:',
'amazon.com/s',
'amazon.cn/s',
'taobao.com/search',
'jd.com/search',
'tmall.com/search',
'ebay.com/sch',
'aliexpress.com/wholesale'
'promotion',
'marketing',
'banner',
'popup',
// 购物相关
'cart',
'shop',
'store',
'buy',
'price',
'deal',
'coupon',
'discount',
// 社交媒体评论区
'comment',
'comments',
'forum',
'bbs'
],
// 优先的域名(相关性更高)
priorityDomains: [
@ -143,7 +157,7 @@ export default class DeepSearchProvider extends BaseWebSearchProvider {
// 不再强制要求provider.url因为我们有默认的搜索引擎
}
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
public async search(query: string, websearch: WebSearchState, engineCategory?: string): Promise<WebSearchResponse> {
try {
if (!query.trim()) {
throw new Error('Search query cannot be empty')
@ -155,8 +169,22 @@ export default class DeepSearchProvider extends BaseWebSearchProvider {
// 存储所有搜索引擎的结果
const allItems: Array<{ title: string; url: string; source: string }> = []
// 并行搜索所有引擎
const searchPromises = this.searchEngines.map(async (engine) => {
// 根据类别筛选搜索引擎
let enginesToUse = this.searchEngines
// 如果指定了类别,则只使用该类别的搜索引擎
if (engineCategory) {
enginesToUse = this.searchEngines.filter((engine) => engine.category === engineCategory)
// 如果该类别没有搜索引擎,则使用所有搜索引擎
if (enginesToUse.length === 0) {
enginesToUse = this.searchEngines
}
}
console.log(`[DeepSearch] 使用${engineCategory || '所有'}类别的搜索引擎,共 ${enginesToUse.length}`)
// 并行搜索选定的引擎
const searchPromises = enginesToUse.map(async (engine) => {
try {
const uid = `deep-search-${engine.name.toLowerCase()}-${nanoid()}`
const url = engine.url.replace('%s', encodeURIComponent(cleanedQuery))
@ -291,6 +319,93 @@ export default class DeepSearchProvider extends BaseWebSearchProvider {
}
}
/**
*
* @param content
* @returns truefalse
*/
private containsGarbage(content: string): boolean {
if (!content) return true
// 检测内容长度
if (content.length < 50) return true // 内容过短
// 检测乱码字符比例
// 使用安全的方式检测乱码字符
// 手动检测不可打印字符而不使用正则表达式
let nonReadableCharsCount = 0
for (let i = 0; i < content.length; i++) {
const charCode = content.charCodeAt(i)
// 检测控制字符和特殊字符
if (
(charCode >= 0 && charCode <= 8) ||
charCode === 11 ||
charCode === 12 ||
(charCode >= 14 && charCode <= 31) ||
(charCode >= 127 && charCode <= 159) ||
charCode === 0xfffd ||
charCode === 0xfffe ||
charCode === 0xffff
) {
nonReadableCharsCount++
}
}
if (nonReadableCharsCount > content.length * 0.05) {
return true // 乱码字符超过5%
}
// 检测重复模式
const repeatedPatterns = content.match(/(.{10,})\1{3,}/g)
if (repeatedPatterns && repeatedPatterns.length > 0) {
return true // 存在多次重复的长模式
}
// 检测广告关键词
const adKeywords = [
'advertisement',
'sponsored',
'promotion',
'discount',
'sale',
'buy now',
'limited time',
'special offer',
'click here',
'best price',
'free shipping',
'广告',
'促销',
'特惠',
'打折',
'限时',
'点击购买',
'立即购买'
]
const contentLower = content.toLowerCase()
const adKeywordCount = adKeywords.filter((keyword) => contentLower.includes(keyword)).length
if (adKeywordCount >= 3) {
return true // 包含多个广告关键词
}
// 检测内容多样性(字符类型比例)
const letters = content.match(/[a-zA-Z]/g)?.length || 0
const digits = content.match(/\d/g)?.length || 0
const spaces = content.match(/\s/g)?.length || 0
const punctuation = content.match(/[.,;:!?]/g)?.length || 0
// 如果内容几乎只有一种字符类型,可能是乱码
const totalChars = content.length
const mainCharType = Math.max(letters, digits, spaces, punctuation)
if (mainCharType / totalChars > 0.9) {
return true // 单一字符类型超过90%
}
return false
}
/**
*
* @param results
@ -623,10 +738,19 @@ export default class DeepSearchProvider extends BaseWebSearchProvider {
return scoreB - scoreA
})
// 过滤掉明显不相关的结果,提高阈值以只保留更相关的结果
// 过滤掉明显不相关的结果和乱码内容
const filteredResults = analyzedResults.filter((result) => {
// 检查相关性分数
const score = (result as AnalyzedResult).relevanceScore || 0
return score > 0.2 // 提高阈值到 0.2,只保留相关性分数较高的结果
if (score <= 0.05) return false // 相关性分数过低
// 检查是否包含乱码或广告
if (this.containsGarbage(result.content)) {
console.log(`[DeepSearch] 过滤乱码或广告内容: ${result.title}`)
return false
}
return true
})
console.log(`[DeepSearch] 完成分析 ${results.length} 个结果,过滤后剩余 ${filteredResults.length} 个结果`)

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

@ -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 = {
@ -70,7 +80,13 @@ const initialState: WebSearchState = {
maxResults: 10,
excludeDomains: [],
subscribeSources: [],
overwrite: false
overwrite: false,
deepResearchConfig: {
maxIterations: 3,
maxResultsPerQuery: 5,
autoSummary: true,
enableQueryOptimization: true
}
}
export const defaultWebSearchProviders = initialState.providers
@ -144,6 +160,9 @@ const websearchSlice = createSlice({
},
setContentLimit: (state, action: PayloadAction<number>) => {
state.contentLimit = action.payload
},
setDeepResearchConfig: (state, action: PayloadAction<DeepResearchConfig>) => {
state.deepResearchConfig = action.payload
}
}
})
@ -162,7 +181,8 @@ export const {
setSubscribeSources,
setOverwrite,
addWebSearchProvider,
setContentLimit
setContentLimit,
setDeepResearchConfig
} = websearchSlice.actions
export default websearchSlice.reducer

View File

@ -367,6 +367,22 @@ export type WebSearchResult = {
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 = {
id: number
content: string
@ -515,3 +531,25 @@ export interface Citation {
}
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
// Deep Research 类型定义
export interface ResearchIteration {
query: string
results: WebSearchResult[]
analysis: string
followUpQueries: string[]
}
export interface ResearchReport {
originalQuery: string
iterations: ResearchIteration[]
summary: string
directAnswer: string // 对原始问题的直接回答
keyInsights: string[]
sources: string[]
tokenUsage?: {
inputTokens: number
outputTokens: number
totalTokens: number
}
}