mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
升级
This commit is contained in:
parent
d4b2d53118
commit
68541a769e
@ -101,6 +101,8 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@sentry/electron": "^6.5.0",
|
||||
"@sentry/react": "^9.14.0",
|
||||
"@shikijs/markdown-it": "^3.2.2",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
|
||||
@ -5,6 +5,7 @@ import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import DeepClaudeProvider from './components/DeepClaudeProvider'
|
||||
import GeminiInitializer from './components/GeminiInitializer'
|
||||
import MemoryProvider from './components/MemoryProvider'
|
||||
import PDFSettingsInitializer from './components/PDFSettingsInitializer'
|
||||
import WebSearchInitializer from './components/WebSearchInitializer'
|
||||
@ -26,6 +27,7 @@ function App(): React.ReactElement {
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<MemoryProvider>
|
||||
<DeepClaudeProvider />
|
||||
<GeminiInitializer />
|
||||
<PDFSettingsInitializer />
|
||||
<WebSearchInitializer />
|
||||
<WorkspaceInitializer />
|
||||
|
||||
@ -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;
|
||||
}
|
||||
472
src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx
Normal file
472
src/renderer/src/components/DeepResearch/DeepResearchPanel.tsx
Normal 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
|
||||
4
src/renderer/src/components/DeepResearch/index.ts
Normal file
4
src/renderer/src/components/DeepResearch/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import DeepResearchPanel from './DeepResearchPanel'
|
||||
|
||||
export { DeepResearchPanel }
|
||||
export default DeepResearchPanel
|
||||
35
src/renderer/src/components/GeminiInitializer.tsx
Normal file
35
src/renderer/src/components/GeminiInitializer.tsx
Normal 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
|
||||
@ -1,19 +1,36 @@
|
||||
import { RootState } from '@renderer/store'
|
||||
import { addWebSearchProvider } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider } from '@renderer/types'
|
||||
import { useEffect } from 'react'
|
||||
import WebSearchService from '@renderer/services/WebSearchService'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
/**
|
||||
* 初始化WebSearch服务的组件
|
||||
* 确保DeepSearch供应商被添加到列表中
|
||||
* WebSearchInitializer组件
|
||||
* 用于在应用启动时初始化WebSearchService
|
||||
* 确保DeepSearch在应用启动时被正确设置
|
||||
*/
|
||||
const WebSearchInitializer = () => {
|
||||
useEffect(() => {
|
||||
// 触发WebSearchService的初始化
|
||||
// 这将确保DeepSearch供应商被添加到列表中
|
||||
WebSearchService.getWebSearchProvider()
|
||||
console.log('[WebSearchInitializer] 初始化WebSearch服务')
|
||||
}, [])
|
||||
const dispatch = useDispatch()
|
||||
const providers = useSelector((state: RootState) => state.websearch.providers)
|
||||
|
||||
// 这个组件不渲染任何内容
|
||||
useEffect(() => {
|
||||
// 检查是否已经存在DeepSearch提供商
|
||||
const hasDeepSearch = providers.some((provider) => provider.id === 'deep-search')
|
||||
|
||||
// 如果不存在,添加DeepSearch提供商
|
||||
if (!hasDeepSearch) {
|
||||
const deepSearchProvider: WebSearchProvider = {
|
||||
id: 'deep-search',
|
||||
name: 'DeepSearch',
|
||||
usingBrowser: true,
|
||||
contentLimit: 10000,
|
||||
description: '多引擎深度搜索'
|
||||
}
|
||||
dispatch(addWebSearchProvider(deepSearchProvider))
|
||||
}
|
||||
}, [dispatch, providers])
|
||||
|
||||
// 这是一个初始化组件,不需要渲染任何UI
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
Languages,
|
||||
LayoutGrid,
|
||||
MessageSquareQuote,
|
||||
Microscope,
|
||||
Moon,
|
||||
Palette,
|
||||
Settings,
|
||||
@ -138,7 +139,8 @@ const MainMenus: FC = () => {
|
||||
minapp: <LayoutGrid size={18} className="icon" />,
|
||||
knowledge: <FileSearch size={18} className="icon" />,
|
||||
files: <Folder size={17} className="icon" />,
|
||||
workspace: <FolderGit size={17} className="icon" />
|
||||
workspace: <FolderGit size={17} className="icon" />,
|
||||
deepresearch: <Microscope size={18} className="icon" />
|
||||
}
|
||||
|
||||
const pathMap = {
|
||||
@ -149,7 +151,8 @@ const MainMenus: FC = () => {
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files',
|
||||
workspace: '/workspace'
|
||||
workspace: '/workspace',
|
||||
deepresearch: '/deepresearch'
|
||||
}
|
||||
|
||||
return sidebarIcons.visible.map((icon) => {
|
||||
|
||||
13
src/renderer/src/hooks/useWebSearchStore.ts
Normal file
13
src/renderer/src/hooks/useWebSearchStore.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -356,6 +356,44 @@
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
},
|
||||
"deepresearch": {
|
||||
"title": "Deep Research",
|
||||
"description": "Deep Research provides comprehensive research reports through multiple rounds of search, analysis, and summarization.",
|
||||
"engine_rotation": "Use different categories of search engines for each iteration: Chinese, International, Metasearch, and Academic",
|
||||
"startResearch": "Start Deep Research",
|
||||
"open": "Open Deep Research",
|
||||
"open_success": "Opening Deep Research page",
|
||||
"open_error": "Failed to open Deep Research page",
|
||||
"history": "History",
|
||||
"exportReport": "Export Report",
|
||||
"maxIterations": "Max Iterations",
|
||||
"researchLoading": "Conducting Deep Research",
|
||||
"iteration": "Iteration",
|
||||
"directAnswer": "Answer",
|
||||
"keyInsights": "Key Insights",
|
||||
"summary": "Research Summary",
|
||||
"researchIterations": "Research Iterations",
|
||||
"sources": "Sources",
|
||||
"searchResults": "Search Results",
|
||||
"analysis": "Analysis",
|
||||
"followUpQueries": "Follow-up Queries",
|
||||
"historyTitle": "Research History",
|
||||
"load": "Load",
|
||||
"export": "Export",
|
||||
"noHistory": "No history records",
|
||||
"date": "Date",
|
||||
"iterationCount": "Iteration Count",
|
||||
"tokenStats": "Token Statistics",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"total": "Total",
|
||||
"error": {
|
||||
"emptyQuery": "Please enter a research query",
|
||||
"noProvider": "Please select a search provider",
|
||||
"providerNotFound": "Selected search provider not found",
|
||||
"researchFailed": "Error during research process"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "Backup file format error",
|
||||
"chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers",
|
||||
@ -1659,7 +1697,15 @@
|
||||
"overwrite": "Override search service",
|
||||
"overwrite_tooltip": "Force use search service instead of LLM",
|
||||
"apikey": "API key",
|
||||
"free": "Free"
|
||||
"free": "Free",
|
||||
"deep_research": {
|
||||
"title": "Deep Research",
|
||||
"max_iterations": "Maximum Iterations",
|
||||
"max_results_per_query": "Maximum Results Per Query",
|
||||
"auto_summary": "Auto Summary",
|
||||
"enable_query_optimization": "Enable Query Optimization",
|
||||
"query_optimization_desc": "Use AI to analyze your question and generate more effective search queries"
|
||||
}
|
||||
},
|
||||
"quickPhrase": {
|
||||
"title": "Quick Phrases",
|
||||
|
||||
@ -358,6 +358,44 @@
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
},
|
||||
"deepresearch": {
|
||||
"title": "深度研究",
|
||||
"description": "深度研究功能通过多轮搜索、分析和总结,为您提供全面的研究报告。",
|
||||
"engine_rotation": "每次迭代使用不同类别的搜索引擎:中文、国际、元搜索和学术搜索",
|
||||
"startResearch": "开始深度研究",
|
||||
"open": "打开深度研究",
|
||||
"open_success": "正在打开深度研究页面",
|
||||
"open_error": "打开深度研究页面失败",
|
||||
"history": "历史记录",
|
||||
"exportReport": "导出报告",
|
||||
"maxIterations": "最大迭代次数",
|
||||
"researchLoading": "正在进行深度研究",
|
||||
"iteration": "迭代",
|
||||
"directAnswer": "问题回答",
|
||||
"keyInsights": "关键见解",
|
||||
"summary": "研究总结",
|
||||
"researchIterations": "研究迭代",
|
||||
"sources": "信息来源",
|
||||
"searchResults": "搜索结果",
|
||||
"analysis": "分析",
|
||||
"followUpQueries": "后续查询",
|
||||
"historyTitle": "历史研究记录",
|
||||
"load": "加载",
|
||||
"export": "导出",
|
||||
"noHistory": "暂无历史记录",
|
||||
"date": "日期",
|
||||
"iterationCount": "迭代次数",
|
||||
"tokenStats": "Token统计",
|
||||
"input": "输入",
|
||||
"output": "输出",
|
||||
"total": "总计",
|
||||
"error": {
|
||||
"emptyQuery": "请输入研究查询",
|
||||
"noProvider": "请选择搜索提供商",
|
||||
"providerNotFound": "找不到选定的搜索提供商",
|
||||
"researchFailed": "研究过程中出错"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"backup.file_format": "备份文件格式错误",
|
||||
"chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥",
|
||||
@ -1766,7 +1804,15 @@
|
||||
},
|
||||
"title": "网络搜索",
|
||||
"apikey": "API 密钥",
|
||||
"free": "免费"
|
||||
"free": "免费",
|
||||
"deep_research": {
|
||||
"title": "深度研究",
|
||||
"max_iterations": "最大迭代次数",
|
||||
"max_results_per_query": "每次查询最大结果数",
|
||||
"auto_summary": "自动生成摘要",
|
||||
"enable_query_optimization": "启用查询优化",
|
||||
"query_optimization_desc": "使用 AI 分析您的问题并生成更有效的搜索查询"
|
||||
}
|
||||
},
|
||||
"quickPhrase": {
|
||||
"title": "快捷短语",
|
||||
|
||||
34
src/renderer/src/locales/en/deepresearch.json
Normal file
34
src/renderer/src/locales/en/deepresearch.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Deep Research",
|
||||
"description": "Deep Research provides comprehensive research reports through multiple rounds of search, analysis, and summarization.",
|
||||
"startResearch": "Start Deep Research",
|
||||
"history": "History",
|
||||
"exportReport": "Export Report",
|
||||
"maxIterations": "Max Iterations",
|
||||
"researchLoading": "Conducting Deep Research",
|
||||
"iteration": "Iteration",
|
||||
"directAnswer": "Answer",
|
||||
"keyInsights": "Key Insights",
|
||||
"summary": "Research Summary",
|
||||
"researchIterations": "Research Iterations",
|
||||
"sources": "Sources",
|
||||
"searchResults": "Search Results",
|
||||
"analysis": "Analysis",
|
||||
"followUpQueries": "Follow-up Queries",
|
||||
"historyTitle": "Research History",
|
||||
"load": "Load",
|
||||
"export": "Export",
|
||||
"noHistory": "No history records",
|
||||
"date": "Date",
|
||||
"iterationCount": "Iteration Count",
|
||||
"tokenStats": "Token Statistics",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"total": "Total",
|
||||
"error": {
|
||||
"emptyQuery": "Please enter a research query",
|
||||
"noProvider": "Please select a search provider",
|
||||
"providerNotFound": "Selected search provider not found",
|
||||
"researchFailed": "Error during research process"
|
||||
}
|
||||
}
|
||||
34
src/renderer/src/locales/zh-CN/deepresearch.json
Normal file
34
src/renderer/src/locales/zh-CN/deepresearch.json
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "深度研究",
|
||||
"description": "深度研究功能通过多轮搜索、分析和总结,为您提供全面的研究报告。",
|
||||
"startResearch": "开始深度研究",
|
||||
"history": "历史记录",
|
||||
"exportReport": "导出报告",
|
||||
"maxIterations": "最大迭代次数",
|
||||
"researchLoading": "正在进行深度研究",
|
||||
"iteration": "迭代",
|
||||
"directAnswer": "问题回答",
|
||||
"keyInsights": "关键见解",
|
||||
"summary": "研究总结",
|
||||
"researchIterations": "研究迭代",
|
||||
"sources": "信息来源",
|
||||
"searchResults": "搜索结果",
|
||||
"analysis": "分析",
|
||||
"followUpQueries": "后续查询",
|
||||
"historyTitle": "历史研究记录",
|
||||
"load": "加载",
|
||||
"export": "导出",
|
||||
"noHistory": "暂无历史记录",
|
||||
"date": "日期",
|
||||
"iterationCount": "迭代次数",
|
||||
"tokenStats": "Token统计",
|
||||
"input": "输入",
|
||||
"output": "输出",
|
||||
"total": "总计",
|
||||
"error": {
|
||||
"emptyQuery": "请输入研究查询",
|
||||
"noProvider": "请选择搜索提供商",
|
||||
"providerNotFound": "找不到选定的搜索提供商",
|
||||
"researchFailed": "研究过程中出错"
|
||||
}
|
||||
}
|
||||
36
src/renderer/src/pages/deepresearch/DeepResearchPage.tsx
Normal file
36
src/renderer/src/pages/deepresearch/DeepResearchPage.tsx
Normal 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
|
||||
@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import EnableDeepResearch from './EnableDeepResearch'
|
||||
import SidebarIconsManager from './SidebarIconsManager'
|
||||
|
||||
const DisplaySettings: FC = () => {
|
||||
@ -178,6 +179,8 @@ const DisplaySettings: FC = () => {
|
||||
setDisabledIcons={setDisabledIcons}
|
||||
/>
|
||||
</SettingGroup>
|
||||
|
||||
<EnableDeepResearch />
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>
|
||||
{t('settings.display.custom.css')}
|
||||
|
||||
@ -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
|
||||
@ -10,7 +10,7 @@ import {
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setSidebarIcons } from '@renderer/store/settings'
|
||||
import { message } from 'antd'
|
||||
import { Folder, Languages, LayoutGrid, LibraryBig, MessageSquareQuote, Palette, Sparkle } from 'lucide-react'
|
||||
import { Folder, FolderGit, Languages, LayoutGrid, LibraryBig, MessageSquareQuote, Microscope, Palette, Sparkle } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -115,7 +115,9 @@ const SidebarIconsManager: FC<SidebarIconsManagerProps> = ({
|
||||
translate: <Languages size={16} />,
|
||||
minapp: <LayoutGrid size={16} />,
|
||||
knowledge: <LibraryBig size={16} />,
|
||||
files: <Folder size={15} />
|
||||
files: <Folder size={15} />,
|
||||
workspace: <FolderGit size={15} />,
|
||||
deepresearch: <Microscope size={16} />
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,228 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setDeepSearchConfig } from '@renderer/store/websearch'
|
||||
import { Checkbox, Space } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const SubDescription = styled.div`
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
`
|
||||
|
||||
const DeepSearchSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme: themeMode } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// 从 store 获取 DeepSearch 配置
|
||||
const deepSearchConfig = useAppSelector((state) => state.websearch.deepSearchConfig)
|
||||
|
||||
// 本地状态 - 使用 deepSearchConfig?.enabledEngines 作为初始值,如果不存在则使用默认值
|
||||
const [enabledEngines, setEnabledEngines] = useState(() => deepSearchConfig?.enabledEngines || {
|
||||
// 中文搜索引擎
|
||||
baidu: true,
|
||||
sogou: true,
|
||||
'360': false,
|
||||
yisou: false,
|
||||
|
||||
// 国际搜索引擎
|
||||
bing: true,
|
||||
duckduckgo: true,
|
||||
brave: false,
|
||||
qwant: false,
|
||||
|
||||
// 元搜索引擎
|
||||
searx: true,
|
||||
ecosia: false,
|
||||
startpage: false,
|
||||
mojeek: false,
|
||||
|
||||
// 学术搜索引擎
|
||||
scholar: true,
|
||||
semantic: false,
|
||||
base: false,
|
||||
cnki: false
|
||||
})
|
||||
|
||||
// 当 deepSearchConfig.enabledEngines 的引用发生变化时更新本地状态
|
||||
useEffect(() => {
|
||||
if (deepSearchConfig?.enabledEngines) {
|
||||
// 比较当前状态和新状态,只有当它们不同时才更新
|
||||
const currentKeys = Object.keys(enabledEngines);
|
||||
const newKeys = Object.keys(deepSearchConfig.enabledEngines);
|
||||
|
||||
// 检查键是否相同
|
||||
if (currentKeys.length !== newKeys.length ||
|
||||
!currentKeys.every(key => newKeys.includes(key))) {
|
||||
setEnabledEngines(deepSearchConfig.enabledEngines);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查值是否相同
|
||||
let needsUpdate = false;
|
||||
for (const key of currentKeys) {
|
||||
if (enabledEngines[key] !== deepSearchConfig.enabledEngines[key]) {
|
||||
needsUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
setEnabledEngines(deepSearchConfig.enabledEngines);
|
||||
}
|
||||
}
|
||||
}, [deepSearchConfig?.enabledEngines])
|
||||
|
||||
// 处理搜索引擎选择变化
|
||||
const handleEngineChange = (engine: string, checked: boolean) => {
|
||||
const newEnabledEngines = {
|
||||
...enabledEngines,
|
||||
[engine]: checked
|
||||
}
|
||||
|
||||
setEnabledEngines(newEnabledEngines)
|
||||
|
||||
// 更新 store
|
||||
dispatch(
|
||||
setDeepSearchConfig({
|
||||
enabledEngines: newEnabledEngines
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={themeMode}>
|
||||
<SettingTitle>{t('settings.websearch.deepsearch.title', 'DeepSearch 设置')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.websearch.deepsearch.description', '选择要在 DeepSearch 中使用的搜索引擎')}
|
||||
<SubDescription>
|
||||
{t('settings.websearch.deepsearch.subdescription', '选择的搜索引擎将在 DeepSearch 中并行使用,不会影响 DeepResearch')}
|
||||
</SubDescription>
|
||||
</SettingRowTitle>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '20px' }}>
|
||||
<Space direction="vertical">
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>中文搜索引擎</div>
|
||||
<Checkbox
|
||||
checked={enabledEngines.baidu}
|
||||
onChange={(e) => handleEngineChange('baidu', e.target.checked)}
|
||||
>
|
||||
百度 (Baidu)
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.sogou}
|
||||
onChange={(e) => handleEngineChange('sogou', e.target.checked)}
|
||||
>
|
||||
搜狗 (Sogou)
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines['360']}
|
||||
onChange={(e) => handleEngineChange('360', e.target.checked)}
|
||||
>
|
||||
360搜索
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.yisou}
|
||||
onChange={(e) => handleEngineChange('yisou', e.target.checked)}
|
||||
>
|
||||
一搜 (Yisou)
|
||||
</Checkbox>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical">
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>国际搜索引擎</div>
|
||||
<Checkbox
|
||||
checked={enabledEngines.bing}
|
||||
onChange={(e) => handleEngineChange('bing', e.target.checked)}
|
||||
>
|
||||
必应 (Bing)
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.duckduckgo}
|
||||
onChange={(e) => handleEngineChange('duckduckgo', e.target.checked)}
|
||||
>
|
||||
DuckDuckGo
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.brave}
|
||||
onChange={(e) => handleEngineChange('brave', e.target.checked)}
|
||||
>
|
||||
Brave Search
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.qwant}
|
||||
onChange={(e) => handleEngineChange('qwant', e.target.checked)}
|
||||
>
|
||||
Qwant
|
||||
</Checkbox>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical">
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>元搜索引擎</div>
|
||||
<Checkbox
|
||||
checked={enabledEngines.searx}
|
||||
onChange={(e) => handleEngineChange('searx', e.target.checked)}
|
||||
>
|
||||
SearX
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.ecosia}
|
||||
onChange={(e) => handleEngineChange('ecosia', e.target.checked)}
|
||||
>
|
||||
Ecosia
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.startpage}
|
||||
onChange={(e) => handleEngineChange('startpage', e.target.checked)}
|
||||
>
|
||||
Startpage
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.mojeek}
|
||||
onChange={(e) => handleEngineChange('mojeek', e.target.checked)}
|
||||
>
|
||||
Mojeek
|
||||
</Checkbox>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical">
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '8px' }}>学术搜索引擎</div>
|
||||
<Checkbox
|
||||
checked={enabledEngines.scholar}
|
||||
onChange={(e) => handleEngineChange('scholar', e.target.checked)}
|
||||
>
|
||||
Google Scholar
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.semantic}
|
||||
onChange={(e) => handleEngineChange('semantic', e.target.checked)}
|
||||
>
|
||||
Semantic Scholar
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.base}
|
||||
onChange={(e) => handleEngineChange('base', e.target.checked)}
|
||||
>
|
||||
BASE
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={enabledEngines.cnki}
|
||||
onChange={(e) => handleEngineChange('cnki', e.target.checked)}
|
||||
>
|
||||
CNKI 知网
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</div>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeepSearchSettings
|
||||
@ -9,6 +9,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import BasicSettings from './BasicSettings'
|
||||
import BlacklistSettings from './BlacklistSettings'
|
||||
import DeepResearchSettings from './DeepResearchSettings'
|
||||
import DeepSearchSettings from './DeepSearchSettings'
|
||||
import WebSearchProviderSetting from './WebSearchProviderSetting'
|
||||
|
||||
const WebSearchSettings: FC = () => {
|
||||
@ -57,6 +59,8 @@ const WebSearchSettings: FC = () => {
|
||||
)}
|
||||
<BasicSettings />
|
||||
<BlacklistSettings />
|
||||
<DeepSearchSettings />
|
||||
<DeepResearchSettings />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,777 @@
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import {
|
||||
ResearchIteration,
|
||||
ResearchReport,
|
||||
WebSearchProvider,
|
||||
WebSearchResponse,
|
||||
WebSearchResult
|
||||
} from '@renderer/types'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
import DeepSearchProvider from './DeepSearchProvider'
|
||||
|
||||
/**
|
||||
* 分析配置接口
|
||||
*/
|
||||
interface AnalysisConfig {
|
||||
maxIterations: number
|
||||
maxResultsPerQuery: number
|
||||
minConfidenceScore: number
|
||||
autoSummary: boolean
|
||||
modelId?: string
|
||||
minOutputTokens?: number // 最小输出token数
|
||||
maxInputTokens?: number // 最大输入token数
|
||||
}
|
||||
|
||||
// 使用从 types/index.ts 导入的 ResearchIteration 和 ResearchReport 类型
|
||||
|
||||
/**
|
||||
* DeepResearchProvider 类
|
||||
* 提供深度研究功能,包括多轮搜索、分析和总结
|
||||
*/
|
||||
class DeepResearchProvider extends BaseWebSearchProvider {
|
||||
private deepSearchProvider: DeepSearchProvider
|
||||
private analysisConfig: AnalysisConfig
|
||||
|
||||
constructor(provider: WebSearchProvider) {
|
||||
super(provider)
|
||||
this.deepSearchProvider = new DeepSearchProvider(provider)
|
||||
this.analysisConfig = {
|
||||
maxIterations: 3, // 默认最大迭代次数
|
||||
maxResultsPerQuery: 50, // 每次查询的最大结果数
|
||||
minConfidenceScore: 0.6, // 最小可信度分数
|
||||
autoSummary: true, // 自动生成摘要
|
||||
minOutputTokens: 20000, // 最小输出20,000 tokens
|
||||
maxInputTokens: 200000 // 最大输入200,000 tokens
|
||||
}
|
||||
}
|
||||
|
||||
// 实现 BaseWebSearchProvider 的抽象方法
|
||||
public async search(query: string, websearch: WebSearchState): Promise<WebSearchResponse> {
|
||||
// 调用 research 方法并将结果转换为 WebSearchResponse 格式
|
||||
const researchResults = await this.research(query, websearch)
|
||||
|
||||
// 从研究结果中提取搜索结果
|
||||
const allResults = researchResults.iterations.flatMap((iter) => iter.results)
|
||||
|
||||
// 返回标准的 WebSearchResponse 格式
|
||||
return {
|
||||
query,
|
||||
results: allResults
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化查询
|
||||
* @param query 用户原始查询
|
||||
* @returns 优化后的查询
|
||||
*/
|
||||
private async optimizeQuery(query: string): Promise<string> {
|
||||
try {
|
||||
console.log(`[DeepResearch] 正在优化查询: "${query}"`)
|
||||
|
||||
// 使用模型优化查询
|
||||
const { fetchGenerate } = await import('@renderer/services/ApiService')
|
||||
const prompt = `你是一个搜索优化专家,负责将用户的问题转化为最有效的搜索查询。
|
||||
|
||||
用户问题: "${query}"
|
||||
|
||||
请分析这个问题,并生成一个更有效的搜索查询。你的查询应该:
|
||||
1. 提取关键概念和术语
|
||||
2. 去除不必要的虚词和介词
|
||||
3. 增加相关的同义词或专业术语(如果适用)
|
||||
4. 保持简洁明确,不超过 10 个关键词
|
||||
|
||||
只返回优化后的查询词,不要添加任何解释或其他文本。`
|
||||
|
||||
const optimizedQuery = await fetchGenerate({
|
||||
prompt,
|
||||
content: ' ', // 确保内容不为空
|
||||
modelId: this.analysisConfig.modelId
|
||||
})
|
||||
|
||||
// 如果优化失败,返回原始查询
|
||||
if (!optimizedQuery || optimizedQuery.trim() === '') {
|
||||
return query
|
||||
}
|
||||
|
||||
console.log(`[DeepResearch] 查询优化结果: "${optimizedQuery}"`)
|
||||
return optimizedQuery.trim()
|
||||
} catch (error) {
|
||||
console.error('[DeepResearch] 查询优化失败:', error)
|
||||
return query // 出错时返回原始查询
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行深度研究
|
||||
* @param query 初始查询
|
||||
* @param websearch WebSearch状态
|
||||
* @param progressCallback 进度回调函数
|
||||
* @returns 研究报告
|
||||
*/
|
||||
public async research(
|
||||
query: string,
|
||||
websearch?: WebSearchState,
|
||||
progressCallback?: (iteration: number, status: string, percent: number) => void
|
||||
): Promise<ResearchReport> {
|
||||
// 确保 websearch 存在
|
||||
const webSearchState: WebSearchState = websearch || {
|
||||
defaultProvider: '',
|
||||
providers: [],
|
||||
maxResults: 10,
|
||||
excludeDomains: [],
|
||||
searchWithTime: false,
|
||||
subscribeSources: [],
|
||||
enhanceMode: true,
|
||||
overwrite: false,
|
||||
deepResearchConfig: {
|
||||
maxIterations: this.analysisConfig.maxIterations,
|
||||
maxResultsPerQuery: this.analysisConfig.maxResultsPerQuery,
|
||||
autoSummary: this.analysisConfig.autoSummary || true
|
||||
}
|
||||
}
|
||||
console.log(`[DeepResearch] 开始深度研究: "${query}"`)
|
||||
|
||||
// 根据配置决定是否优化查询
|
||||
let optimizedQuery = query
|
||||
if (webSearchState.deepResearchConfig?.enableQueryOptimization !== false) {
|
||||
console.log(`[DeepResearch] 启用查询优化`)
|
||||
optimizedQuery = await this.optimizeQuery(query)
|
||||
} else {
|
||||
console.log(`[DeepResearch] 未启用查询优化`)
|
||||
}
|
||||
|
||||
const report: ResearchReport = {
|
||||
originalQuery: query,
|
||||
iterations: [],
|
||||
summary: '',
|
||||
directAnswer: '', // 初始化为空字符串
|
||||
keyInsights: [],
|
||||
sources: [],
|
||||
tokenUsage: {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0
|
||||
}
|
||||
}
|
||||
|
||||
let currentQuery = optimizedQuery
|
||||
let iterationCount = 0
|
||||
const allSources = new Set<string>()
|
||||
|
||||
// 定义搜索引擎类别列表
|
||||
const engineCategories = ['chinese', 'international', 'meta', 'academic']
|
||||
|
||||
// 迭代研究过程
|
||||
while (iterationCount < this.analysisConfig.maxIterations) {
|
||||
// 调用进度回调
|
||||
if (progressCallback) {
|
||||
const percent = Math.round((iterationCount / this.analysisConfig.maxIterations) * 100)
|
||||
progressCallback(iterationCount + 1, `迭代 ${iterationCount + 1}: ${currentQuery}`, percent)
|
||||
}
|
||||
|
||||
console.log(`[DeepResearch] 迭代 ${iterationCount + 1}: "${currentQuery}"`)
|
||||
|
||||
// 根据当前迭代选择搜索引擎类别
|
||||
const categoryIndex = iterationCount % engineCategories.length
|
||||
const currentCategory = engineCategories[categoryIndex]
|
||||
console.log(`[DeepResearch] 这一迭代使用 ${currentCategory} 类别的搜索引擎`)
|
||||
|
||||
// 1. 使用DeepSearch获取当前查询的结果,指定搜索引擎类别
|
||||
if (progressCallback) {
|
||||
progressCallback(
|
||||
iterationCount + 1,
|
||||
`正在搜索: ${currentQuery}`,
|
||||
Math.round((iterationCount / this.analysisConfig.maxIterations) * 100)
|
||||
)
|
||||
}
|
||||
const searchResponse = await this.deepSearchProvider.search(currentQuery, webSearchState, currentCategory)
|
||||
|
||||
// 限制结果数量
|
||||
const limitedResults = searchResponse.results.slice(0, this.analysisConfig.maxResultsPerQuery)
|
||||
|
||||
// 2. 分析搜索结果
|
||||
if (progressCallback) {
|
||||
progressCallback(
|
||||
iterationCount + 1,
|
||||
`正在分析 ${limitedResults.length} 个结果...`,
|
||||
Math.round((iterationCount / this.analysisConfig.maxIterations) * 100)
|
||||
)
|
||||
}
|
||||
const analysis = await this.analyzeResults(limitedResults, currentQuery, report)
|
||||
|
||||
// 3. 生成后续查询
|
||||
if (progressCallback) {
|
||||
progressCallback(
|
||||
iterationCount + 1,
|
||||
`正在生成后续查询...`,
|
||||
Math.round((iterationCount / this.analysisConfig.maxIterations) * 100)
|
||||
)
|
||||
}
|
||||
const followUpQueries = await this.generateFollowUpQueries(analysis, currentQuery, report.iterations)
|
||||
|
||||
// 4. 记录这次迭代
|
||||
const iteration: ResearchIteration = {
|
||||
query: currentQuery,
|
||||
results: limitedResults,
|
||||
analysis,
|
||||
followUpQueries
|
||||
}
|
||||
|
||||
report.iterations.push(iteration)
|
||||
|
||||
// 5. 收集源
|
||||
limitedResults.forEach((result) => {
|
||||
if (result.url) {
|
||||
allSources.add(result.url)
|
||||
}
|
||||
})
|
||||
|
||||
// 6. 检查是否继续迭代
|
||||
if (followUpQueries.length === 0) {
|
||||
console.log(`[DeepResearch] 没有更多的后续查询,结束迭代`)
|
||||
if (progressCallback) {
|
||||
progressCallback(
|
||||
iterationCount + 1,
|
||||
`迭代完成,没有更多后续查询`,
|
||||
Math.round(((iterationCount + 1) / this.analysisConfig.maxIterations) * 100)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 7. 更新查询并继续
|
||||
currentQuery = followUpQueries[0] // 使用第一个后续查询
|
||||
iterationCount++
|
||||
}
|
||||
|
||||
// 生成最终总结
|
||||
if (progressCallback) {
|
||||
progressCallback(iterationCount, `正在生成研究总结...`, 70)
|
||||
}
|
||||
report.summary = await this.generateSummary(report.iterations, report)
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(iterationCount, `正在提取关键见解...`, 80)
|
||||
}
|
||||
report.keyInsights = await this.extractKeyInsights(report.iterations, report)
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(iterationCount, `正在生成问题回答...`, 90)
|
||||
}
|
||||
report.directAnswer = await this.generateDirectAnswer(query, report.summary, report.keyInsights, report)
|
||||
|
||||
report.sources = Array.from(allSources)
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(iterationCount, `研究完成`, 100)
|
||||
}
|
||||
|
||||
console.log(`[DeepResearch] 完成深度研究,共 ${report.iterations.length} 次迭代`)
|
||||
return report
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析搜索结果
|
||||
* @param results 搜索结果
|
||||
* @param query 当前查询
|
||||
* @returns 分析文本
|
||||
*/
|
||||
private async analyzeResults(results: WebSearchResult[], query: string, report?: ResearchReport): Promise<string> {
|
||||
if (results.length === 0) {
|
||||
return `没有找到关于"${query}"的相关信息。`
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[DeepResearch] 分析 ${results.length} 个结果`)
|
||||
|
||||
// 提取关键信息
|
||||
const contentSummaries = results
|
||||
.map((result, index) => {
|
||||
const content = result.content || '无内容'
|
||||
// 提取前300个字符作为摘要
|
||||
const summary = content.length > 300 ? content.substring(0, 300) + '...' : content
|
||||
|
||||
return `[${index + 1}] ${result.title}\n${summary}\n来源: ${result.url}\n`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// 使用模型分析内容
|
||||
const analysis = await this.analyzeWithModel(contentSummaries, query, report)
|
||||
|
||||
return analysis
|
||||
} catch (error: any) {
|
||||
console.error('[DeepResearch] 分析结果时出错:', error)
|
||||
return `分析过程中出现错误: ${error?.message || '未知错误'}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用项目中的模型分析搜索结果
|
||||
*/
|
||||
private async analyzeWithModel(contentSummaries: string, query: string, report?: ResearchReport): Promise<string> {
|
||||
try {
|
||||
console.log(`[DeepResearch] 使用模型分析搜索结果`)
|
||||
|
||||
// 分析提示词
|
||||
const prompt = `你是一个高级研究分析师,负责深入分析搜索结果并提取全面、详细的见解。
|
||||
请对以下关于"${query}"的搜索结果进行彻底分析,并提供以下内容:
|
||||
|
||||
1. 主要发现(详细列出5-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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DeepResearchProvider
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import { WebSearchProvider } from '@renderer/types'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
import DefaultProvider from './DefaultProvider'
|
||||
import DeepResearchProvider from './DeepResearchProvider'
|
||||
import DeepSearchProvider from './DeepSearchProvider'
|
||||
import ExaProvider from './ExaProvider'
|
||||
import LocalBaiduProvider from './LocalBaiduProvider'
|
||||
@ -27,6 +28,8 @@ export default class WebSearchProviderFactory {
|
||||
return new LocalBingProvider(provider)
|
||||
case 'deep-search':
|
||||
return new DeepSearchProvider(provider)
|
||||
case 'deep-research':
|
||||
return new DeepResearchProvider(provider)
|
||||
default:
|
||||
return new DefaultProvider(provider)
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { createHashRouter, HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import AgentsPage from '@renderer/pages/agents/AgentsPage'
|
||||
import AppsPage from '@renderer/pages/apps/AppsPage'
|
||||
import DeepResearchPage from '@renderer/pages/deepresearch/DeepResearchPage'
|
||||
import FilesPage from '@renderer/pages/files/FilesPage'
|
||||
import HomePage from '@renderer/pages/home/HomePage'
|
||||
import KnowledgePage from '@renderer/pages/knowledge/KnowledgePage'
|
||||
@ -46,6 +47,10 @@ export const router = createHashRouter(
|
||||
path: '/workspace',
|
||||
element: <WorkspacePage />
|
||||
},
|
||||
{
|
||||
path: '/deepresearch',
|
||||
element: <DeepResearchPage />
|
||||
},
|
||||
{
|
||||
path: '/settings/*',
|
||||
element: <SettingsPage />
|
||||
@ -80,6 +85,7 @@ export const RouterComponent = ({ children }: { children?: React.ReactNode }) =>
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/workspace" element={<WorkspacePage />} />
|
||||
<Route path="/deepresearch" element={<DeepResearchPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
{children}
|
||||
|
||||
@ -17,6 +17,7 @@ export type SidebarIcon =
|
||||
| 'files'
|
||||
| 'projects'
|
||||
| 'workspace'
|
||||
| 'deepresearch'
|
||||
|
||||
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'assistants',
|
||||
@ -26,7 +27,8 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'minapp',
|
||||
'knowledge',
|
||||
'files',
|
||||
'workspace'
|
||||
'workspace',
|
||||
'deepresearch'
|
||||
]
|
||||
|
||||
export interface NutstoreSyncRuntime extends WebDAVSyncState {}
|
||||
@ -191,6 +193,12 @@ export interface SettingsState {
|
||||
siyuan: boolean
|
||||
docx: boolean
|
||||
}
|
||||
// DeepResearch 设置
|
||||
enableDeepResearch: boolean
|
||||
deepResearchShortcut: string
|
||||
deepResearchMaxDepth: number
|
||||
deepResearchMaxUrls: number
|
||||
deepResearchTimeLimit: number
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
@ -344,7 +352,13 @@ export const initialState: SettingsState = {
|
||||
obsidian: true,
|
||||
siyuan: true,
|
||||
docx: true
|
||||
}
|
||||
},
|
||||
// DeepResearch 设置
|
||||
enableDeepResearch: true,
|
||||
deepResearchShortcut: 'Alt+D',
|
||||
deepResearchMaxDepth: 3,
|
||||
deepResearchMaxUrls: 30,
|
||||
deepResearchTimeLimit: 120
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@ -802,6 +816,22 @@ const settingsSlice = createSlice({
|
||||
setEnableBackspaceDeleteModel: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableBackspaceDeleteModel = action.payload
|
||||
},
|
||||
// DeepResearch 设置
|
||||
setEnableDeepResearch: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableDeepResearch = action.payload
|
||||
},
|
||||
setDeepResearchShortcut: (state, action: PayloadAction<string>) => {
|
||||
state.deepResearchShortcut = action.payload
|
||||
},
|
||||
setDeepResearchMaxDepth: (state, action: PayloadAction<number>) => {
|
||||
state.deepResearchMaxDepth = action.payload
|
||||
},
|
||||
setDeepResearchMaxUrls: (state, action: PayloadAction<number>) => {
|
||||
state.deepResearchMaxUrls = action.payload
|
||||
},
|
||||
setDeepResearchTimeLimit: (state, action: PayloadAction<number>) => {
|
||||
state.deepResearchTimeLimit = action.payload
|
||||
},
|
||||
// PDF设置相关的action
|
||||
setPdfSettings: (
|
||||
state,
|
||||
@ -942,7 +972,13 @@ export const {
|
||||
setLastPlayedMessageId,
|
||||
setSkipNextAutoTTS,
|
||||
setEnableBackspaceDeleteModel,
|
||||
setUsePromptForToolCalling
|
||||
setUsePromptForToolCalling,
|
||||
// DeepResearch 设置
|
||||
setEnableDeepResearch,
|
||||
setDeepResearchShortcut,
|
||||
setDeepResearchMaxDepth,
|
||||
setDeepResearchMaxUrls,
|
||||
setDeepResearchTimeLimit
|
||||
} = settingsSlice.actions
|
||||
|
||||
// PDF设置相关的action
|
||||
|
||||
@ -24,6 +24,41 @@ export interface WebSearchState {
|
||||
enhanceMode: boolean
|
||||
// 是否覆盖服务商搜索
|
||||
overwrite: boolean
|
||||
// 深度研究配置
|
||||
deepResearchConfig?: {
|
||||
maxIterations?: number
|
||||
maxResultsPerQuery?: number
|
||||
autoSummary?: boolean
|
||||
enableQueryOptimization?: boolean
|
||||
}
|
||||
// DeepSearch 配置
|
||||
deepSearchConfig?: {
|
||||
enabledEngines?: {
|
||||
// 中文搜索引擎
|
||||
baidu?: boolean
|
||||
sogou?: boolean
|
||||
'360'?: boolean
|
||||
yisou?: boolean
|
||||
|
||||
// 国际搜索引擎
|
||||
bing?: boolean
|
||||
duckduckgo?: boolean
|
||||
brave?: boolean
|
||||
qwant?: boolean
|
||||
|
||||
// 元搜索引擎
|
||||
searx?: boolean
|
||||
ecosia?: boolean
|
||||
startpage?: boolean
|
||||
mojeek?: boolean
|
||||
|
||||
// 学术搜索引擎
|
||||
scholar?: boolean
|
||||
semantic?: boolean
|
||||
base?: boolean
|
||||
cnki?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: WebSearchState = {
|
||||
@ -64,6 +99,12 @@ const initialState: WebSearchState = {
|
||||
name: 'DeepSearch (多引擎)',
|
||||
description: '使用Baidu、Bing、DuckDuckGo、搜狗和SearX进行深度搜索',
|
||||
contentLimit: 10000
|
||||
},
|
||||
{
|
||||
id: 'deep-research',
|
||||
name: 'DeepResearch (深度研究)',
|
||||
description: '使用多轮搜索、分析和总结进行深度研究',
|
||||
contentLimit: 30000
|
||||
}
|
||||
],
|
||||
searchWithTime: true,
|
||||
@ -71,7 +112,34 @@ const initialState: WebSearchState = {
|
||||
excludeDomains: [],
|
||||
subscribeSources: [],
|
||||
enhanceMode: true,
|
||||
overwrite: false
|
||||
overwrite: false,
|
||||
deepSearchConfig: {
|
||||
enabledEngines: {
|
||||
// 中文搜索引擎
|
||||
baidu: true,
|
||||
sogou: true,
|
||||
'360': false,
|
||||
yisou: false,
|
||||
|
||||
// 国际搜索引擎
|
||||
bing: true,
|
||||
duckduckgo: true,
|
||||
brave: false,
|
||||
qwant: false,
|
||||
|
||||
// 元搜索引擎
|
||||
searx: true,
|
||||
ecosia: false,
|
||||
startpage: false,
|
||||
mojeek: false,
|
||||
|
||||
// 学术搜索引擎
|
||||
scholar: true,
|
||||
semantic: false,
|
||||
base: false,
|
||||
cnki: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultWebSearchProviders = initialState.providers
|
||||
@ -145,6 +213,39 @@ const websearchSlice = createSlice({
|
||||
// Add the new provider to the array
|
||||
state.providers.push(action.payload)
|
||||
}
|
||||
},
|
||||
setDeepResearchConfig: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
maxIterations?: number
|
||||
maxResultsPerQuery?: number
|
||||
autoSummary?: boolean
|
||||
enableQueryOptimization?: boolean
|
||||
modelId?: string
|
||||
}>
|
||||
) => {
|
||||
state.deepResearchConfig = {
|
||||
...state.deepResearchConfig,
|
||||
...action.payload
|
||||
}
|
||||
},
|
||||
setDeepSearchConfig: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
enabledEngines?: {
|
||||
baidu?: boolean
|
||||
bing?: boolean
|
||||
duckduckgo?: boolean
|
||||
sogou?: boolean
|
||||
searx?: boolean
|
||||
scholar?: boolean
|
||||
}
|
||||
}>
|
||||
) => {
|
||||
state.deepSearchConfig = {
|
||||
...state.deepSearchConfig,
|
||||
...action.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -163,7 +264,9 @@ export const {
|
||||
setSubscribeSources,
|
||||
setEnhanceMode,
|
||||
setOverwrite,
|
||||
addWebSearchProvider
|
||||
addWebSearchProvider,
|
||||
setDeepResearchConfig,
|
||||
setDeepSearchConfig
|
||||
} = websearchSlice.actions
|
||||
|
||||
export default websearchSlice.reducer
|
||||
|
||||
@ -363,6 +363,8 @@ export type SidebarIcon =
|
||||
| 'knowledge'
|
||||
| 'files'
|
||||
| 'projects'
|
||||
| 'workspace'
|
||||
| 'deepresearch'
|
||||
|
||||
export type WebSearchProvider = {
|
||||
id: string
|
||||
@ -374,6 +376,7 @@ export type WebSearchProvider = {
|
||||
contentLimit?: number
|
||||
usingBrowser?: boolean
|
||||
description?: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
export type WebSearchResponse = {
|
||||
@ -389,6 +392,31 @@ export type WebSearchResult = {
|
||||
summary?: string
|
||||
keywords?: string[]
|
||||
relevanceScore?: number
|
||||
meta?: {
|
||||
priorityScore?: number
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResearchIteration {
|
||||
query: string
|
||||
results: WebSearchResult[]
|
||||
analysis: string
|
||||
followUpQueries: string[]
|
||||
}
|
||||
|
||||
export interface ResearchReport {
|
||||
originalQuery: string
|
||||
iterations: ResearchIteration[]
|
||||
summary: string
|
||||
directAnswer: string
|
||||
keyInsights: string[]
|
||||
sources: string[]
|
||||
tokenUsage?: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
totalTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
export type KnowledgeReference = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user