This commit is contained in:
1600822305 2025-04-22 11:44:21 +08:00
parent 607cded6c9
commit 4dde843ef4
89 changed files with 6814 additions and 841 deletions

View File

@ -96,6 +96,7 @@
"@monaco-editor/react": "^4.7.0",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@shikijs/markdown-it": "^3.2.2",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tryfabric/martian": "^1.2.4",
"@types/react-infinite-scroll-component": "^5.0.0",
@ -117,18 +118,25 @@
"fast-xml-parser": "^5.0.9",
"fetch-socks": "^1.3.2",
"fs-extra": "^11.2.0",
"glob": "^11.0.1",
"got-scraping": "^4.1.1",
"js-tiktoken": "^1.0.19",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"minimatch": "^10.0.1",
"monaco-editor": "^0.52.2",
"node-edge-tts": "^1.2.8",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.1",
"path-browserify": "^1.0.1",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.1.91",
"proxy-agent": "^6.5.0",
"react-syntax-highlighter": "^15.6.1",
"react-transition-group": "^4.4.5",
"react-virtualized": "^9.22.6",
"react-vtree": "^2.0.4",
"react-window": "^1.8.11",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
@ -163,15 +171,19 @@
"@types/d3": "^7",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/glob": "^8.1.0",
"@types/lodash": "^4.17.16",
"@types/markdown-it": "^14",
"@types/md5": "^2.3.5",
"@types/node": "^18.19.9",
"@types/pako": "^1.0.2",
"@types/path-browserify": "^1.0.3",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/react-infinite-scroll-component": "^5.0.0",
"@types/react-syntax-highlighter": "^15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-virtualized": "^9.22.2",
"@types/react-window": "^1",
"@types/tinycolor2": "^1",
"@vitejs/plugin-react": "^4.3.4",
"analytics": "^0.8.16",
@ -198,6 +210,7 @@
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"i18next": "^23.11.5",
"less": "^4.3.0",
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",

View File

@ -55,6 +55,7 @@ export enum IpcChannel {
Mcp_ListResources = 'mcp:list-resources',
Mcp_GetResource = 'mcp:get-resource',
Mcp_GetInstallInfo = 'mcp:get-install-info',
Mcp_RerunTool = 'mcp:rerunTool', // Add rerun tool channel
Mcp_ServersChanged = 'mcp:servers-changed',
Mcp_ServersUpdated = 'mcp:servers-updated',
@ -186,5 +187,8 @@ export enum IpcChannel {
// PDF
PDF_SplitPDF = 'pdf:split-pdf',
PDF_GetPageCount = 'pdf:get-page-count'
PDF_GetPageCount = 'pdf:get-page-count',
// MCP Rerun Updates (Main -> Renderer)
Mcp_ToolRerunUpdate = 'mcp:tool-rerun-update'
}

View File

@ -0,0 +1,39 @@
# Cherry Studio 项目文件功能分类索引
本文档提供了Cherry Studio项目文件的功能分类索引帮助开发者快速了解项目结构和各模块功能。
## 功能分类目录
1. [核心架构与配置](./01_核心架构与配置.md) - 项目的核心架构、配置文件和主要进程
2. [AI助手与对话功能](./02_AI助手与对话功能.md) - AI提供者、助手管理、对话功能和话题管理
3. [知识库管理](./03_知识库管理.md) - 知识库核心功能、队列处理、嵌入与检索
4. [工作区功能](./04_工作区功能.md) - 工作区核心功能、组件和文件管理
5. [翻译功能](./05_翻译功能.md) - 翻译核心功能、组件和数据管理
6. [绘画功能](./06_绘画功能.md) - 绘画核心功能、组件和模型
7. [文件管理](./07_文件管理.md) - 文件管理核心功能、数据管理和组件
8. [用户界面与组件](./08_用户界面与组件.md) - 主要页面、核心组件和样式主题
9. [数据库与存储](./09_数据库与存储.md) - 数据库核心、数据表和状态管理
10. [工具与实用功能](./10_工具与实用功能.md) - 语音识别、代码执行、网页搜索等工具
## 项目概述
Cherry Studio是一个功能强大的AI助手应用主要特点包括
- 支持多种AI模型和提供者OpenAI、Gemini、Anthropic等
- 内置300+预配置AI助手
- 知识库管理和检索
- 工作区文件管理
- 翻译功能
- AI绘画功能
- 文件管理和处理
- 跨平台支持Windows、Mac、Linux
## 技术栈
- Electron - 跨平台桌面应用框架
- React - 前端UI库
- TypeScript - 类型安全的JavaScript超集
- Redux - 状态管理
- Dexie - IndexedDB封装库
- Ant Design - UI组件库
- Styled Components - CSS-in-JS解决方案

View File

@ -0,0 +1,112 @@
# 核心架构与配置
本文档记录了Cherry Studio的核心架构和配置相关文件。
## 主要配置文件
- `package.json` - 项目的主要配置文件,包含依赖、脚本和项目元数据,定义了项目名称、版本、依赖项和构建脚本
- `electron.vite.config.ts` - Electron Vite配置文件定义了主进程、预加载脚本和渲染进程的构建配置包含路径别名和插件配置
- `tsconfig.json` - TypeScript主配置文件定义了项目的编译选项和类型检查规则
- `tsconfig.node.json` - Node.js相关的TypeScript配置针对主进程的特殊配置
- `tsconfig.web.json` - Web相关的TypeScript配置针对渲染进程的特殊配置
- `.eslintrc.js` - ESLint配置文件定义代码风格和质量检查规则
- `.prettierrc` - Prettier配置文件定义代码格式化规则
## 主进程相关文件
- `src/main/index.ts` - 主进程入口文件,负责创建和管理应用窗口,初始化应用程序
- `src/main/ipc.ts` - IPC通信处理注册所有IPC处理程序连接渲染进程和主进程
- `src/main/constant.ts` - 主进程常量定义,包含平台检测和全局常量
- `src/main/utils/` - 主进程工具函数目录
- `src/main/utils/file.ts` - 文件操作相关工具函数
- `src/main/utils/process.ts` - 进程管理相关工具函数
- `src/main/utils/aes.ts` - AES加密解密工具函数
- `src/main/utils/zip.ts` - 压缩解压工具函数
- `src/main/utils/storage.ts` - 存储相关工具函数
- `src/main/services/` - 主进程服务实现目录
- `src/main/services/WindowService.ts` - 窗口管理服务
- `src/main/services/FileService.ts` - 文件服务
- `src/main/services/KnowledgeService.ts` - 知识库服务
- `src/main/services/MCPService.ts` - MCP服务
- `src/main/services/WorkspaceService.ts` - 工作区服务
- `src/main/services/SearchService.ts` - 搜索服务
- `src/main/services/ProxyManager.ts` - 代理管理服务
- `src/main/services/ShortcutService.ts` - 快捷键服务
- `src/main/services/TrayService.ts` - 托盘服务
- `src/main/services/ProtocolClient.ts` - 协议客户端服务
- `src/main/services/ConfigManager.ts` - 配置管理服务
- `src/main/services/MsTTSService.ts` - 微软TTS服务
- `src/main/services/PDFService.ts` - PDF服务
- `src/main/services/CodeExecutorService.ts` - 代码执行服务
- `src/main/services/MemoryFileService.ts` - 记忆文件服务
- `src/main/services/NutstoreService.ts` - 坚果云服务
- `src/main/services/ObsidianVaultService.ts` - Obsidian集成服务
- `src/main/services/BackupManager.ts` - 备份管理服务
- `src/main/services/ExportService.ts` - 导出服务
- `src/main/services/GeminiService.ts` - Gemini服务
- `src/main/services/AppUpdater.ts` - 应用更新服务
## 渲染进程相关文件
- `src/renderer/index.html` - 渲染进程HTML入口定义了基本的HTML结构
- `src/renderer/src/main.tsx` - 渲染进程主入口文件负责渲染React应用
- `src/renderer/src/init.ts` - 渲染进程初始化,包含启动时的初始化逻辑
- `src/renderer/src/App.tsx` - 主应用组件,定义了应用的整体结构和路由
- `src/renderer/src/env.d.ts` - 环境变量类型定义文件
- `src/renderer/src/assets/` - 静态资源文件目录
- `src/renderer/src/assets/styles/` - 样式文件
- `src/renderer/src/assets/images/` - 图片资源
- `src/renderer/src/assets/fonts/` - 字体资源
- `src/renderer/src/assets/asr-server/` - ASR服务器资源
- `src/renderer/src/context/` - React上下文提供者目录
- `src/renderer/src/context/AntdProvider.tsx` - Ant Design上下文提供者
- `src/renderer/src/context/StyleSheetManager.tsx` - 样式表管理器
- `src/renderer/src/context/SyntaxHighlighterProvider.tsx` - 语法高亮提供者
- `src/renderer/src/context/ThemeProvider.tsx` - 主题提供者
## 预加载脚本
- `src/preload/index.ts` - 预加载脚本提供渲染进程与主进程通信的桥梁定义了IPC通信接口
## 共享代码
- `packages/shared/` - 主进程和渲染进程共享的代码目录
- `packages/shared/config/` - 共享配置
- `packages/shared/IpcChannel.ts` - IPC通道定义
- `packages/database/` - 数据库相关代码目录
- `packages/database/src/agents.js` - 代理数据库操作
- `packages/database/src/csv.js` - CSV数据处理
- `packages/database/src/email.js` - 邮件数据处理
## 构建与部署
- `build/` - 构建相关资源目录
- `build/icon.ico` - 应用图标
- `build/logo.png` - 应用Logo
- `scripts/` - 构建和发布脚本目录
- `scripts/build-npm.js` - NPM构建脚本
- `scripts/version.js` - 版本管理脚本
- `scripts/check-i18n.js` - 国际化检查脚本
## 配置与常量
- `src/shared/config/constant.ts` - 共享常量,定义了应用中使用的常量值
- `src/shared/config/types.ts` - 共享类型定义,定义了跨进程使用的类型
- `src/shared/IpcChannel.ts` - IPC通道定义定义了所有IPC通信通道
## MCP服务器实现
- `src/main/mcpServers/factory.ts` - MCP服务器工厂创建不同类型的MCP服务器
- `src/main/mcpServers/sequentialthinking.ts` - 顺序思考MCP服务器
- `src/main/mcpServers/memory.ts` - 记忆MCP服务器
- `src/main/mcpServers/simpleremember.ts` - 简单记忆MCP服务器
- `src/main/mcpServers/brave-search.ts` - Brave搜索MCP服务器
- `src/main/mcpServers/fetch.ts` - 获取MCP服务器
- `src/main/mcpServers/filesystem.ts` - 文件系统MCP服务器
## 文件加载器实现
- `src/main/loader/index.ts` - 文件加载器入口
- `src/main/loader/draftsExportLoader.ts` - Drafts导出加载器
- `src/main/loader/epubLoader.ts` - EPUB加载器
- `src/main/loader/odLoader.ts` - Office文档加载器

View File

@ -0,0 +1,89 @@
AI助手与对话功能
本文档记录了Cherry Studio的AI助手和对话功能相关文件。
AI提供者
src/renderer/src/providers/AiProvider/index.ts - AI提供者基类定义了所有AI提供者的通用接口和方法
src/renderer/src/providers/AiProvider/BaseProvider.ts - 基础提供者类实现了通用的AI提供者功能
src/renderer/src/providers/AiProvider/OpenAIProvider.ts - OpenAI提供者实现处理与OpenAI API的交互
src/renderer/src/providers/AiProvider/AnthropicProvider.ts - Anthropic提供者实现处理与Claude API的交互
src/renderer/src/providers/AiProvider/GeminiProvider.ts - Google Gemini提供者实现处理与Gemini API的交互
src/renderer/src/providers/AiProvider/ZhipuProvider.ts - 智谱AI提供者实现处理与智谱API的交互
src/renderer/src/providers/AiProvider/DeepClaudeProvider.ts - DeepClaude提供者实现组合多个模型实现增强功能
src/renderer/src/providers/AiProvider/ProviderFactory.ts - 提供者工厂类根据配置创建不同的AI提供者实例
src/renderer/src/config/providers.ts - 提供者配置包含所有支持的AI提供者的配置信息
助手管理
src/renderer/src/store/assistants.ts - 助手状态管理使用Redux管理助手数据
src/renderer/src/hooks/useAssistant.ts - 助手相关钩子函数提供助手操作的React钩子
src/renderer/src/hooks/useDefaultModel.ts - 默认模型钩子获取默认AI模型
src/renderer/src/services/AssistantService.ts - 助手服务,提供助手相关的业务逻辑
src/renderer/src/types/index.ts - 助手类型定义定义了Assistant、Model等类型
src/renderer/src/config/models.ts - 模型配置定义了支持的AI模型及其属性
对话功能
src/renderer/src/pages/home/Chat/index.tsx - 聊天组件主入口,管理对话界面
src/renderer/src/pages/home/Chat/ChatContainer.tsx - 聊天容器组件,管理消息列表
src/renderer/src/pages/home/Chat/Message.tsx - 消息组件,渲染单条消息
src/renderer/src/pages/home/Inputbar/Inputbar.tsx - 输入栏组件,处理用户输入
src/renderer/src/pages/home/Inputbar/SendMessageButton.tsx - 发送消息按钮组件
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx - 附件按钮组件,处理文件上传
src/renderer/src/services/ApiService.ts - API服务处理与AI提供者的通信
src/renderer/src/services/MessagesService.ts - 消息服务,提供消息相关的业务逻辑
src/renderer/src/store/messages.ts - 消息状态管理使用Redux管理消息数据
src/renderer/src/hooks/useMessages.ts - 消息相关钩子函数提供消息操作的React钩子
src/renderer/src/hooks/useMessageOperations.ts - 消息操作钩子,提供消息编辑、删除等操作
话题管理
src/renderer/src/store/topics.ts - 话题状态管理使用Redux管理话题数据
src/renderer/src/hooks/useTopic.ts - 话题相关钩子函数提供话题操作的React钩子
src/renderer/src/pages/home/Navbar/index.tsx - 导航栏组件,包含话题管理功能
src/renderer/src/pages/home/Navbar/TopicSidebar.tsx - 话题侧边栏组件,显示话题列表
src/renderer/src/pages/home/Navbar/TopicItem.tsx - 话题项组件,显示单个话题
src/renderer/src/pages/home/Messages/MessageMenubar.tsx - 消息菜单栏,提供话题相关操作
记忆功能
src/renderer/src/components/MemoryProvider/index.tsx - 记忆提供者组件,管理记忆功能
src/renderer/src/store/memory.ts - 记忆状态管理使用Redux管理记忆数据
src/renderer/src/services/MemoryService.ts - 记忆服务,提供记忆相关的业务逻辑
src/renderer/src/services/HistoricalContextService.ts - 历史上下文服务,管理对话历史记忆
src/renderer/src/services/VectorService.ts - 向量服务,处理记忆的向量表示
src/main/services/MemoryFileService.ts - 记忆文件服务,处理记忆的持久化存储
src/renderer/src/components/Popups/ShortMemoryPopup.tsx - 短期记忆弹窗组件
src/renderer/src/components/AssistantMemoryPopup.tsx - 助手记忆弹窗组件
MCP服务
src/main/services/MCPService.ts - MCP服务管理模型上下文协议服务
src/main/mcpServers/factory.ts - MCP服务器工厂创建不同类型的MCP服务器
src/main/mcpServers/sequentialthinking.ts - 顺序思考MCP服务器提供结构化思考功能
src/main/mcpServers/memory.ts - 记忆MCP服务器提供记忆功能
src/main/mcpServers/simpleremember.ts - 简单记忆MCP服务器提供基础记忆功能
src/main/mcpServers/brave-search.ts - Brave搜索MCP服务器提供网络搜索功能
src/main/mcpServers/fetch.ts - 获取MCP服务器提供网络请求功能
src/main/mcpServers/filesystem.ts - 文件系统MCP服务器提供文件操作功能
src/renderer/src/store/mcp.ts - MCP状态管理使用Redux管理MCP服务器配置
src/renderer/src/hooks/useMCPServers.ts - MCP服务器钩子提供MCP服务器操作的React钩子
src/renderer/src/utils/mcp-tools.ts - MCP工具函数处理MCP工具调用和响应
语音功能
src/renderer/src/components/ASRButton/index.tsx - 语音识别按钮组件,提供语音输入功能
src/renderer/src/components/VoiceCallButton.tsx - 语音通话按钮组件,提供语音对话功能
src/renderer/src/components/VoiceCallModal.tsx - 语音通话模态框组件,显示语音对话界面
src/renderer/src/components/DraggableVoiceCallWindow.tsx - 可拖动语音通话窗口组件
src/renderer/src/components/VoiceVisualizer.tsx - 语音可视化组件,显示语音波形
src/renderer/src/services/ASRService.ts - 语音识别服务,处理语音转文本
src/renderer/src/services/ASRServerService.ts - 语音识别服务器服务管理ASR服务器
src/renderer/src/services/VoiceCallService.ts - 语音通话服务,处理语音对话
src/main/services/MsTTSService.ts - 微软TTS服务处理文本转语音
src/renderer/src/services/tts/TTSService.ts - TTS服务管理文本转语音功能
src/renderer/src/services/tts/TTSServiceFactory.ts - TTS服务工厂创建不同的TTS服务实例
src/renderer/src/services/tts/EdgeTTSService.ts - Edge TTS服务实现
src/renderer/src/services/tts/MsTTSService.ts - 微软TTS服务实现
src/renderer/src/services/tts/OpenAITTSService.ts - OpenAI TTS服务实现
src/renderer/src/services/tts/SiliconflowTTSService.ts - 硅基流动TTS服务实现
src/renderer/src/services/tts/TTSTextFilter.ts - TTS文本过滤器处理不适合朗读的内容
其他相关功能
src/renderer/src/services/SuggestionService.ts - 建议服务,提供对话建议功能
src/renderer/src/components/DeepClaudeProvider/index.tsx - Deep Claude提供者组件管理增强型Claude功能
src/renderer/src/utils/createDeepClaudeProvider.ts - 创建Deep Claude提供者工具函数
src/renderer/src/utils/thinkingLibrary.ts - 思考库工具函数,处理结构化思考
src/renderer/src/components/ModelTags.tsx - 模型标签组件,显示模型功能标签
src/renderer/src/components/ModelTagsWithLabel.tsx - 带标签的模型标签组件
src/renderer/src/components/TTSButton.tsx - TTS按钮组件提供文本朗读功能
src/renderer/src/components/TTSSegmentedText.tsx - TTS分段文本组件显示分段朗读文本
src/renderer/src/utils/formats.ts - 格式化工具函数,处理消息格式化
src/renderer/src/utils/linkConverter.ts - 链接转换工具函数,处理消息中的链接
src/renderer/src/utils/prompt.ts - 提示词工具函数,处理系统提示词构建
这些文件共同构成了Cherry Studio的AI助手和对话功能提供了丰富的AI交互体验包括文本对话、语音交互、记忆功能和结构化思考等高级特性。

View File

@ -0,0 +1,63 @@
知识库管理
本文档记录了Cherry Studio的知识库管理相关文件。
知识库核心功能
src/main/services/KnowledgeService.ts - 知识库主服务,实现了知识库的核心功能,包括创建、搜索、添加和删除知识库项目
src/renderer/src/services/KnowledgeService.ts - 渲染进程知识库服务,提供前端与主进程知识库服务的交互接口
src/renderer/src/store/knowledge.ts - 知识库状态管理使用Redux管理知识库数据
src/renderer/src/hooks/useKnowledge.ts - 知识库相关钩子函数提供知识库操作的React钩子
src/renderer/src/hooks/useKnowledgeReference.ts - 知识库引用钩子,提供知识库引用功能
src/renderer/src/pages/knowledge/KnowledgePage.tsx - 知识库页面,显示知识库管理界面
知识库队列处理
src/renderer/src/queue/KnowledgeQueue.ts - 知识库处理队列,管理知识库项目的异步处理
src/renderer/src/queue/index.ts - 队列管理,提供队列处理的基础功能
src/renderer/src/queue/QueueManager.ts - 队列管理器,管理多个处理队列
嵌入与检索
src/main/embeddings/Embeddings.ts - 嵌入模型,处理文本向量化
src/main/reranker/Reranker.ts - 重排序器,优化检索结果排序
src/main/loader/index.ts - 文件加载器入口,管理不同类型文件的加载
src/renderer/src/services/ModelService.ts - 模型服务,提供模型相关功能
src/renderer/src/services/VectorService.ts - 向量服务,处理向量计算和相似度搜索
知识库加载器
src/main/loader/draftsExportLoader.ts - Drafts导出加载器处理Drafts导出文件
src/main/loader/epubLoader.ts - EPUB加载器处理EPUB电子书文件
src/main/loader/odLoader.ts - Office文档加载器处理Word、Excel、PowerPoint文件
@cherrystudio/embedjs-loader-web - 网页加载器,处理网页内容
@cherrystudio/embedjs-loader-markdown - Markdown加载器处理Markdown文件
@cherrystudio/embedjs-loader-pdf - PDF加载器处理PDF文件
@cherrystudio/embedjs-loader-sitemap - 站点地图加载器,处理网站站点地图
@cherrystudio/embedjs-loader-image - 图片加载器,处理图片内容
@cherrystudio/embedjs-loader-xml - XML加载器处理XML文件
@cherrystudio/embedjs-loader-msoffice - Office加载器处理Office文件
@cherrystudio/embedjs-loader-csv - CSV加载器处理CSV文件
知识库组件
src/renderer/src/pages/knowledge/KnowledgeBaseList/index.tsx - 知识库列表组件,显示所有知识库
src/renderer/src/pages/knowledge/KnowledgeBaseDetail/index.tsx - 知识库详情组件,显示知识库详细信息
src/renderer/src/pages/knowledge/KnowledgeBaseForm/index.tsx - 知识库表单组件,用于创建和编辑知识库
src/renderer/src/pages/knowledge/KnowledgeItemList/index.tsx - 知识库项目列表组件,显示知识库中的项目
src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx - 添加知识库弹窗组件
src/renderer/src/pages/knowledge/components/KnowledgeSettingsPopup.tsx - 知识库设置弹窗组件
src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx - 知识库搜索弹窗组件
src/renderer/src/pages/knowledge/KnowledgeContent.tsx - 知识库内容组件,显示知识库内容
src/renderer/src/components/KnowledgeBaseSelector/index.tsx - 知识库选择器组件,用于选择知识库
知识库笔记
src/renderer/src/databases/index.ts - 知识库笔记数据库表定义
src/renderer/src/components/KnowledgeNoteEditor/index.tsx - 知识库笔记编辑器组件
src/renderer/src/pages/knowledge/components/NoteItem.tsx - 笔记项组件,显示单个笔记
src/renderer/src/pages/knowledge/components/AddNotePopup.tsx - 添加笔记弹窗组件
知识库集成
src/renderer/src/hooks/useKnowledgeReference.ts - 知识库引用钩子,提供知识库引用功能
src/renderer/src/components/KnowledgeReferencePanel/index.tsx - 知识库引用面板组件,显示知识库引用
src/renderer/src/components/KnowledgeReferenceItem.tsx - 知识库引用项组件,显示单个引用项
src/renderer/src/services/KnowledgeReferenceService.ts - 知识库引用服务,提供引用相关功能
src/renderer/src/utils/knowledge.ts - 知识库工具函数,提供知识库相关工具函数
知识库搜索与检索
src/main/services/KnowledgeService.ts - 知识库搜索功能实现
src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx - 知识库搜索弹窗组件
src/renderer/src/pages/knowledge/components/SearchResultItem.tsx - 搜索结果项组件
src/renderer/src/services/KnowledgeSearchService.ts - 知识库搜索服务,提供搜索相关功能
知识库数据存储
src/main/services/KnowledgeService.ts - 知识库数据存储实现
@cherrystudio/embedjs-libsql - LibSQL数据库集成用于存储向量数据
src/renderer/src/databases/index.ts - 知识库笔记数据库表定义
src/renderer/src/databases/upgrades.ts - 数据库升级脚本
这些文件共同构成了Cherry Studio的知识库管理功能提供了强大的知识库创建、管理、搜索和引用能力支持多种文件格式和数据源实现了AI与知识库的深度集成。

View File

@ -0,0 +1,45 @@
工作区功能
本文档记录了Cherry Studio的工作区功能相关文件。
工作区核心功能
src/main/services/WorkspaceService.ts - 工作区主服务,实现了工作区的核心功能,包括文件浏览、读取和写入
src/renderer/src/services/WorkspaceService.ts - 渲染进程工作区服务,提供前端与主进程工作区服务的交互接口
src/renderer/src/store/workspace.ts - 工作区状态管理使用Redux管理工作区数据和状态
src/renderer/src/pages/workspace/index.tsx - 工作区页面,显示工作区管理界面,包含文件浏览器和文件查看器
工作区组件
src/renderer/src/components/WorkspaceSelector/index.tsx - 工作区选择器组件,用于选择和管理工作区
src/renderer/src/components/WorkspaceInitializer/index.tsx - 工作区初始化组件,负责初始化工作区环境
src/renderer/src/components/ChatWorkspacePanel/index.tsx - 聊天工作区面板组件,在聊天界面中集成工作区功能
src/renderer/src/components/WorkspaceExplorer/index.tsx - 工作区浏览器组件,显示工作区文件结构
src/renderer/src/components/WorkspaceFileViewer/index.tsx - 工作区文件查看器组件,显示文件内容
工作区文件管理
src/renderer/src/pages/workspace/FileExplorer/index.tsx - 文件浏览器组件,浏览工作区文件
src/renderer/src/pages/workspace/FileExplorer/FileTree.tsx - 文件树组件,显示文件层次结构
src/renderer/src/pages/workspace/FileExplorer/FileItem.tsx - 文件项组件,显示单个文件或文件夹
src/renderer/src/pages/workspace/FileViewer/index.tsx - 文件查看器组件,查看文件内容
src/renderer/src/pages/workspace/FileViewer/CodeViewer.tsx - 代码查看器组件,查看代码文件
src/renderer/src/pages/workspace/FileViewer/ImageViewer.tsx - 图片查看器组件,查看图片文件
src/renderer/src/pages/workspace/FileViewer/TextViewer.tsx - 文本查看器组件,查看文本文件
src/renderer/src/pages/workspace/FileEditor/index.tsx - 文件编辑器组件,编辑文件内容
src/renderer/src/pages/workspace/FileEditor/CodeEditor.tsx - 代码编辑器组件,编辑代码文件
工作区集成
src/renderer/src/hooks/useWorkspace.ts - 工作区钩子函数提供工作区操作的React钩子
src/renderer/src/hooks/useWorkspaceFiles.ts - 工作区文件钩子提供文件操作的React钩子
src/renderer/src/databases/index.ts - 工作区数据库表定义,存储工作区配置
src/renderer/src/components/ChatWorkspacePanel/index.tsx - 聊天工作区面板,将工作区功能集成到聊天界面
src/renderer/src/components/WorkspaceContextMenu/index.tsx - 工作区上下文菜单组件,提供文件操作菜单
工作区文件操作
src/main/utils/file.ts - 文件操作工具函数,提供底层文件系统操作
src/main/services/WorkspaceService.ts - 工作区文件操作实现,包括读取、写入、删除等功能
src/renderer/src/utils/file.ts - 渲染进程文件操作工具函数,提供前端文件处理功能
src/renderer/src/services/WorkspaceFileService.ts - 工作区文件服务,提供文件操作的业务逻辑
src/renderer/src/utils/fileType.ts - 文件类型工具函数,识别和处理不同类型的文件
工作区与AI集成
src/renderer/src/components/ChatWorkspacePanel/index.tsx - 聊天工作区面板实现AI与工作区的集成
src/renderer/src/services/WorkspaceAIService.ts - 工作区AI服务提供AI与工作区交互的功能
src/main/mcpServers/filesystem.ts - 文件系统MCP服务器允许AI访问文件系统
src/renderer/src/utils/workspacePrompt.ts - 工作区提示词工具生成与工作区相关的AI提示词
工作区设置与配置
src/renderer/src/pages/settings/WorkspaceSettings.tsx - 工作区设置页面,配置工作区参数
src/renderer/src/store/workspace.ts - 工作区状态管理,存储工作区配置
src/renderer/src/utils/workspaceConfig.ts - 工作区配置工具,处理工作区配置文件
这些文件共同构成了Cherry Studio的工作区功能提供了强大的文件浏览、查看和编辑能力并与AI功能深度集成使用户能够在AI助手的帮助下高效处理工作区中的文件和代码。

View File

@ -0,0 +1,49 @@
翻译功能
本文档记录了Cherry Studio的翻译功能相关文件。
翻译核心功能
src/renderer/src/services/TranslateService.ts - 翻译服务,实现了翻译的核心功能,包括文本翻译和历史记录管理
src/renderer/src/pages/translate/TranslatePage.tsx - 翻译页面,显示主要翻译界面,包含输入框、结果显示和历史记录
src/renderer/src/windows/mini/translate/TranslateWindow.tsx - 迷你翻译窗口,提供轻量级的翻译界面
src/renderer/src/services/ApiService.ts - API服务中的翻译相关方法处理与AI模型的翻译交互
src/renderer/src/config/translate.ts - 翻译配置,定义支持的语言和翻译选项
翻译组件
src/renderer/src/components/TranslateButton.tsx - 翻译按钮组件,提供快速翻译功能
src/renderer/src/components/TranslateHistory/index.tsx - 翻译历史组件,显示翻译历史记录
src/renderer/src/components/TranslateHistory/HistoryItem.tsx - 翻译历史项组件,显示单条翻译历史
src/renderer/src/pages/translate/TranslateInput.tsx - 翻译输入组件,处理用户输入
src/renderer/src/pages/translate/TranslateResult.tsx - 翻译结果组件,显示翻译结果
src/renderer/src/pages/translate/LanguageSelector.tsx - 语言选择器组件,选择源语言和目标语言
翻译数据管理
src/renderer/src/databases/index.ts - 翻译历史数据库表定义,存储翻译历史记录
src/renderer/src/types/index.ts - 翻译相关类型定义定义了TranslateHistory等类型
src/renderer/src/hooks/useTranslateHistory.ts - 翻译历史钩子提供历史记录操作的React钩子
src/renderer/src/services/TranslateHistoryService.ts - 翻译历史服务,管理翻译历史记录
src/renderer/src/utils/translate.ts - 翻译工具函数,提供翻译相关的辅助功能
翻译设置
src/renderer/src/pages/settings/TranslateSettings.tsx - 翻译设置页面,配置翻译参数
src/renderer/src/store/llm.ts - 翻译模型状态管理存储翻译使用的AI模型
src/renderer/src/hooks/useTranslateSettings.ts - 翻译设置钩子提供设置操作的React钩子
src/renderer/src/config/translate.ts - 翻译配置,定义默认设置和选项
国际化支持
src/renderer/src/i18n/index.ts - 国际化支持,管理应用的多语言支持
src/renderer/src/i18n/locales/en-us.json - 英语翻译文件
src/renderer/src/i18n/locales/zh-cn.json - 简体中文翻译文件
src/renderer/src/i18n/locales/zh-tw.json - 繁体中文翻译文件
src/renderer/src/i18n/locales/ja-jp.json - 日语翻译文件
src/renderer/src/i18n/locales/ru-ru.json - 俄语翻译文件
src/renderer/src/i18n/translate/es-es.json - 西班牙语翻译文件(机器翻译)
src/renderer/src/i18n/translate/fr-fr.json - 法语翻译文件(机器翻译)
src/renderer/src/i18n/translate/pt-pt.json - 葡萄牙语翻译文件(机器翻译)
src/renderer/src/i18n/translate/el-gr.json - 希腊语翻译文件(机器翻译)
src/renderer/src/hooks/useLanguage.ts - 语言钩子,提供语言切换功能
翻译快捷功能
src/renderer/src/components/TranslateButton.tsx - 翻译按钮组件,提供快速翻译功能
src/renderer/src/hooks/useQuickTranslate.ts - 快速翻译钩子,提供快捷翻译功能
src/renderer/src/utils/clipboard.ts - 剪贴板工具函数,支持翻译剪贴板内容
src/renderer/src/components/QuickPanel/index.tsx - 快速面板中的翻译功能
翻译与AI集成
src/renderer/src/services/ApiService.ts - API服务中的翻译方法使用AI模型进行翻译
src/renderer/src/services/AssistantService.ts - 助手服务中的翻译助手创建功能
src/renderer/src/config/prompts.ts - 翻译相关提示词配置
src/renderer/src/utils/translatePrompt.ts - 翻译提示词工具,生成翻译提示词
这些文件共同构成了Cherry Studio的翻译功能提供了强大的文本翻译能力支持多种语言之间的互译并与AI模型深度集成实现高质量的翻译结果。

View File

@ -0,0 +1,49 @@
绘画功能
本文档记录了Cherry Studio的绘画功能相关文件。
绘画核心功能
src/renderer/src/pages/paintings/PaintingsPage.tsx - 绘画页面,显示主要绘画界面,包含画板、设置面板和绘画列表
src/renderer/src/store/paintings.ts - 绘画状态管理使用Redux管理绘画数据和状态
src/renderer/src/hooks/usePaintings.ts - 绘画相关钩子函数提供绘画操作的React钩子
src/renderer/src/providers/AiProvider/index.ts - AI提供者中的图像生成方法处理与AI模型的图像生成交互
src/renderer/src/services/ApiService.ts - API服务中的图像生成相关方法
绘画组件
src/renderer/src/pages/paintings/Artboard/index.tsx - 画板组件,显示生成的图像
src/renderer/src/pages/paintings/Artboard/ImageDisplay.tsx - 图像显示组件,处理图像渲染
src/renderer/src/pages/paintings/Artboard/ImageControls.tsx - 图像控制组件,提供图像操作按钮
src/renderer/src/pages/paintings/PaintingsList/index.tsx - 绘画列表组件,显示所有绘画项目
src/renderer/src/pages/paintings/PaintingsList/PaintingItem.tsx - 绘画项组件,显示单个绘画项目
src/renderer/src/pages/paintings/SettingsPanel/index.tsx - 设置面板组件,提供绘画参数设置
src/renderer/src/pages/paintings/SettingsPanel/ModelSelector.tsx - 模型选择器组件,选择绘画模型
src/renderer/src/pages/paintings/SettingsPanel/SizeSelector.tsx - 尺寸选择器组件,设置图像尺寸
src/renderer/src/pages/paintings/SettingsPanel/AdvancedSettings.tsx - 高级设置组件,提供更多绘画参数设置
绘画模型
src/renderer/src/config/models.ts - 绘画模型配置,定义支持的图像生成模型
src/renderer/src/providers/AiProvider/index.ts - AI提供者中的图像生成功能实现
src/renderer/src/providers/AiProvider/OpenAIProvider.ts - OpenAI提供者中的DALL-E模型实现
src/renderer/src/providers/AiProvider/StabilityProvider.ts - Stability AI提供者实现
src/renderer/src/providers/AiProvider/MidjourneyProvider.ts - Midjourney提供者实现
src/renderer/src/utils/imageGeneration.ts - 图像生成工具函数
绘画设置
src/renderer/src/types/index.ts - 绘画相关类型定义定义了Painting等类型
src/renderer/src/pages/settings/PaintingsSettings.tsx - 绘画设置页面,配置绘画参数
src/renderer/src/hooks/usePaintingSettings.ts - 绘画设置钩子提供设置操作的React钩子
src/renderer/src/config/paintingDefaults.ts - 绘画默认设置,定义默认参数
绘画文件管理
src/renderer/src/services/FileManager.ts - 文件管理服务中的绘画文件处理
src/renderer/src/hooks/usePaintingFiles.ts - 绘画文件钩子提供文件操作的React钩子
src/renderer/src/utils/file.ts - 文件工具函数,处理绘画文件
src/renderer/src/utils/imageCompression.ts - 图像压缩工具函数,优化图像存储
提示词增强
src/renderer/src/services/PromptEnhancementService.ts - 提示词增强服务,改进绘画提示词
src/renderer/src/utils/promptEnhancement.ts - 提示词增强工具函数
src/renderer/src/hooks/usePromptEnhancement.ts - 提示词增强钩子提供增强功能的React钩子
绘画导出与分享
src/renderer/src/pages/paintings/Artboard/ExportOptions.tsx - 导出选项组件,提供图像导出功能
src/renderer/src/utils/export.ts - 导出工具函数,处理图像导出
src/renderer/src/utils/share.ts - 分享工具函数,处理图像分享
src/renderer/src/components/ShareButton.tsx - 分享按钮组件,提供快速分享功能
绘画与翻译集成
src/renderer/src/pages/paintings/PaintingsPage.tsx - 绘画页面中的翻译功能
src/renderer/src/components/TranslateButton.tsx - 翻译按钮组件,用于翻译绘画提示词
src/renderer/src/services/TranslateService.ts - 翻译服务,提供提示词翻译功能
这些文件共同构成了Cherry Studio的绘画功能提供了强大的AI图像生成能力支持多种模型和参数设置实现高质量的图像创作。

View File

@ -0,0 +1,58 @@
import { BulbOutlined, CloseOutlined, CopyOutlined, SendOutlined } from '@ant-design/icons'
文件管理
本文档记录了Cherry Studio的文件管理相关文件。
文件管理核心功能
src/renderer/src/services/FileManager.ts - 文件管理服务,实现了文件的核心功能,包括上传、下载、删除和查询
src/renderer/src/pages/files/FilesPage.tsx - 文件页面,显示文件管理界面,包含文件列表和操作按钮
src/main/services/FileService.ts - 主进程文件服务,处理底层文件系统操作
src/main/services/FileStorage.ts - 文件存储服务,管理文件的持久化存储
src/renderer/src/utils/file.ts - 文件工具函数,提供文件处理的辅助功能
文件数据管理
src/renderer/src/databases/index.ts - 文件数据库表定义,存储文件元数据
src/renderer/src/types/index.ts - 文件相关类型定义定义了FileType等类型
src/renderer/src/hooks/useFiles.ts - 文件钩子提供文件操作的React钩子
src/renderer/src/store/files.ts - 文件状态管理使用Redux管理文件数据
src/renderer/src/utils/fileType.ts - 文件类型工具函数,识别和处理不同类型的文件
文件组件
src/renderer/src/pages/files/FileList/index.tsx - 文件列表组件,显示所有文件
src/renderer/src/pages/files/FileList/FileItem.tsx - 文件项组件,显示单个文件
src/renderer/src/pages/files/FileUploader/index.tsx - 文件上传组件,处理文件上传
src/renderer/src/pages/files/FileUploader/DropZone.tsx - 拖放区组件,支持拖放上传
src/renderer/src/components/FileViewer/index.tsx - 文件查看器组件,查看文件内容
src/renderer/src/components/FileViewer/ImageViewer.tsx - 图片查看器组件,查看图片文件
src/renderer/src/components/FileViewer/PDFViewer.tsx - PDF查看器组件查看PDF文件
src/renderer/src/components/FileViewer/TextViewer.tsx - 文本查看器组件,查看文本文件
src/renderer/src/components/FileViewer/AudioViewer.tsx - 音频查看器组件,查看音频文件
src/renderer/src/components/FileViewer/VideoViewer.tsx - 视频查看器组件,查看视频文件
PDF处理
src/main/services/PDFService.ts - PDF服务提供PDF处理功能
src/renderer/src/components/PDFSettingsInitializer/index.tsx - PDF设置初始化组件
src/renderer/src/components/PDFViewer/index.tsx - PDF查看器组件查看PDF文件
src/renderer/src/components/PDFViewer/PDFControls.tsx - PDF控制组件提供PDF操作按钮
src/renderer/src/pages/settings/PDFSettings.tsx - PDF设置页面配置PDF处理参数
src/renderer/src/utils/pdf.ts - PDF工具函数处理PDF文件
文件导出
src/renderer/src/utils/export.ts - 导出工具函数,处理文件导出
src/renderer/src/components/ExportButton/index.tsx - 导出按钮组件,提供快速导出功能
src/main/services/ExportService.ts - 导出服务,处理文件导出到外部应用
src/renderer/src/utils/exportFormats.ts - 导出格式工具函数,支持多种导出格式
src/renderer/src/components/Popups/ExportPopup.tsx - 导出弹窗组件,配置导出选项
云存储集成
src/main/services/NutstoreService.ts - 坚果云服务,提供坚果云集成
src/renderer/src/store/nutstore.ts - 坚果云状态管理使用Redux管理坚果云数据
src/renderer/src/components/NutstorePathSelector.tsx - 坚果云路径选择器组件
src/renderer/src/pages/settings/DataSettings/NutstoreSettings.tsx - 坚果云设置页面
src/renderer/src/services/NutstoreService.ts - 渲染进程坚果云服务
src/renderer/src/utils/webdav.ts - WebDAV工具函数支持WebDAV协议
文件操作工具
src/main/utils/file.ts - 主进程文件操作工具函数,提供底层文件系统操作
src/renderer/src/utils/file.ts - 渲染进程文件操作工具函数,提供前端文件处理功能
src/renderer/src/utils/fileIcon.ts - 文件图标工具函数,根据文件类型显示不同图标
src/renderer/src/utils/filePath.ts - 文件路径工具函数,处理文件路径
src/renderer/src/utils/fileSize.ts - 文件大小工具函数,格式化文件大小显示
src/renderer/src/utils/imageCompression.ts - 图像压缩工具函数,优化图像存储
Gemini文件管理
src/renderer/src/pages/files/GeminiFiles.tsx - Gemini文件组件管理Gemini模型的文件
src/renderer/src/services/GeminiFileService.ts - Gemini文件服务处理Gemini文件操作
src/main/services/GeminiService.ts - Gemini服务提供Gemini API集成
这些文件共同构成了Cherry Studio的文件管理功能提供了全面的文件上传、存储、查看和导出能力支持多种文件格式和云存储集成为用户提供便捷的文件管理体验

View File

@ -0,0 +1,73 @@
用户界面与组件
本文档记录了Cherry Studio的用户界面与组件相关文件。
主要页面
src/renderer/src/pages/home/HomePage.tsx - 首页,显示主要聊天界面,包含侧边栏、导航栏和聊天区域
src/renderer/src/pages/agents/AgentsPage.tsx - 代理页面管理和使用AI代理
src/renderer/src/pages/paintings/PaintingsPage.tsx - 绘画页面提供AI图像生成功能
src/renderer/src/pages/translate/TranslatePage.tsx - 翻译页面,提供文本翻译功能
src/renderer/src/pages/files/FilesPage.tsx - 文件页面,管理上传和存储的文件
src/renderer/src/pages/knowledge/KnowledgePage.tsx - 知识库页面,管理知识库和检索
src/renderer/src/pages/apps/AppsPage.tsx - 应用页面,访问集成的第三方应用
src/renderer/src/pages/workspace/index.tsx - 工作区页面,管理工作区文件和代码
src/renderer/src/pages/settings/SettingsPage.tsx - 设置页面,配置应用参数和选项
src/renderer/src/pages/history/HistoryPage.tsx - 历史页面,查看对话历史记录
核心组件
src/renderer/src/components/app/Sidebar.tsx - 侧边栏组件,提供主导航
src/renderer/src/components/TopView/index.tsx - 顶部视图容器,管理顶层视图
src/renderer/src/components/QuickPanel/index.tsx - 快速面板组件,提供快捷功能访问
src/renderer/src/components/Modal/index.tsx - 模态框组件,显示弹出对话框
src/renderer/src/components/Popups/index.tsx - 弹窗组件,显示各种弹出窗口
src/renderer/src/components/Layout/index.tsx - 布局组件,提供常用布局结构
src/renderer/src/components/Avatar/index.tsx - 头像组件,显示用户和助手头像
src/renderer/src/components/Markdown/index.tsx - Markdown组件渲染Markdown内容
src/renderer/src/components/CodeBlock/index.tsx - 代码块组件,显示代码高亮
src/renderer/src/components/Scrollbar/index.tsx - 滚动条组件,提供自定义滚动条
上下文提供者
src/renderer/src/context/AntdProvider.tsx - Ant Design提供者配置Ant Design主题和组件
src/renderer/src/context/StyleSheetManager.tsx - 样式表管理器,管理样式表渲染
src/renderer/src/context/SyntaxHighlighterProvider.tsx - 语法高亮提供者,提供代码高亮功能
src/renderer/src/context/ThemeProvider.tsx - 主题提供者,管理应用主题
src/renderer/src/context/MemoryProvider.tsx - 记忆提供者管理AI记忆功能
src/renderer/src/context/DeepClaudeProvider.tsx - Deep Claude提供者管理增强型Claude功能
样式与主题
src/renderer/src/assets/styles/index.scss - 主样式文件,导入所有样式
src/renderer/src/assets/styles/markdown.scss - Markdown样式定义Markdown渲染样式
src/renderer/src/assets/styles/ant.scss - Ant Design样式覆盖自定义Ant Design组件样式
src/renderer/src/assets/styles/scrollbar.scss - 滚动条样式,定义自定义滚动条样式
src/renderer/src/assets/styles/container.scss - 容器样式,定义常用容器样式
src/renderer/src/assets/styles/animation.scss - 动画样式,定义动画效果
src/renderer/src/assets/images/ - 图片资源目录,存储应用图片
src/renderer/src/assets/images/models/ - 模型图标目录存储AI模型图标
src/renderer/src/assets/images/providers/ - 提供者图标目录存储AI提供者图标
src/renderer/src/assets/images/apps/ - 应用图标目录,存储第三方应用图标
src/renderer/src/assets/fonts/ - 字体资源目录,存储应用字体
src/renderer/src/assets/icons/ - 图标资源目录,存储应用图标
迷你窗口
src/renderer/src/windows/mini/App.tsx - 迷你应用,轻量级应用窗口
src/renderer/src/windows/mini/home/HomeWindow.tsx - 迷你首页窗口,显示简化的聊天界面
src/renderer/src/windows/mini/translate/TranslateWindow.tsx - 迷你翻译窗口,提供快速翻译功能
src/renderer/src/windows/mini/home/components/InputBar.tsx - 迷你输入栏组件,处理用户输入
src/renderer/src/windows/mini/home/components/MessageList.tsx - 迷你消息列表组件,显示消息
通用组件
src/renderer/src/components/Button/index.tsx - 按钮组件,提供自定义按钮
src/renderer/src/components/Input/index.tsx - 输入组件,提供自定义输入框
src/renderer/src/components/Markdown/index.tsx - Markdown组件渲染Markdown内容
src/renderer/src/components/CodeBlock/index.tsx - 代码块组件,显示代码高亮
src/renderer/src/components/Dropdown/index.tsx - 下拉菜单组件,提供自定义下拉菜单
src/renderer/src/components/Tooltip/index.tsx - 工具提示组件,显示提示信息
src/renderer/src/components/Switch/index.tsx - 开关组件,提供自定义开关
src/renderer/src/components/Tabs/index.tsx - 标签页组件,提供自定义标签页
src/renderer/src/components/Card/index.tsx - 卡片组件,提供自定义卡片
src/renderer/src/components/List/index.tsx - 列表组件,提供自定义列表
src/renderer/src/components/DragableList/index.tsx - 可拖动列表组件,支持拖拽排序
src/renderer/src/components/IndicatorLight.tsx - 指示灯组件,显示状态指示
src/renderer/src/components/CustomTag.tsx - 自定义标签组件,显示标签
src/renderer/src/components/ModelTags.tsx - 模型标签组件,显示模型功能标签
src/renderer/src/components/ModelTagsWithLabel.tsx - 带标签的模型标签组件
导航与路由
src/renderer/src/handler/NavigationHandler.tsx - 导航处理器,管理应用导航
src/renderer/src/App.tsx - 应用路由配置,定义主要路由
src/renderer/src/components/app/Sidebar.tsx - 侧边栏导航组件
src/renderer/src/pages/home/Navbar/index.tsx - 导航栏组件,提供二级导航
src/renderer/src/utils/navigation.ts - 导航工具函数,处理导航逻辑
这些文件共同构成了Cherry Studio的用户界面与组件系统提供了丰富的UI组件和页面实现了美观、易用的用户界面支持多种主题和样式定制。

View File

@ -0,0 +1,75 @@
数据库与存储
本文档记录了Cherry Studio的数据库与存储相关文件。
数据库核心
src/renderer/src/databases/index.ts - 数据库初始化和表定义使用Dexie.js封装IndexedDB
src/renderer/src/databases/upgrades.ts - 数据库升级脚本,处理数据库版本升级
src/renderer/src/utils/storage.ts - 存储工具函数,提供本地存储操作
src/main/utils/storage.ts - 主进程存储工具函数,处理文件系统存储
src/renderer/src/services/StorageService.ts - 存储服务,提供统一的存储接口
数据表
files - 文件表,存储上传文件的元数据
字段id, name, origin_name, path, size, ext, type, created_at, count
用途:管理用户上传的文件信息
topics - 话题表,存储对话话题和消息
字段id, messages
用途保存用户与AI助手的对话话题
settings - 设置表,存储应用设置
字段id, value
用途:保存用户配置和应用设置
knowledge_notes - 知识库笔记表,存储知识库笔记
字段id, baseId, type, content, created_at, updated_at
用途:管理知识库中的笔记内容
translate_history - 翻译历史表,存储翻译记录
字段id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt
用途:保存翻译历史记录
quick_phrases - 快速短语表,存储常用短语
字段id, content, createdAt
用途:管理用户定义的快速短语
workspaces - 工作区表,存储工作区配置
字段id, name, path, createdAt, updatedAt
用途:管理用户的工作区配置
状态管理
src/renderer/src/store/index.ts - Redux存储配置设置Redux存储和中间件
src/renderer/src/store/migrate.ts - 状态迁移处理Redux状态版本升级
src/renderer/src/store/assistants.ts - 助手状态管理AI助手数据
src/renderer/src/store/agents.ts - 代理状态管理AI代理数据
src/renderer/src/store/backup.ts - 备份状态,管理数据备份配置
src/renderer/src/store/copilot.ts - Copilot状态管理GitHub Copilot集成
src/renderer/src/store/knowledge.ts - 知识库状态,管理知识库数据
src/renderer/src/store/llm.ts - 语言模型状态管理AI模型和提供者
src/renderer/src/store/mcp.ts - MCP状态管理模型上下文协议服务
src/renderer/src/store/memory.ts - 记忆状态管理AI记忆功能
src/renderer/src/store/messages.ts - 消息状态,管理对话消息
src/renderer/src/store/workspace.ts - 工作区状态,管理工作区配置
src/renderer/src/store/minapps.ts - 小程序状态,管理集成的第三方应用
src/renderer/src/store/nutstore.ts - 坚果云状态,管理坚果云集成
src/renderer/src/store/paintings.ts - 绘画状态管理AI绘画数据
src/renderer/src/store/runtime.ts - 运行时状态,管理应用运行时配置
src/renderer/src/store/settings.ts - 设置状态,管理应用设置
src/renderer/src/store/shortcuts.ts - 快捷键状态,管理键盘快捷键
src/renderer/src/store/websearch.ts - 网页搜索状态,管理网页搜索配置
持久化
src/renderer/src/store/index.ts - Redux持久化配置使用redux-persist实现状态持久化
src/main/services/BackupService.ts - 备份服务,提供数据备份和恢复功能
src/renderer/src/services/BackupService.ts - 渲染进程备份服务,管理备份操作
src/renderer/src/utils/backup.ts - 备份工具函数,处理备份数据格式
src/main/utils/zip.ts - 压缩工具函数,用于备份数据压缩
src/main/utils/aes.ts - 加密工具函数,用于备份数据加密
本地存储
src/main/utils/storage.ts - 主进程存储工具函数,处理文件系统存储
src/renderer/src/utils/storage.ts - 渲染进程存储工具函数,处理浏览器存储
src/renderer/src/services/KeyvStorageService.ts - Keyv存储服务提供键值对存储
src/renderer/src/init.ts - 初始化Keyv存储
src/main/services/FileStorage.ts - 文件存储服务,管理文件的持久化存储
数据同步
src/renderer/src/services/SyncService.ts - 同步服务,管理数据同步
src/main/services/NutstoreService.ts - 坚果云服务,提供云存储同步
src/renderer/src/services/NutstoreService.ts - 渲染进程坚果云服务
src/renderer/src/pages/settings/DataSettings/SyncSettings.tsx - 同步设置页面
src/renderer/src/utils/sync.ts - 同步工具函数,处理数据同步逻辑
数据迁移
src/renderer/src/databases/upgrades.ts - 数据库升级脚本,处理数据库版本升级
src/renderer/src/store/migrate.ts - 状态迁移处理Redux状态版本升级
src/renderer/src/utils/migration.ts - 迁移工具函数,处理数据迁移
src/renderer/src/services/MigrationService.ts - 迁移服务,管理数据迁移过程
这些文件共同构成了Cherry Studio的数据库与存储系统提供了可靠的数据持久化、状态管理和同步功能确保用户数据的安全存储和高效访问。

View File

@ -0,0 +1,81 @@
工具与实用功能
本文档记录了Cherry Studio的工具与实用功能相关文件。
语音识别
src/renderer/src/assets/asr-server/ - ASR服务器资源目录包含语音识别服务器代码
src/renderer/src/assets/asr-server/server.js - ASR服务器主程序
src/renderer/src/assets/asr-server/index.html - ASR服务器Web界面
src/renderer/src/assets/asr-server/package.json - ASR服务器依赖配置
src/renderer/src/components/ASRButton/index.tsx - 语音识别按钮组件,提供语音输入功能
src/renderer/src/services/ASRService.ts - 语音识别服务,处理语音转文本
src/renderer/src/services/ASRServerService.ts - 语音识别服务器服务管理ASR服务器
public/asr-server/ - ASR服务器公共文件用于打包发布
public/asr-server/server.js - 打包后的ASR服务器主程序
public/asr-server/package.json - 打包后的ASR服务器依赖配置
代码执行
src/main/services/CodeExecutorService.ts - 代码执行服务提供JavaScript和Python代码执行功能
src/preload/index.ts - 代码执行预加载脚本暴露代码执行API
src/renderer/src/components/CodeBlock/index.tsx - 代码块组件,支持代码执行
src/renderer/src/components/CodeBlock/CodeExecutor.tsx - 代码执行器组件,处理代码执行
src/renderer/src/utils/codeExecution.ts - 代码执行工具函数,处理代码执行逻辑
src/renderer/src/pages/settings/CodeExecutionSettings.tsx - 代码执行设置页面
网页搜索
src/main/services/SearchService.ts - 搜索服务,提供网页搜索功能
src/renderer/src/store/websearch.ts - 网页搜索状态管理使用Redux管理搜索配置
src/renderer/src/components/WebSearchPanel/index.tsx - 网页搜索面板组件,显示搜索结果
src/renderer/src/components/WebSearchPanel/SearchResult.tsx - 搜索结果组件,显示单个搜索结果
src/renderer/src/services/WebSearchService.ts - 网页搜索服务,处理搜索请求
src/renderer/src/utils/search.ts - 搜索工具函数,处理搜索逻辑
src/main/mcpServers/brave-search.ts - Brave搜索MCP服务器提供搜索功能
快捷键
src/main/services/ShortcutService.ts - 快捷键服务,提供全局快捷键功能
src/renderer/src/store/shortcuts.ts - 快捷键状态管理使用Redux管理快捷键配置
src/renderer/src/hooks/useShortcuts.ts - 快捷键钩子函数提供快捷键操作的React钩子
src/renderer/src/pages/settings/ShortcutSettings.tsx - 快捷键设置页面,配置快捷键
src/renderer/src/utils/shortcuts.ts - 快捷键工具函数,处理快捷键逻辑
src/renderer/src/components/ShortcutInput/index.tsx - 快捷键输入组件,捕获快捷键
代理管理
src/renderer/src/services/ProxyService.ts - 代理服务,提供网络代理功能
src/main/services/ProxyManager.ts - 代理管理器,管理系统代理设置
src/renderer/src/pages/settings/ProxySettings.tsx - 代理设置页面,配置代理参数
src/renderer/src/utils/proxy.ts - 代理工具函数,处理代理逻辑
src/main/utils/proxy.ts - 主进程代理工具函数,处理系统代理设置
窗口管理
src/main/services/WindowService.ts - 窗口服务,管理应用窗口
src/main/services/TrayService.ts - 托盘服务,管理系统托盘图标
src/renderer/src/components/TopView/index.tsx - 顶部视图容器,管理顶层视图
src/renderer/src/utils/window.ts - 窗口工具函数,处理窗口操作
src/main/utils/window.ts - 主进程窗口工具函数,处理窗口创建和管理
实用工具函数
src/renderer/src/utils/index.ts - 通用工具函数集合
src/renderer/src/utils/format.ts - 格式化工具函数,处理文本格式化
src/renderer/src/utils/date.ts - 日期工具函数,处理日期和时间
src/renderer/src/utils/string.ts - 字符串工具函数,处理字符串操作
src/renderer/src/utils/array.ts - 数组工具函数,处理数组操作
src/renderer/src/utils/object.ts - 对象工具函数,处理对象操作
src/renderer/src/utils/math.ts - 数学工具函数,处理数学计算
src/renderer/src/utils/color.ts - 颜色工具函数,处理颜色转换
src/renderer/src/utils/clipboard.ts - 剪贴板工具函数,处理剪贴板操作
src/renderer/src/utils/analytics.ts - 分析工具函数,处理使用分析
小程序支持
src/renderer/src/store/minapps.ts - 小程序状态管理使用Redux管理小程序配置
src/renderer/src/pages/apps/AppsPage.tsx - 应用页面,管理和使用小程序
src/renderer/src/pages/apps/App.tsx - 小程序组件,显示单个小程序
src/renderer/src/config/minapps.ts - 小程序配置,定义默认小程序
src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx - 小程序设置页面
src/renderer/src/components/Popups/MinAppsPopover.tsx - 小程序弹出窗口组件
其他工具
src/main/services/ObsidianVaultService.ts - Obsidian集成服务提供与Obsidian的集成
src/renderer/src/utils/export.ts - 导出工具函数,处理内容导出
src/renderer/src/components/Popups/ObsidianExportPopup.tsx - Obsidian导出弹窗组件
src/main/services/AppUpdater.ts - 应用更新服务,处理应用自动更新
src/renderer/src/pages/settings/AboutSettings.tsx - 关于设置页面,显示更新信息
src/renderer/src/utils/update.ts - 更新工具函数,处理更新检查和安装
协议客户端
src/main/services/ProtocolClient.ts - 协议客户端服务,处理自定义协议
src/main/index.ts - 注册协议处理程序
src/renderer/src/utils/protocol.ts - 协议工具函数处理协议URL
配置管理
src/main/services/ConfigManager.ts - 配置管理服务,管理应用配置
src/renderer/src/utils/config.ts - 配置工具函数,处理配置操作
src/renderer/src/pages/settings/SettingsPage.tsx - 设置页面,管理所有配置
这些文件共同构成了Cherry Studio的工具与实用功能提供了丰富的辅助工具和实用功能增强了应用的使用体验和功能性。

View File

@ -2,8 +2,7 @@ import './services/MemoryFileService'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
import { app, ipcMain } from 'electron'
import { app } from 'electron'
import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } from 'electron-devtools-installer'
import Logger from 'electron-log'
@ -60,13 +59,7 @@ if (!app.requestSingleInstanceLock()) {
.then((name) => console.log(`Added Extension: ${name}`))
.catch((err) => console.log('An error occurred: ', err))
}
ipcMain.handle(IpcChannel.System_GetDeviceType, () => {
return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux'
})
ipcMain.handle(IpcChannel.System_GetHostname, () => {
return require('os').hostname()
})
// 系统相关的IPC处理程序已在ipc.ts中注册
})
registerProtocolClient(app)

View File

@ -6,7 +6,7 @@ import { arch } from 'node:os'
import { isMac, isWin } from '@main/constant'
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
import { IpcChannel } from '@shared/IpcChannel'
import { Shortcut, ThemeMode } from '@types'
import { MCPServer, Shortcut, ThemeMode } from '@types' // Import MCPServer here
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import log from 'electron-log'
@ -33,6 +33,7 @@ import { searchService } from './services/SearchService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { windowService } from './services/WindowService'
import WorkspaceService from './services/WorkspaceService'
import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
import { getConfigDir, getFilesDir } from './utils/file'
@ -278,6 +279,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources)
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource)
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
// Add handler for rerunTool
// Update handler for rerunTool to accept serverId and toolName
ipcMain.handle(
IpcChannel.Mcp_RerunTool,
(
event,
messageId: string,
toolCallId: string,
server: MCPServer, // Changed from serverId: string to server: MCPServer
toolName: string,
args: Record<string, any>
) => mcpService.rerunTool(event, messageId, toolCallId, server, toolName, args) // Pass the full server object
)
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
@ -359,4 +373,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// PDF服务
ipcMain.handle(IpcChannel.PDF_SplitPDF, PDFService.splitPDF.bind(PDFService))
ipcMain.handle(IpcChannel.PDF_GetPageCount, PDFService.getPDFPageCount.bind(PDFService))
// 工作区服务
const workspaceService = new WorkspaceService()
ipcMain.handle('workspace:selectFolder', workspaceService.selectWorkspaceFolder)
ipcMain.handle('workspace:getFiles', workspaceService.getWorkspaceFiles)
ipcMain.handle('workspace:readFile', workspaceService.readWorkspaceFile)
ipcMain.handle('workspace:getFolderStructure', workspaceService.getWorkspaceFolderStructure)
}

View File

@ -7,8 +7,13 @@ import FileSystemServer from './filesystem'
import MemoryServer from './memory'
import ThinkingServer from './sequentialthinking'
import SimpleRememberServer from './simpleremember'
import { WorkspaceFileToolServer } from './workspacefile'
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
export async function createInMemoryMCPServer(
name: string,
args: string[] = [],
envs: Record<string, string> = {}
): Promise<Server> {
Logger.info(`[MCP] Creating in-memory MCP server: ${name} with args: ${args} and envs: ${JSON.stringify(envs)}`)
switch (name) {
case '@cherry/memory': {
@ -31,6 +36,32 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs:
const envPath = envs.SIMPLEREMEMBER_FILE_PATH
return new SimpleRememberServer(envPath).server
}
case '@cherry/workspacefile': {
const workspacePath = envs.WORKSPACE_PATH
if (!workspacePath) {
throw new Error('WORKSPACE_PATH environment variable is required for WorkspaceFileTool server')
}
// 验证工作区路径是否存在
try {
const fs = require('fs/promises')
const stats = await fs.stat(workspacePath)
if (!stats.isDirectory()) {
throw new Error(`工作区路径不是一个目录: ${workspacePath}`)
}
} catch (error) {
Logger.error(`[WorkspaceFileTool] 工作区路径无效:`, error)
// 添加类型检查,确保 error 是 Error 实例
if (error instanceof Error) {
throw new Error(`工作区路径无效: ${error.message}`)
} else {
// 如果不是 Error 实例,抛出通用错误
throw new Error(`工作区路径无效: 未知错误`)
}
}
return new WorkspaceFileToolServer(workspacePath).server
}
default:
throw new Error(`Unknown in-memory MCP server: ${name}`)
}

View File

@ -0,0 +1,523 @@
// src/main/mcpServers/workspacefile.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
ToolSchema
} from '@modelcontextprotocol/sdk/types.js'
import { createTwoFilesPatch } from 'diff'
import Logger from 'electron-log'
import fs from 'fs/promises'
import { minimatch } from 'minimatch'
import path from 'path'
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
// 工具名称常量
const TOOL_READ_FILE = 'workspace_read_file'
const TOOL_WRITE_FILE = 'workspace_write_file'
const TOOL_SEARCH_FILES = 'workspace_search_files'
const TOOL_LIST_FILES = 'workspace_list_files'
const TOOL_CREATE_FILE = 'workspace_create_file'
const TOOL_EDIT_FILE = 'workspace_edit_file'
// 规范化路径
function normalizePath(p: string): string {
return path.normalize(p)
}
// 验证路径是否在允许的工作区内
async function validatePath(workspacePath: string, requestedPath: string): Promise<string> {
// 增加日志输出,便于调试
Logger.info(`[WorkspaceFileTool] 验证路径: workspacePath=${workspacePath}, requestedPath=${requestedPath}`)
// 如果请求的路径为空,直接返回工作区路径
if (!requestedPath || requestedPath === '.') {
Logger.info(`[WorkspaceFileTool] 请求的路径为空或为'.',返回工作区路径: ${workspacePath}`)
return workspacePath
}
// 检查请求的路径是否已经包含工作区路径
// 例如,如果工作区是 "测试",而请求的路径是 "测试/文件.txt",则应该处理为 "文件.txt"
const workspaceName = path.basename(workspacePath)
try {
// 使用更安全的方式检测路径前缀
const workspacePattern = new RegExp(`^${workspaceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\/]`)
// 如果路径以工作区名称开头,则移除工作区名称部分
if (workspacePattern.test(requestedPath)) {
Logger.info(`[WorkspaceFileTool] 检测到路径包含工作区名称,原路径: ${requestedPath}`)
requestedPath = requestedPath.replace(workspacePattern, '')
Logger.info(`[WorkspaceFileTool] 处理后的路径: ${requestedPath}`)
}
} catch (error) {
Logger.error(`[WorkspaceFileTool] 处理路径前缀时出错:`, error)
// 出错时不做处理,继续使用原始路径
}
// 如果请求的路径是相对路径,则相对于工作区路径
const absolute = path.isAbsolute(requestedPath)
? path.resolve(requestedPath)
: path.resolve(workspacePath, requestedPath)
const normalizedRequested = normalizePath(absolute)
const normalizedWorkspace = normalizePath(workspacePath)
// 检查路径是否在工作区内
if (!normalizedRequested.startsWith(normalizedWorkspace)) {
throw new McpError(
ErrorCode.InvalidParams,
`访问被拒绝 - 路径超出工作区范围: ${requestedPath} 不在 ${workspacePath}`
)
}
// 处理符号链接
try {
const realPath = await fs.realpath(absolute)
const normalizedReal = normalizePath(realPath)
if (!normalizedReal.startsWith(normalizedWorkspace)) {
throw new McpError(ErrorCode.InvalidParams, '访问被拒绝 - 符号链接目标超出工作区范围')
}
return realPath
} catch (error) {
// 对于尚不存在的新文件,验证父目录
const parentDir = path.dirname(absolute)
try {
const realParentPath = await fs.realpath(parentDir)
const normalizedParent = normalizePath(realParentPath)
if (!normalizedParent.startsWith(normalizedWorkspace)) {
throw new McpError(ErrorCode.InvalidParams, '访问被拒绝 - 父目录超出工作区范围')
}
return absolute
} catch {
throw new McpError(ErrorCode.InvalidParams, `父目录不存在: ${parentDir}`)
}
}
}
// 参数模式定义
const ReadFileArgsSchema = z.object({
path: z.string().describe('要读取的文件路径,可以是相对于工作区的路径')
})
const WriteFileArgsSchema = z.object({
path: z.string().describe('要写入的文件路径,可以是相对于工作区的路径'),
content: z.string().describe('要写入文件的内容')
})
const SearchFilesArgsSchema = z.object({
pattern: z.string().describe('搜索模式,可以是文件名的一部分或通配符'),
excludePatterns: z.array(z.string()).optional().default([]).describe('要排除的文件模式数组')
})
const ListFilesArgsSchema = z.object({
path: z.string().optional().default('').describe('要列出文件的目录路径,默认为工作区根目录'),
recursive: z.boolean().optional().default(false).describe('是否递归列出子目录中的文件')
})
const CreateFileArgsSchema = z.object({
path: z.string().describe('要创建的文件路径,可以是相对于工作区的路径'),
content: z.string().describe('文件的初始内容')
})
const EditFileArgsSchema = z.object({
path: z.string().describe('要编辑的文件路径,可以是相对于工作区的路径'),
changes: z
.array(
z.object({
start: z.number().describe('开始行号从1开始'),
end: z.number().describe('结束行号从1开始'),
content: z.string().describe('要替换的新内容')
})
)
.describe('要应用的更改数组')
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ToolInputSchema = ToolSchema.shape.inputSchema
type ToolInput = z.infer<typeof ToolInputSchema>
// 工具实现
export class WorkspaceFileToolServer {
public server: Server
private workspacePath: string
constructor(workspacePath: string) {
if (!workspacePath) {
throw new Error('未提供工作区路径,请在环境变量中指定 WORKSPACE_PATH')
}
this.workspacePath = normalizePath(path.resolve(workspacePath))
// 验证工作区目录存在且可访问
this.validateWorkspace().catch((error) => {
Logger.error('验证工作区目录时出错:', error)
throw new Error(`验证工作区目录时出错: ${error}`)
})
this.server = new Server(
{
name: 'workspace-file-tool-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
)
this.initialize()
}
async validateWorkspace() {
try {
const stats = await fs.stat(this.workspacePath)
if (!stats.isDirectory()) {
Logger.error(`错误: ${this.workspacePath} 不是一个目录`)
throw new Error(`错误: ${this.workspacePath} 不是一个目录`)
}
} catch (error: any) {
Logger.error(`访问工作区目录 ${this.workspacePath} 时出错:`, error)
throw new Error(`访问工作区目录 ${this.workspacePath} 时出错:`, error)
}
}
initialize() {
// 工具处理程序
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: TOOL_READ_FILE,
description: '读取工作区中的文件内容。提供文件的完整内容,适用于查看单个文件的内容。',
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput
},
{
name: TOOL_WRITE_FILE,
description: '将内容写入工作区中的文件。如果文件不存在,将创建新文件;如果文件已存在,将覆盖其内容。',
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput
},
{
name: TOOL_SEARCH_FILES,
description: '在工作区中搜索匹配指定模式的文件。可以使用文件名的一部分或通配符进行搜索。',
inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput
},
{
name: TOOL_LIST_FILES,
description: '列出工作区中指定目录下的所有文件和子目录。可以选择是否递归列出子目录中的文件。',
inputSchema: zodToJsonSchema(ListFilesArgsSchema) as ToolInput
},
{
name: TOOL_CREATE_FILE,
description: '在工作区中创建新文件。如果文件已存在,将返回错误。',
inputSchema: zodToJsonSchema(CreateFileArgsSchema) as ToolInput
},
{
name: TOOL_EDIT_FILE,
description: '编辑工作区中的文件,可以替换指定行范围的内容。适用于对文件进行部分修改。',
inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput
}
]
}
})
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params
if (!args) {
throw new McpError(ErrorCode.InvalidParams, `未提供参数: ${name}`)
}
switch (name) {
case TOOL_READ_FILE: {
const parsed = ReadFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new McpError(ErrorCode.InvalidParams, `读取文件的参数无效: ${parsed.error}`)
}
const validPath = await validatePath(this.workspacePath, parsed.data.path)
const content = await fs.readFile(validPath, 'utf-8')
return {
content: [{ type: 'text', text: content }]
}
}
case TOOL_WRITE_FILE: {
const parsed = WriteFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new McpError(ErrorCode.InvalidParams, `写入文件的参数无效: ${parsed.error}`)
}
const validPath = await validatePath(this.workspacePath, parsed.data.path)
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
return {
content: [{ type: 'text', text: `文件已成功写入: ${parsed.data.path}` }]
}
}
case TOOL_SEARCH_FILES: {
const parsed = SearchFilesArgsSchema.safeParse(args)
if (!parsed.success) {
throw new McpError(ErrorCode.InvalidParams, `搜索文件的参数无效: ${parsed.error}`)
}
async function searchFiles(
rootPath: string,
pattern: string,
excludePatterns: string[] = []
): Promise<string[]> {
const results: string[] = []
async function search(currentPath: string, relativePath: string = '') {
const entries = await fs.readdir(currentPath, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name)
const entryRelativePath = path.join(relativePath, entry.name)
// 检查是否匹配排除模式
const shouldExclude = excludePatterns.some((pattern) => {
const globPattern = pattern.includes('*') ? pattern : `**/${pattern}/**`
return minimatch(entryRelativePath, globPattern, { dot: true })
})
if (shouldExclude) {
continue
}
if (
entry.name.toLowerCase().includes(pattern.toLowerCase()) ||
minimatch(entry.name, pattern, { nocase: true })
) {
results.push(entryRelativePath)
}
if (entry.isDirectory()) {
await search(fullPath, entryRelativePath)
}
}
}
await search(rootPath)
return results
}
const results = await searchFiles(this.workspacePath, parsed.data.pattern, parsed.data.excludePatterns)
return {
content: [
{
type: 'text',
text: results.length > 0 ? `找到 ${results.length} 个匹配项:\n${results.join('\n')}` : '未找到匹配项'
}
]
}
}
case TOOL_LIST_FILES: {
Logger.info(`[WorkspaceFileTool] 收到列出文件请求,参数:`, args)
const parsed = ListFilesArgsSchema.safeParse(args)
if (!parsed.success) {
const errorMsg = `列出文件的参数无效: ${parsed.error}`
Logger.error(`[WorkspaceFileTool] ${errorMsg}`)
throw new McpError(ErrorCode.InvalidParams, errorMsg)
}
Logger.info(
`[WorkspaceFileTool] 解析参数成功: path=${parsed.data.path}, recursive=${parsed.data.recursive}`
)
const dirPath = parsed.data.path
? await validatePath(this.workspacePath, parsed.data.path)
: this.workspacePath
async function listFiles(dirPath: string, recursive: boolean): Promise<string[]> {
try {
Logger.info(`[WorkspaceFileTool] 列出目录内容: dirPath=${dirPath}, recursive=${recursive}`)
// 检查目录是否存在
try {
const stats = await fs.stat(dirPath)
if (!stats.isDirectory()) {
Logger.error(`[WorkspaceFileTool] 路径不是目录: ${dirPath}`)
return [`[错误] 路径不是目录: ${dirPath}`]
}
} catch (error) {
Logger.error(`[WorkspaceFileTool] 目录不存在: ${dirPath}`, error)
return [`[错误] 目录不存在: ${dirPath}`]
}
const results: string[] = []
const entries = await fs.readdir(dirPath, { withFileTypes: true })
Logger.info(`[WorkspaceFileTool] 读取到 ${entries.length} 个条目`)
for (const entry of entries) {
try {
const fullPath = path.join(dirPath, entry.name)
const isDir = entry.isDirectory()
results.push(`${isDir ? '[目录]' : '[文件]'} ${entry.name}`)
if (isDir && recursive) {
try {
const subResults = await listFiles(fullPath, recursive)
results.push(...subResults.map((item) => ` ${item}`))
} catch (subError) {
Logger.error(`[WorkspaceFileTool] 读取子目录失败: ${fullPath}`, subError)
results.push(` [错误] 无法读取子目录: ${entry.name}`)
}
}
} catch (entryError) {
Logger.error(`[WorkspaceFileTool] 处理目录条目失败: ${entry.name}`, entryError)
results.push(`[错误] 无法处理条目: ${entry.name}`)
}
}
return results
} catch (error) {
Logger.error(`[WorkspaceFileTool] 列出文件时出错:`, error)
return [`[错误] 列出文件时出错: ${error instanceof Error ? error.message : String(error)}`]
}
}
try {
Logger.info(`[WorkspaceFileTool] 开始列出目录: ${dirPath}`)
const files = await listFiles(dirPath, parsed.data.recursive)
const relativeDirPath = path.relative(this.workspacePath, dirPath) || '.'
Logger.info(`[WorkspaceFileTool] 成功列出目录,找到 ${files.length} 个条目`)
const resultText = `目录 "${relativeDirPath}" 的内容:\n${files.join('\n')}`
Logger.info(
`[WorkspaceFileTool] 返回结果: ${resultText.substring(0, 100)}${resultText.length > 100 ? '...' : ''}`
)
return {
content: [
{
type: 'text',
text: resultText
}
]
}
} catch (error) {
const errorMsg = `列出目录内容时出错: ${error instanceof Error ? error.message : String(error)}`
Logger.error(`[WorkspaceFileTool] ${errorMsg}`, error)
return {
content: [
{
type: 'text',
text: `[错误] ${errorMsg}`
}
]
}
}
}
case TOOL_CREATE_FILE: {
const parsed = CreateFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new McpError(ErrorCode.InvalidParams, `创建文件的参数无效: ${parsed.error}`)
}
const validPath = await validatePath(this.workspacePath, parsed.data.path)
// 检查文件是否已存在
try {
await fs.access(validPath)
throw new McpError(ErrorCode.InvalidParams, `文件已存在: ${parsed.data.path}`)
} catch (error: any) {
// 如果文件不存在,则继续创建
if (error instanceof McpError) {
throw error
}
}
// 确保父目录存在
const parentDir = path.dirname(validPath)
await fs.mkdir(parentDir, { recursive: true })
// 创建文件
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
return {
content: [{ type: 'text', text: `文件已成功创建: ${parsed.data.path}` }]
}
}
case TOOL_EDIT_FILE: {
const parsed = EditFileArgsSchema.safeParse(args)
if (!parsed.success) {
throw new McpError(ErrorCode.InvalidParams, `编辑文件的参数无效: ${parsed.error}`)
}
const validPath = await validatePath(this.workspacePath, parsed.data.path)
// 读取原始文件内容
const originalContent = await fs.readFile(validPath, 'utf-8')
const lines = originalContent.split('\n')
// 应用更改(从后向前应用,以避免行号变化)
const sortedChanges = [...parsed.data.changes].sort((a, b) => b.start - a.start)
for (const change of sortedChanges) {
if (change.start < 1 || change.end > lines.length || change.start > change.end) {
throw new McpError(
ErrorCode.InvalidParams,
`无效的行范围: ${change.start}-${change.end},文件共有 ${lines.length}`
)
}
// 替换指定行范围的内容
const beforeLines = lines.slice(0, change.start - 1)
const afterLines = lines.slice(change.end)
const newLines = change.content.split('\n')
lines.splice(0, lines.length, ...beforeLines, ...newLines, ...afterLines)
}
// 写入修改后的内容
const newContent = lines.join('\n')
await fs.writeFile(validPath, newContent, 'utf-8')
// 生成差异信息
const diff = createTwoFilesPatch(parsed.data.path, parsed.data.path, originalContent, newContent)
return {
content: [
{
type: 'text',
text: `文件已成功编辑: ${parsed.data.path}\n\n差异信息:\n${diff}`
}
]
}
}
default:
throw new McpError(ErrorCode.MethodNotFound, `未知工具: ${name}`)
}
} catch (error) {
Logger.error(`[WorkspaceFileTool] 调用工具时出错:`, error)
if (error instanceof McpError) {
throw error
}
throw new McpError(
ErrorCode.InternalError,
`调用工具时出错: ${error instanceof Error ? error.message : String(error)}`
)
}
})
}
}

View File

@ -12,6 +12,7 @@ import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprot
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
import { nanoid } from '@reduxjs/toolkit'
import { IpcChannel } from '@shared/IpcChannel' // Import IpcChannel
import {
GetMCPPromptResponse,
GetResourceResponse,
@ -27,6 +28,7 @@ import { EventEmitter } from 'events'
import { memoize } from 'lodash'
import { CacheService } from './CacheService'
// Import configManager
import { CallBackServer } from './mcp/oauth/callback'
import { McpOAuthClientProvider } from './mcp/oauth/provider'
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
@ -137,8 +139,10 @@ class McpService {
Logger.info(`[MCP] Using in-memory transport for server: ${server.name}`)
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
// start the in-memory server with the given name and environment variables
const inMemoryServer = createInMemoryMCPServer(server.name, args, server.env || {})
// Add await here
const inMemoryServer = await createInMemoryMCPServer(server.name, args, server.env || {})
try {
// Now connect the resolved server instance
await inMemoryServer.connect(serverTransport)
Logger.info(`[MCP] In-memory server started: ${server.name}`)
} catch (error: Error | any) {
@ -349,15 +353,17 @@ class McpService {
const client = await this.initClient(server)
try {
const { tools } = await client.listTools()
const serverTools: MCPTool[] = []
tools.map((tool: any) => {
const serverTools: MCPTool[] = tools.map((tool: any) => {
// Generate the descriptive toolKey
const toolKey = `${server.id}-${tool.name}` // Combine server ID and tool name
const serverTool: MCPTool = {
...tool,
id: `f${nanoid()}`,
id: `f${nanoid()}`, // Keep the random ID for internal use (e.g., React keys)
serverId: server.id,
serverName: server.name
serverName: server.name,
toolKey: toolKey // Add the generated toolKey
}
serverTools.push(serverTool)
return serverTool
})
return serverTools
} catch (error) {
@ -560,6 +566,87 @@ class McpService {
return await cachedGetResource(server, uri)
}
/**
* Rerun a specific tool call with potentially modified arguments
*/
public rerunTool = async (
event: Electron.IpcMainInvokeEvent,
messageId: string,
toolCallId: string,
server: MCPServer, // Changed from serverId: string to server: MCPServer
toolName: string,
args: Record<string, any>
): Promise<void> => {
// Use the passed server object directly
const serverConfig = server // Rename for clarity, or just use 'server' directly
Logger.info(
`[MCP] Rerunning tool call ${toolCallId} (Server: ${serverConfig.name} [${serverConfig.id}], Tool: ${toolName}) for message ${messageId} with args:`,
args
)
// Server lookup logic is removed as the server object is passed directly
// Send 'rerunning' status update to renderer
event.sender.send(IpcChannel.Mcp_ToolRerunUpdate, {
messageId,
toolCallId,
status: 'rerunning',
args // Include the new args being used
})
// 3. Call the tool
try {
// Note: this.callTool expects the event as the first argument, but it's not strictly needed
// for the core logic here. Passing null or a placeholder if the original event isn't required.
// However, callTool itself might need the event if it sends updates. Let's pass the event.
const result = await this.callTool(event, { server: serverConfig, name: toolName, args })
// 4. Send 'done' status update with the result
event.sender.send(IpcChannel.Mcp_ToolRerunUpdate, {
messageId,
toolCallId,
status: 'done',
response: result // Send the actual tool response
})
Logger.info(`[MCP] Rerun successful for tool call ${toolCallId}`)
} catch (error: any) {
Logger.error(`[MCP] Error rerunning tool ${toolName} on ${serverConfig.name}:`, error)
// 5. Send 'error' status update
event.sender.send(IpcChannel.Mcp_ToolRerunUpdate, {
messageId,
toolCallId,
status: 'error',
error: error.message || String(error) // Send error message
})
}
// Original placeholder logic removed
/* const message = findMessageById(messageId); // Hypothetical function
const toolCall = message?.metadata?.mcpTools?.find(tc => tc.id === toolCallId);
if (toolCall && toolCall.tool) {
const serverConfig = findServerById(toolCall.tool.serverId); // Hypothetical function
if (serverConfig) {
try {
const result = await this.callTool(null, { server: serverConfig, name: toolCall.tool.name, args });
// Send result back to renderer to update the specific tool call in the message
// mainWindow?.webContents.send('mcp:toolRerunCompleted', messageId, toolCallId, result);
} catch (error) {
Logger.error(`[MCP] Error rerunning tool ${toolCall.tool.name}:`, error);
// Send error back to renderer
// mainWindow?.webContents.send('mcp:toolRerunFailed', messageId, toolCallId, error);
}
} else {
Logger.error(`[MCP] Server not found for tool call ${toolCallId}`);
}
} else {
Logger.error(`[MCP] Original tool call ${toolCallId} not found for message ${messageId}`);
}
*/
// For now, just log and return
return Promise.resolve()
}
private getSystemPath = memoize(async (): Promise<string> => {
return new Promise((resolve, reject) => {
let command: string

View File

@ -110,26 +110,12 @@ export class MemoryFileService {
// 如果文件不存在或读取失败,使用空对象
}
// 合并数据,注意数组的处理
const mergedData = { ...existingData }
// 处理每个属性
Object.entries(data).forEach(([key, value]) => {
// 如果是数组属性,需要特殊处理
if (Array.isArray(value) && Array.isArray(mergedData[key])) {
// 对于 shortMemories 和 memories直接使用传入的数组完全替换现有的记忆
if (key === 'shortMemories' || key === 'memories') {
mergedData[key] = value
log.info(`Replacing ${key} array with provided data`)
} else {
// 其他数组属性,使用新值
mergedData[key] = value
}
} else {
// 非数组属性,直接使用新值
mergedData[key] = value
}
})
// 合并接收到的部分数据 (data) 到现有数据 (existingData)
// 使用 Object.assign 或 spread operator 进行浅合并
// 对于嵌套对象或数组,如果需要深度合并,可能需要更复杂的逻辑,
// 但对于顶层设置(如提示词字符串),浅合并足够。
const mergedData = { ...existingData, ...data }
log.info('Merging partial data into existing memory data')
// 保存合并后的数据
await fs.writeFile(memoryDataPath, JSON.stringify(mergedData, null, 2))

View File

@ -1,5 +1,6 @@
import { ProxyConfig as _ProxyConfig, session } from 'electron'
import { socksDispatcher } from 'fetch-socks'
import { getSystemProxy } from 'os-proxy-config'
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
import { ProxyAgent, setGlobalDispatcher } from 'undici'
@ -70,15 +71,14 @@ export class ProxyManager {
private async setSystemProxy(): Promise<void> {
try {
await this.setSessionsProxy({ mode: 'system' })
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
const [protocol, address] = proxyString.split(';')[0].split(' ')
const url = protocol === 'PROXY' ? `http://${address}` : null
if (url && url !== this.config.url) {
this.config.url = url.toLowerCase()
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
const currentProxy = await getSystemProxy()
if (!currentProxy || currentProxy.proxyUrl === this.config.url) {
return
}
await this.setSessionsProxy({ mode: 'system' })
this.config.url = currentProxy.proxyUrl.toLowerCase()
this.setEnvironment(this.config.url)
this.proxyAgent = new GeneralProxyAgent()
} catch (error) {
console.error('Failed to set system proxy:', error)
throw error

View File

@ -0,0 +1,250 @@
import { BrowserWindow, dialog, IpcMainInvokeEvent } from 'electron'
import fs from 'fs/promises'
import { glob } from 'glob'
import path from 'path'
import { promisify } from 'util'
import { Logger } from '../utils/logger'
// 定义文件夹结构接口
export interface FileNode {
name: string
type: 'file'
extension: string
path: string
}
export interface DirectoryNode {
name: string
type: 'directory'
path: string
hasChildren: boolean
children: (FileNode | DirectoryNode)[]
}
export default class WorkspaceService {
/**
*
*/
public async selectWorkspaceFolder(event: IpcMainInvokeEvent) {
try {
const browserWindow = BrowserWindow.fromWebContents(event.sender)
if (!browserWindow) {
throw new Error('No browser window found')
}
const { canceled, filePaths } = await dialog.showOpenDialog(browserWindow, {
properties: ['openDirectory']
})
if (canceled || filePaths.length === 0) {
return null
}
return filePaths[0]
} catch (error) {
Logger.error('[WorkspaceService] Error selecting workspace folder:', error)
throw error
}
}
/**
*
*/
public async getWorkspaceFiles(
_: IpcMainInvokeEvent,
workspacePath: string,
options: {
extensions?: string[]
excludePatterns?: string[]
maxDepth?: number
maxFiles?: number
} = {}
) {
try {
const {
extensions = [],
excludePatterns = ['**/node_modules/**', '**/dist/**', '**/.git/**'],
maxDepth = 5,
maxFiles = 1000
} = options
// 检查路径是否存在
await fs.access(workspacePath)
// 构建glob模式
let pattern = '**/*'
if (extensions.length > 0) {
pattern = `**/*.{${extensions.join(',')}}`
}
// 使用glob查找文件
const files = (await promisify(glob)(pattern, {
cwd: workspacePath,
ignore: excludePatterns,
nodir: true,
dot: false,
maxDepth: maxDepth
})) as string[]
// 限制文件数量
const limitedFiles = files.slice(0, maxFiles)
// 获取文件信息
const fileInfoPromises = limitedFiles.map(async (file) => {
const fullPath = path.join(workspacePath, file)
const stats = await fs.stat(fullPath)
return {
path: file,
fullPath,
name: path.basename(file),
size: stats.size,
isDirectory: stats.isDirectory(),
extension: path.extname(file),
modifiedTime: stats.mtime.getTime()
}
})
return await Promise.all(fileInfoPromises)
} catch (error) {
Logger.error('[WorkspaceService] Error getting workspace files:', error)
throw error
}
}
/**
*
*/
public async readWorkspaceFile(_: IpcMainInvokeEvent, filePath: string) {
try {
// 检查文件是否存在
await fs.access(filePath)
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8')
return content
} catch (error) {
Logger.error('[WorkspaceService] Error reading workspace file:', error)
throw error
}
}
/**
*
*/
public async getWorkspaceFolderStructure(
_: IpcMainInvokeEvent,
workspacePath: string,
options: {
maxDepth?: number
excludePatterns?: string[]
directoryPath?: string // 新增参数,指定要加载的目录路径,相对于工作区路径
lazyLoad?: boolean // 新增参数,指定是否懒加载
} = {}
): Promise<DirectoryNode> {
try {
const {
maxDepth = 3,
excludePatterns = ['**/node_modules/**', '**/dist/**', '**/.git/**'],
directoryPath = '', // 默认为空,表示工作区根目录
lazyLoad = false // 默认为 false兼容原有行为
} = options
// 计算要加载的目录的完整路径
const targetPath = directoryPath ? path.join(workspacePath, directoryPath) : workspacePath
// 检查路径是否存在
await fs.access(targetPath)
// 递归获取文件夹结构
const getFolderStructure = async (currentPath: string, depth: number = 0): Promise<DirectoryNode> => {
if (depth > maxDepth) {
// 如果是懒加载模式且超过最大深度,标记该目录可以被展开
if (lazyLoad) {
return {
name: path.basename(currentPath),
type: 'directory',
path: path.relative(workspacePath, currentPath),
hasChildren: true, // 标记该目录有子项,但尚未加载
children: [] // 空数组表示未加载
}
}
return {
name: path.basename(currentPath),
type: 'directory',
path: path.relative(workspacePath, currentPath),
hasChildren: false,
children: []
}
}
const entries = await fs.readdir(currentPath, { withFileTypes: true })
const children: (FileNode | DirectoryNode)[] = []
// 检查目录是否为空
const hasChildren = entries.length > 0
for (const entry of entries) {
const entryPath = path.join(currentPath, entry.name)
// 检查是否应该排除
const relativePath = path.relative(workspacePath, entryPath)
const shouldExclude = excludePatterns.some((pattern) => {
const regex = new RegExp(pattern.replace(/\*/g, '.*'))
return regex.test(relativePath)
})
if (shouldExclude) {
continue
}
if (entry.isDirectory()) {
if (lazyLoad && depth > 0) {
// 在懒加载模式下,对于非根目录,只添加目录节点,不加载其子项
// 检查目录是否有子项
let dirHasChildren = false
try {
const subEntries = await fs.readdir(entryPath)
dirHasChildren = subEntries.length > 0
} catch (err) {
Logger.error(`[WorkspaceService] Error checking directory contents: ${entryPath}`, err)
}
children.push({
name: entry.name,
type: 'directory',
path: relativePath,
hasChildren: dirHasChildren, // 标记该目录是否有子项
children: [] // 空数组表示未加载
})
} else {
// 非懒加载模式或根目录,递归加载子目录
const subDir = await getFolderStructure(entryPath, depth + 1)
children.push(subDir)
}
} else {
children.push({
name: entry.name,
type: 'file',
extension: path.extname(entry.name),
path: relativePath
})
}
}
return {
name: path.basename(currentPath),
type: 'directory',
path: path.relative(workspacePath, currentPath),
hasChildren, // 添加该属性标记目录是否有子项
children
}
}
return await getFolderStructure(targetPath)
} catch (error) {
Logger.error('[WorkspaceService] Error getting workspace folder structure:', error)
throw error
}
}
}

View File

@ -0,0 +1,34 @@
import { ipcMain } from 'electron';
import { IpcChannel } from '@shared/IpcChannel';
import mcpService from './MCPService';
/**
* MCP相关的IPC处理程序
*/
export function registerMCPHandlers(): void {
// 注册MCP服务的IPC处理程序
ipcMain.handle(IpcChannel.Mcp_RemoveServer, mcpService.removeServer);
ipcMain.handle(IpcChannel.Mcp_RestartServer, mcpService.restartServer);
ipcMain.handle(IpcChannel.Mcp_StopServer, mcpService.stopServer);
ipcMain.handle(IpcChannel.Mcp_ListTools, mcpService.listTools);
ipcMain.handle(IpcChannel.Mcp_CallTool, mcpService.callTool);
ipcMain.handle(IpcChannel.Mcp_ListPrompts, mcpService.listPrompts);
ipcMain.handle(IpcChannel.Mcp_GetPrompt, mcpService.getPrompt);
ipcMain.handle(IpcChannel.Mcp_ListResources, mcpService.listResources);
ipcMain.handle(IpcChannel.Mcp_GetResource, mcpService.getResource);
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo);
ipcMain.handle(IpcChannel.Mcp_RerunTool, mcpService.rerunTool);
// 同时注册兼容旧版本的处理程序
ipcMain.handle('mcp:restart-server', mcpService.restartServer);
ipcMain.handle('mcp:remove-server', mcpService.removeServer);
ipcMain.handle('mcp:stop-server', mcpService.stopServer);
ipcMain.handle('mcp:list-tools', mcpService.listTools);
ipcMain.handle('mcp:call-tool', mcpService.callTool);
ipcMain.handle('mcp:list-prompts', mcpService.listPrompts);
ipcMain.handle('mcp:get-prompt', mcpService.getPrompt);
ipcMain.handle('mcp:list-resources', mcpService.listResources);
ipcMain.handle('mcp:get-resource', mcpService.getResource);
ipcMain.handle('mcp:get-install-info', mcpService.getInstallInfo);
ipcMain.handle('mcp:rerunTool', mcpService.rerunTool);
}

32
src/main/utils/logger.ts Normal file
View File

@ -0,0 +1,32 @@
/**
*
*/
export class Logger {
/**
*
*/
public static info(...args: any[]): void {
console.log('[INFO]', ...args)
}
/**
*
*/
public static warn(...args: any[]): void {
console.warn('[WARN]', ...args)
}
/**
*
*/
public static error(...args: any[]): void {
console.error('[ERROR]', ...args)
}
/**
*
*/
public static debug(...args: any[]): void {
console.debug('[DEBUG]', ...args)
}
}

View File

@ -56,7 +56,7 @@ declare global {
read: (fileId: string) => Promise<string>
clear: () => Promise<void>
get: (filePath: string) => Promise<FileType | null>
selectFolder: () => Promise<string | null>
selectFolder: () => Promise<{ canceled: boolean; filePaths: string[] }>
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
@ -73,7 +73,7 @@ declare global {
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
}
fs: {
read: (path: string) => Promise<string>
read: (path: string, encoding?: BufferEncoding) => Promise<string>
}
export: {
toWord: (markdown: string, fileName: string) => Promise<void>
@ -209,6 +209,12 @@ declare global {
loadLongTermData: () => Promise<any>
saveLongTermData: (data: any, forceOverwrite?: boolean) => Promise<boolean>
}
workspace: {
selectFolder: () => Promise<string | null>
getFiles: (workspacePath: string, options: any) => Promise<any[]>
readFile: (filePath: string) => Promise<string>
getFolderStructure: (workspacePath: string, options: any) => Promise<any>
}
asrServer: {
startServer: () => Promise<{ success: boolean; pid?: number; port?: number; error?: string }>
stopServer: (pid: number) => Promise<{ success: boolean; error?: string }>

View File

@ -1,7 +1,16 @@
import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { electronAPI } from '@electron-toolkit/preload'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
// Import MCPCallToolResponse along with other types
import {
FileType,
KnowledgeBaseParams,
KnowledgeItem,
MCPCallToolResponse,
MCPServer,
Shortcut,
WebDavConfig
} from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
import { CreateDirectoryOptions } from 'webdav'
@ -146,7 +155,45 @@ const api = {
listResources: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListResources, server),
getResource: ({ server, uri }: { server: MCPServer; uri: string }) =>
ipcRenderer.invoke(IpcChannel.Mcp_GetResource, { server, uri }),
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
// Modify rerunTool function to accept serverId instead of the full server object
rerunTool: (
messageId: string,
toolCallId: string,
server: MCPServer, // Changed from serverId: string to server: MCPServer
toolName: string,
args: Record<string, any>
) => ipcRenderer.invoke(IpcChannel.Mcp_RerunTool, messageId, toolCallId, server, toolName, args),
// Add listener for rerun updates from main process
onToolRerunUpdate: (
callback: (update: {
messageId: string
toolCallId: string
status: 'rerunning' | 'done' | 'error'
args?: Record<string, any> // Included when status is 'rerunning'
response?: MCPCallToolResponse // Included when status is 'done'
error?: string // Included when status is 'error'
}) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
update: {
messageId: string
toolCallId: string
status: 'rerunning' | 'done' | 'error'
args?: Record<string, any>
response?: MCPCallToolResponse
error?: string
}
) => {
callback(update)
}
ipcRenderer.on(IpcChannel.Mcp_ToolRerunUpdate, listener)
// Return a cleanup function to remove the listener
return () => {
ipcRenderer.off(IpcChannel.Mcp_ToolRerunUpdate, listener)
}
}
},
shell: {
openExternal: shell.openExternal
@ -197,6 +244,13 @@ const api = {
saveLongTermData: (data: any, forceOverwrite: boolean = false) =>
ipcRenderer.invoke(IpcChannel.LongTermMemory_SaveData, data, forceOverwrite)
},
workspace: {
selectFolder: () => ipcRenderer.invoke('workspace:selectFolder'),
getFiles: (workspacePath: string, options: any) => ipcRenderer.invoke('workspace:getFiles', workspacePath, options),
readFile: (filePath: string) => ipcRenderer.invoke('workspace:readFile', filePath),
getFolderStructure: (workspacePath: string, options: any) =>
ipcRenderer.invoke('workspace:getFolderStructure', workspacePath, options)
},
asrServer: {
startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer),
stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid)

View File

@ -9,6 +9,7 @@ import Sidebar from './components/app/Sidebar'
import DeepClaudeProvider from './components/DeepClaudeProvider'
import MemoryProvider from './components/MemoryProvider'
import PDFSettingsInitializer from './components/PDFSettingsInitializer'
import WorkspaceInitializer from './components/WorkspaceInitializer'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import StyleSheetManager from './context/StyleSheetManager'
@ -23,6 +24,7 @@ import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsPage from './pages/paintings/PaintingsPage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
import WorkspacePage from './pages/workspace'
function App(): React.ReactElement {
return (
@ -35,6 +37,7 @@ function App(): React.ReactElement {
<MemoryProvider>
<DeepClaudeProvider />
<PDFSettingsInitializer />
<WorkspaceInitializer />
<TopViewContainer>
<HashRouter>
<NavigationHandler />
@ -47,6 +50,7 @@ function App(): React.ReactElement {
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/workspace" element={<WorkspacePage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</HashRouter>

View File

@ -0,0 +1,152 @@
import { LeftOutlined } from '@ant-design/icons'
import WorkspaceExplorer from '@renderer/components/WorkspaceExplorer'
import WorkspaceFileViewer from '@renderer/components/WorkspaceFileViewer'
import WorkspaceSelector from '@renderer/components/WorkspaceSelector'
import { Button, Divider, Drawer, message } from 'antd'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const WorkspaceDrawerContent = styled.div`
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #fafbfc;
`
const SelectorWrapper = styled.div`
padding: 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
`
const ExplorerContainer = styled.div`
flex: 1;
min-height: 0;
overflow-y: auto;
background: #fafbfc;
`
const FileViewerHeader = styled.div`
display: flex;
align-items: center;
height: 56px;
padding: 0 16px;
border-bottom: 1px solid #f0f0f0;
background: #fff;
`
const FileViewerTitle = styled.span`
font-weight: 500;
font-size: 16px;
`
const BackButton = styled(Button)`
margin-right: 10px;
`
const StyledDivider = styled(Divider)`
margin: 0;
`
interface ChatWorkspacePanelProps {
visible: boolean
onClose: () => void
onSendToChat?: (content: string) => void
onSendFileToChat?: (file: any) => void
}
const ChatWorkspacePanel: React.FC<ChatWorkspacePanelProps> = ({
visible,
onClose,
onSendToChat,
onSendFileToChat
}) => {
const { t } = useTranslation()
const [selectedFile, setSelectedFile] = useState<{ path: string; content: string } | null>(null)
// 移除未使用的状态变量
const handleFileSelect = (filePath: string, content: string) => {
setSelectedFile({ path: filePath, content })
// 移除未使用的状态更新
}
const handleCloseViewer = () => {
setSelectedFile(null)
// 移除未使用的状态更新
}
const handleSendToChat = (content: string) => {
onSendToChat?.(content)
onClose()
setSelectedFile(null)
}
const handleSendFileToChat = (file: any) => {
onSendFileToChat?.(file)
onClose()
setSelectedFile(null)
// 移除未使用的状态更新
}
const handleContentChange = async (newContent: string, filePath: string) => {
try {
await window.api.file.write(filePath, newContent)
setSelectedFile((prev) => (prev ? { ...prev, content: newContent } : null))
// 移除未使用的状态更新
return true
} catch (error) {
console.error('保存文件失败:', error)
message.error(t('workspace.saveFileError'))
return false
}
}
return (
<Drawer
title={selectedFile ? t('workspace.fileViewer') : t('workspace.title')}
placement="right"
width="50vw"
onClose={() => {
onClose()
setSelectedFile(null)
}}
open={visible}
styles={{
header: { marginTop: '40px' },
body: { padding: 0, height: 'calc(100% - 95px)', overflow: 'hidden' }
}}
closable={false}
destroyOnClose>
{selectedFile ? (
<>
<FileViewerHeader>
<BackButton type="text" icon={<LeftOutlined />} onClick={handleCloseViewer} />
<FileViewerTitle>{t('workspace.fileViewer')}</FileViewerTitle>
</FileViewerHeader>
<WorkspaceFileViewer
filePath={selectedFile.path}
content={selectedFile.content}
onClose={handleCloseViewer}
onSendToChat={handleSendToChat}
onSendFileToChat={handleSendFileToChat}
onContentChange={handleContentChange}
/>
</>
) : (
<WorkspaceDrawerContent>
<SelectorWrapper>
<WorkspaceSelector />
</SelectorWrapper>
<StyledDivider />
<ExplorerContainer>
<WorkspaceExplorer onFileSelect={handleFileSelect} />
</ExplorerContainer>
</WorkspaceDrawerContent>
)}
</Drawer>
)
}
export default ChatWorkspacePanel

View File

@ -22,7 +22,7 @@ import {
setRecommendationThreshold,
setShortMemoryActive
} from '@renderer/store/memory'
import { FC, ReactNode, useEffect, useMemo, useRef } from 'react'
import { FC, ReactNode, useEffect, useRef } from 'react'
interface MemoryProviderProps {
children: ReactNode
@ -45,17 +45,12 @@ const MemoryProvider: FC<MemoryProviderProps> = ({ children }) => {
// 获取当前对话
const currentTopic = useAppSelector((state) => state.messages?.currentTopic?.id)
// 使用 useMemo 记忆化选择器的结果,避免返回新的数组引用
const messagesByTopic = useAppSelector((state) => state.messages?.messagesByTopic)
// 使用 useMemo 记忆化消息数组
const messages = useMemo(() => {
if (!currentTopic || !messagesByTopic) {
const messages = useAppSelector((state) => {
if (!currentTopic || !state.messages?.messagesByTopic) {
return []
}
return messagesByTopic[currentTopic] || []
}, [currentTopic, messagesByTopic])
return state.messages.messagesByTopic[currentTopic] || []
})
// 存储上一次的话题ID
const previousTopicRef = useRef<string | null>(null)

View File

@ -12,6 +12,7 @@ import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
@ -38,6 +39,7 @@ const MinappPopupContainer: React.FC = () => {
const { closeMinapp, hideMinappPopup } = useMinappPopup()
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const backgroundColor = useNavBackgroundColor()
/** control the drawer open or close */
const [isPopupShow, setIsPopupShow] = useState(true)
@ -236,7 +238,7 @@ const MinappPopupContainer: React.FC = () => {
}
return (
<TitleContainer style={{ justifyContent: 'space-between' }}>
<TitleContainer style={{ backgroundColor: backgroundColor, justifyContent: 'space-between' }}>
<Tooltip
title={
<TitleTextTooltip>

View File

@ -3,7 +3,6 @@ import { Box } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { addShortMemoryItem, analyzeAndAddShortMemories } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { deleteShortMemory } from '@renderer/store/memory'
import { Button, Card, Col, Empty, Input, List, message, Modal, Row, Statistic, Tooltip } from 'antd'
import _ from 'lodash'
@ -120,26 +119,12 @@ const PopupContainer: React.FC<Props> = ({ topicId, resolve }) => {
// 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作
const handleDeleteMemory = useCallback(
_.throttle(async (id: string) => {
// 先从当前状态中获取要删除的记忆之外的所有记忆
const state = store.getState().memory
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
// 执行删除操作
dispatch(deleteShortMemory(id))
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
// 使用主进程的 deleteShortMemoryById 方法删除记忆
try {
// 加载当前文件数据
const currentData = await window.api.memory.loadData()
// 替换 shortMemories 数组
const newData = {
...currentData,
shortMemories: filteredShortMemories
}
// 使用 true 参数强制覆盖文件
const result = await window.api.memory.saveData(newData, true)
const result = await window.api.memory.deleteShortMemoryById(id)
if (result) {
console.log(`[ShortMemoryPopup] Successfully deleted short memory with ID ${id}`)

View File

@ -0,0 +1,316 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { List, AutoSizer } from 'react-virtualized';
import { FileOutlined, FolderOutlined, ReloadOutlined } from '@ant-design/icons';
import { Empty, Spin, message } from 'antd';
import path from 'path-browserify';
import styled from 'styled-components';
import { RootState } from '@renderer/store';
import WorkspaceService from '@renderer/services/WorkspaceService';
import './index.css';
// 样式组件
const ExplorerContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
`;
const HeaderContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #f0f0f0;
`;
const HeaderTitle = styled.div`
font-weight: 500;
font-size: 16px;
`;
const ReloadButton = styled.div`
cursor: pointer;
&:hover {
color: #1890ff;
}
`;
const ListContainer = styled.div`
flex: 1;
overflow: hidden;
`;
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;
const EmptyContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
`;
const FileItem = styled.div`
display: flex;
align-items: center;
padding: 0 8px;
cursor: pointer;
user-select: none;
height: 100%;
&:hover {
background-color: #f5f5f5;
}
`;
const FolderItem = styled(FileItem)`
font-weight: 500;
`;
const IconWrapper = styled.span`
margin-right: 8px;
`;
const BreadcrumbContainer = styled.div`
padding: 8px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
overflow-x: auto;
white-space: nowrap;
`;
const BreadcrumbItem = styled.span`
cursor: pointer;
&:hover {
color: #1890ff;
}
`;
const BreadcrumbSeparator = styled.span`
margin: 0 8px;
color: #d9d9d9;
`;
// 文件项类型
interface FileItemType {
name: string;
path: string;
fullPath: string;
isDirectory: boolean;
extension?: string;
}
// 组件属性
interface SimpleVirtualizedExplorerProps {
onFileSelect?: (filePath: string, content: string) => void;
}
// 主组件
const SimpleVirtualizedExplorer: React.FC<SimpleVirtualizedExplorerProps> = ({ onFileSelect }) => {
const { t } = useTranslation();
const [files, setFiles] = useState<FileItemType[]>([]);
const [loading, setLoading] = useState(false);
const [currentPath, setCurrentPath] = useState('');
const [breadcrumbs, setBreadcrumbs] = useState<{ name: string; path: string }[]>([]);
// 从Redux获取当前工作区
const currentWorkspace = useSelector((state: RootState) => {
const { currentWorkspaceId, workspaces } = state.workspace;
return currentWorkspaceId ? workspaces.find((w) => w.id === currentWorkspaceId) || null : null;
});
// 加载当前目录的文件
const loadFiles = async (dirPath = '') => {
if (!currentWorkspace) return;
try {
setLoading(true);
// 获取文件夹结构
const folderStructure = await WorkspaceService.getWorkspaceFolderStructure(
currentWorkspace.path,
{
directoryPath: dirPath,
maxDepth: 1,
excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/build/**'],
lazyLoad: true
}
);
// 转换为文件列表
const fileList: FileItemType[] = [];
// 添加文件夹
if (folderStructure.children) {
// 先添加文件夹
const folders = folderStructure.children
.filter(item => item.type === 'directory')
.map(item => ({
name: item.name,
path: item.path,
fullPath: path.join(currentWorkspace.path, item.path).replace(/\\/g, '/'),
isDirectory: true
}));
// 再添加文件
const files = folderStructure.children
.filter(item => item.type === 'file')
.map(item => ({
name: item.name,
path: item.path,
fullPath: path.join(currentWorkspace.path, item.path).replace(/\\/g, '/'),
isDirectory: false,
extension: item.extension
}));
// 合并并排序
fileList.push(...folders.sort((a, b) => a.name.localeCompare(b.name)));
fileList.push(...files.sort((a, b) => a.name.localeCompare(b.name)));
}
setFiles(fileList);
setCurrentPath(dirPath);
// 更新面包屑
updateBreadcrumbs(dirPath);
} catch (error) {
console.error('Failed to load files:', error);
message.error(t('workspace.loadError'));
} finally {
setLoading(false);
}
};
// 更新面包屑
const updateBreadcrumbs = (dirPath: string) => {
const parts = dirPath.split('/').filter(Boolean);
const breadcrumbItems = [{ name: t('workspace.root'), path: '' }];
let currentPath = '';
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
breadcrumbItems.push({ name: part, path: currentPath });
}
setBreadcrumbs(breadcrumbItems);
};
// 处理文件点击
const handleFileClick = async (file: FileItemType) => {
if (file.isDirectory) {
// 如果是文件夹,进入该文件夹
loadFiles(file.path);
} else if (onFileSelect) {
// 如果是文件,读取文件内容
try {
const content = await WorkspaceService.readWorkspaceFile(file.fullPath);
onFileSelect(file.fullPath, content);
} catch (error) {
console.error('Failed to read file:', error);
message.error(t('workspace.readFileError'));
}
}
};
// 处理面包屑点击
const handleBreadcrumbClick = (path: string) => {
loadFiles(path);
};
// 初始加载
useEffect(() => {
if (currentWorkspace) {
loadFiles();
}
}, [currentWorkspace]);
// 渲染文件项
const renderRow = ({ index, key, style }: { index: number; key: string; style: React.CSSProperties }) => {
const file = files[index];
return (
<div key={key} style={style} onClick={() => handleFileClick(file)}>
{file.isDirectory ? (
<FolderItem>
<IconWrapper>
<FolderOutlined style={{ color: '#e8c341' }} />
</IconWrapper>
{file.name}
</FolderItem>
) : (
<FileItem>
<IconWrapper>
<FileOutlined style={{ color: '#8c8c8c' }} />
</IconWrapper>
{file.name}
</FileItem>
)}
</div>
);
};
if (!currentWorkspace) {
return (
<EmptyContainer>
<Empty description={t('workspace.selectWorkspace')} />
</EmptyContainer>
);
}
return (
<ExplorerContainer>
<HeaderContainer>
<HeaderTitle>{t('workspace.explorer')}</HeaderTitle>
<ReloadButton onClick={() => loadFiles(currentPath)}>
<ReloadOutlined />
</ReloadButton>
</HeaderContainer>
<BreadcrumbContainer>
{breadcrumbs.map((item, index) => (
<React.Fragment key={item.path}>
{index > 0 && <BreadcrumbSeparator>/</BreadcrumbSeparator>}
<BreadcrumbItem onClick={() => handleBreadcrumbClick(item.path)}>
{item.name}
</BreadcrumbItem>
</React.Fragment>
))}
</BreadcrumbContainer>
<ListContainer>
{loading ? (
<LoadingContainer>
<Spin size="large" />
</LoadingContainer>
) : files.length > 0 ? (
<AutoSizer>
{({ width, height }) => (
<List
width={width}
height={height}
rowCount={files.length}
rowHeight={30}
rowRenderer={renderRow}
/>
)}
</AutoSizer>
) : (
<EmptyContainer>
<Empty description={t('workspace.noFiles')} />
</EmptyContainer>
)}
</ListContainer>
</ExplorerContainer>
);
};
export default SimpleVirtualizedExplorer;

View File

@ -0,0 +1,92 @@
.workspace-explorer-container {
height: 100%;
overflow: hidden;
background-color: var(--color-bg-container);
border-right: 1px solid var(--color-border);
}
.workspace-explorer-empty,
.workspace-explorer-loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--color-text-secondary);
}
.workspace-explorer-node {
display: flex;
align-items: center;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
height: 30px;
line-height: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.workspace-explorer-node:hover {
background-color: var(--color-bg-elevated);
}
.workspace-explorer-node.selected {
background-color: var(--color-primary-bg);
}
.node-content {
display: flex;
align-items: center;
width: 100%;
}
.toggle-icon {
margin-right: 4px;
font-size: 10px;
width: 16px;
text-align: center;
cursor: pointer;
}
.folder-icon {
color: #e8c341;
margin-right: 6px;
}
.file-icon {
color: #8c8c8c;
margin-right: 6px;
}
.file-node-content {
display: flex;
align-items: center;
padding-left: 20px; /* 与文件夹图标对齐 */
width: 100%;
}
.node-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 搜索框样式 */
.workspace-search {
padding: 8px;
border-bottom: 1px solid var(--color-border);
}
/* 刷新按钮样式 */
.refresh-button {
margin-left: 8px;
cursor: pointer;
color: var(--color-text-secondary);
}
.refresh-button:hover {
color: var(--color-primary);
}

View File

@ -0,0 +1,370 @@
import { FileOutlined, FolderOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import WorkspaceService from '@renderer/services/WorkspaceService'
import { RootState } from '@renderer/store'
import { Empty, Input, message, Spin, Switch, Tree } from 'antd'
import path from 'path-browserify'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
import SimpleVirtualizedExplorer from './SimpleVirtualizedExplorer'
const { DirectoryTree } = Tree
const WorkspaceExplorerContainer = styled.div`
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
`
const SearchContainer = styled.div`
padding: 8px;
display: flex;
align-items: center;
`
const TreeContainer = styled.div`
flex: 1;
overflow: auto;
padding: 0 8px 8px 8px;
`
const EmptyContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
`
const HeaderContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #f0f0f0;
`
const HeaderTitle = styled.div`
font-weight: 500;
font-size: 16px;
`
const ReloadButton = styled.div`
cursor: pointer;
&:hover {
color: #1890ff;
}
`
const SwitchContainer = styled.div`
display: flex;
align-items: center;
`
const StyledSwitch = styled(Switch)`
margin-right: 8px;
`
const LoadingContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`
const LoadingText = styled.div`
padding: 20px;
text-align: center;
`
const IconWrapper = styled.span`
margin-right: 8px;
`
interface TreeNode {
title: string
key: string
isLeaf: boolean
children?: TreeNode[]
path?: string
fullPath?: string
hasChildren?: boolean // 标记该目录是否有子项
loaded?: boolean // 标记该目录的子项是否已加载
}
const WorkspaceExplorer: React.FC<{
onFileSelect?: (filePath: string, content: string) => void
}> = ({ onFileSelect }) => {
const { t } = useTranslation()
const [treeData, setTreeData] = useState<TreeNode[]>([])
const [expandedKeys, setExpandedKeys] = useState<string[]>([])
const [searchValue, setSearchValue] = useState('')
const [autoExpandParent, setAutoExpandParent] = useState(true)
const [loading, setLoading] = useState(false)
const [useVirtualized, setUseVirtualized] = useState(false)
// 从Redux获取当前工作区
const currentWorkspace = useSelector((state: RootState) => {
const { currentWorkspaceId, workspaces } = state.workspace
return currentWorkspaceId ? workspaces.find((w) => w.id === currentWorkspaceId) || null : null
})
// 转换为Tree组件所需的数据结构
const convertToTreeData = (node: any, parentKey = ''): TreeNode => {
const currentKey = parentKey ? `${parentKey}/${node.name}` : node.name
if (node.type === 'file') {
return {
title: node.name,
key: currentKey,
isLeaf: true,
path: node.path,
fullPath: path.join(currentWorkspace!.path, node.path).replace(/\\/g, '/')
}
}
return {
title: node.name,
key: currentKey,
isLeaf: false,
children:
node.children && node.children.length > 0
? node.children.map((child: any) => convertToTreeData(child, currentKey))
: [],
path: node.path || '',
fullPath: node.path ? path.join(currentWorkspace!.path, node.path).replace(/\\/g, '/') : currentWorkspace!.path,
hasChildren: node.hasChildren,
loaded: node.children && node.children.length > 0 // 如果有子项,则认为已加载
}
}
// 加载目录的子项
const loadDirectoryChildren = async (node: TreeNode) => {
if (!currentWorkspace || !node.path) return
try {
// 获取该目录的结构
const folderStructure = await WorkspaceService.getWorkspaceFolderStructure(currentWorkspace.path, {
directoryPath: node.path,
maxDepth: 1, // 只加载一层
excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/build/**'],
lazyLoad: true // 使用懒加载模式
})
// 更新节点的子项
const updateTreeData = (list: TreeNode[], key: string, children: TreeNode[]): TreeNode[] => {
return list.map((node) => {
if (node.key === key) {
return {
...node,
children,
loaded: true
}
}
if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children)
}
}
return node
})
}
// 转换子项为树节点
const childrenNodes = folderStructure.children.map((child: any) => convertToTreeData(child, node.key))
// 更新树数据
setTreeData((prevTreeData) => updateTreeData(prevTreeData, node.key, childrenNodes))
} catch (error) {
console.error(`Failed to load directory children for ${node.path}:`, error)
message.error(t('workspace.loadError'))
}
}
// 加载工作区文件结构
const loadWorkspaceFiles = async () => {
if (!currentWorkspace) return
try {
setLoading(true)
const folderStructure = await WorkspaceService.getWorkspaceFolderStructure(currentWorkspace.path, {
maxDepth: 1, // 只加载根目录的直接子项
excludePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/build/**'],
lazyLoad: true // 使用懒加载模式
})
const treeNodes = [convertToTreeData(folderStructure)]
setTreeData(treeNodes)
// 默认展开根节点
setExpandedKeys([treeNodes[0].key])
} catch (error) {
console.error('Failed to load workspace files:', error)
message.error(t('workspace.loadError'))
} finally {
setLoading(false)
}
}
// 当工作区变化时加载文件
useEffect(() => {
loadWorkspaceFiles()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentWorkspace])
// 处理搜索
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
setSearchValue(value)
if (!value) {
setExpandedKeys([])
setAutoExpandParent(false)
return
}
// 查找匹配的节点并展开其父节点
const expandedKeysSet = new Set<string>()
const searchTree = (nodes: TreeNode[], parentKey: string | null = null) => {
nodes.forEach((node) => {
if (node.title.toLowerCase().includes(value.toLowerCase())) {
if (parentKey) expandedKeysSet.add(parentKey)
}
if (node.children) {
searchTree(node.children, node.key)
}
})
}
searchTree(treeData)
setExpandedKeys(Array.from(expandedKeysSet))
setAutoExpandParent(true)
}
// 处理展开/折叠
const handleExpand = (expandedKeys: React.Key[], info: any) => {
setExpandedKeys(expandedKeys as string[])
setAutoExpandParent(false)
// 如果是展开操作,并且节点是目录,且还未加载子项
if (info.expanded && !info.node.isLeaf) {
const node = info.node as TreeNode
// 检查节点是否已加载子项
const hasLoadedChildren = node.loaded || (node.children && node.children.length > 0)
// 如果节点有子项但还未加载,或者标记为有子项但子项数组为空
if (!hasLoadedChildren && (node.hasChildren || (node.children && node.children.length === 0))) {
loadDirectoryChildren(node)
}
}
}
// 处理文件选择
const handleSelect = async (selectedKeys: React.Key[], info: any) => {
if (selectedKeys.length === 0 || !info.node.isLeaf) return
try {
const filePath = info.node.fullPath
const content = await WorkspaceService.readWorkspaceFile(filePath)
if (onFileSelect) {
onFileSelect(filePath, content)
}
} catch (error) {
console.error('Failed to read file:', error)
message.error(t('workspace.readFileError'))
}
}
// 过滤树节点
const filterTreeNode = (node: any) => {
if (!searchValue) return true
return node.title.toLowerCase().includes(searchValue.toLowerCase())
}
// 渲染树节点标题
const renderTitle = (nodeData: any) => {
const Icon = nodeData.isLeaf ? FileOutlined : FolderOutlined
return (
<span>
<IconWrapper>
<Icon />
</IconWrapper>
{nodeData.title}
</span>
)
}
return (
<WorkspaceExplorerContainer>
<HeaderContainer>
<HeaderTitle>{t('workspace.explorer')}</HeaderTitle>
<SwitchContainer>
<StyledSwitch
size="small"
checked={useVirtualized}
onChange={setUseVirtualized}
title={t('workspace.useVirtualized')}
/>
<ReloadButton onClick={loadWorkspaceFiles}>
<ReloadOutlined />
</ReloadButton>
</SwitchContainer>
</HeaderContainer>
<SearchContainer>
<Input
placeholder={t('workspace.search')}
prefix={<SearchOutlined />}
onChange={handleSearch}
value={searchValue}
allowClear
/>
</SearchContainer>
<TreeContainer>
{loading ? (
<LoadingContainer>
<Spin size="large">
<LoadingText>{t('common.loading')}</LoadingText>
</Spin>
</LoadingContainer>
) : currentWorkspace ? (
useVirtualized ? (
// 使用虚拟化树视图
<SimpleVirtualizedExplorer onFileSelect={onFileSelect} />
) : treeData.length > 0 ? (
// 使用传统树视图
<DirectoryTree
treeData={treeData}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={handleExpand}
onSelect={handleSelect}
filterTreeNode={filterTreeNode}
titleRender={renderTitle}
/>
) : (
<EmptyContainer>
<Empty description={t('workspace.noFiles')} />
</EmptyContainer>
)
) : (
<EmptyContainer>
<Empty description={t('workspace.selectWorkspace')} />
</EmptyContainer>
)}
</TreeContainer>
</WorkspaceExplorerContainer>
)
}
export default WorkspaceExplorer

View File

@ -0,0 +1,397 @@
import {
BulbOutlined,
CheckOutlined,
CloseOutlined,
CopyOutlined,
EditOutlined,
PaperClipOutlined,
SendOutlined
} from '@ant-design/icons'
import TranslateButton from '@renderer/components/TranslateButton'
import { FileType, FileTypes } from '@renderer/types' // 假设路径正确
import { Button, message, Space, Switch, Tabs, theme as antdTheme, Typography } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import type { GlobalToken } from 'antd/es/theme/interface' // 导入 GlobalToken 类型
import path from 'path-browserify'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SyntaxHighlighter from 'react-syntax-highlighter'
// 选择你喜欢的主题,确保它们与 RawScrollContainer 的背景/颜色有足够区分
import { docco, vs2015 } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import styled from 'styled-components'
import { v4 as uuidv4 } from 'uuid'
// 确认 useTheme hook 路径正确
import { useTheme } from '../../hooks/useTheme'
const { Title } = Typography
const { TabPane } = Tabs
// --- Styled Components Props Interfaces ---
interface StyledPropsWithToken {
token: GlobalToken
}
interface RawScrollContainerProps extends StyledPropsWithToken {
isDark: boolean
}
// --- Styled Components ---
const FileViewerContainer = styled.div<StyledPropsWithToken>`
height: 100%;
max-height: 85vh;
display: flex;
flex-direction: column;
background-color: ${(props) => props.token.colorBgContainer};
border: 1px solid ${(props) => props.token.colorBorderSecondary};
border-radius: ${(props) => props.token.borderRadiusLG}px;
overflow: hidden;
`
const FileHeader = styled.div<StyledPropsWithToken>`
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid ${(props) => props.token.colorBorderSecondary};
flex-shrink: 0;
`
const FileTitle = styled(Title)`
margin: 0 !important;
font-size: 16px !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 16px;
`
const FileContent = styled.div`
flex: 1;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
`
const FullHeightTabs = styled(Tabs)`
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
.ant-tabs-content-holder {
flex: 1;
overflow: hidden;
min-height: 0;
}
.ant-tabs-content {
height: 100%;
}
.ant-tabs-tabpane {
height: 100%;
display: flex;
flex-direction: column;
}
`
const ScrollContainerBase = styled.div`
flex: 1;
overflow: auto;
min-height: 100px;
`
const CodeScrollContainer = styled(ScrollContainerBase)`
padding: 0; /* 内边距由 SyntaxHighlighter 控制 */
position: relative;
`
// 关键:调整 RawScrollContainer 样式以明确区分
const RawScrollContainer = styled(ScrollContainerBase)<RawScrollContainerProps>`
padding: 16px;
white-space: pre-wrap;
word-break: break-all;
/* 尝试使用不同的背景色和字体来强制区分 */
background-color: ${(props) =>
props.isDark ? '#1e1e1e' : props.token.colorFillTertiary}; /* 暗色用#1e1e1e, 亮色用稍暗的填充色 */
color: ${(props) => (props.isDark ? '#d4d4d4' : props.token.colorText)}; /* 暗色用#d4d4d4, 亮色用标准文本色 */
/* 使用标准字体,而不是代码字体,提供视觉区别 */
font-family: ${(props) => props.token.fontFamily};
font-size: ${(props) => props.token.fontSize}px; /* 使用标准字体大小 */
line-height: ${(props) => props.token.lineHeight}; /* 使用标准行高 */
`
const ActionBar = styled.div<StyledPropsWithToken>`
padding: 10px 16px;
border-top: 1px solid ${(props) => props.token.colorBorderSecondary};
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
`
const EditorTextArea = styled(TextArea)<{ isDark: boolean }>`
height: 100% !important;
resize: none !important;
border: none;
outline: none;
padding: 16px;
font-family: ${(props) => props.theme.fontFamilyCode || 'monospace'};
font-size: 14px;
line-height: 1.5;
background-color: ${(props) => (props.isDark ? '#1e1e1e' : '#f5f5f5')};
color: ${(props) => (props.isDark ? '#d4d4d4' : '#333')};
`
// --- Component ---
interface FileViewerProps {
filePath: string
content: string
onClose: () => void
onSendToChat?: (content: string) => void
onSendFileToChat?: (file: FileType) => void
onContentChange?: (newContent: string, filePath: string) => void
}
const WorkspaceFileViewer: React.FC<FileViewerProps> = ({
filePath,
content,
onClose,
onSendToChat,
onSendFileToChat,
onContentChange
}) => {
const { t } = useTranslation()
const { theme: appTheme } = useTheme()
const { token } = antdTheme.useToken()
const extensionMap = useMemo<Record<string, string>>(
() => ({
js: 'javascript',
jsx: 'javascript',
ts: 'typescript',
tsx: 'typescript',
py: 'python',
java: 'java',
c: 'c',
cpp: 'cpp',
cs: 'csharp',
go: 'go',
rs: 'rust',
rb: 'ruby',
php: 'php',
html: 'html',
css: 'css',
scss: 'scss',
less: 'less',
json: 'json',
xml: 'xml',
yaml: 'yaml',
yml: 'yaml',
md: 'markdown',
sh: 'bash',
bat: 'batch',
ps1: 'powershell',
sql: 'sql'
}),
[]
)
const [language, setLanguage] = useState<string>('text')
const [useInternalLightTheme, setUseInternalLightTheme] = useState(appTheme !== 'dark')
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState(content)
const [isTranslating, setIsTranslating] = useState(false)
const textAreaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
const extension = path.extname(filePath).toLowerCase().substring(1)
setLanguage(extensionMap[extension] || 'text')
}, [filePath, extensionMap])
useEffect(() => {
setEditedContent(content)
}, [content])
useEffect(() => {
setUseInternalLightTheme(appTheme !== 'dark')
}, [appTheme])
const toggleSyntaxTheme = useCallback(() => setUseInternalLightTheme((prev) => !prev), [])
const handleCopy = useCallback(() => {
navigator.clipboard.writeText(content)
message.success(t('common.copied'))
}, [content, t])
const handleSendToChat = useCallback(() => {
if (onSendToChat) {
onSendToChat(content)
}
}, [content, onSendToChat])
const handleSendFileAsAttachment = useCallback(() => {
if (onSendFileToChat) {
const fileExt = path.extname(filePath)
const fileName = path.basename(filePath)
const fileType = (() => {
const extLower = fileExt.toLowerCase()
if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(extLower)) return FileTypes.IMAGE
if (['.txt', '.md', '.json', '.log'].includes(extLower)) return FileTypes.TEXT
return FileTypes.DOCUMENT
})()
const file: FileType = {
id: uuidv4(),
name: fileName,
origin_name: fileName,
path: filePath,
size: new Blob([content]).size,
ext: fileExt,
type: fileType,
created_at: new Date().toISOString(),
count: 1
}
onSendFileToChat(file)
message.success(t('workspace.fileSentAsAttachment'))
}
}, [filePath, content, onSendFileToChat, t])
const handleClose = useCallback(() => onClose(), [onClose])
const handleEdit = useCallback(() => {
setIsEditing(true)
}, [])
const handleSave = useCallback(() => {
if (onContentChange && editedContent !== content) {
onContentChange(editedContent, filePath)
message.success(t('common.saved'))
}
setIsEditing(false)
}, [editedContent, content, filePath, onContentChange, t])
const handleCancelEdit = useCallback(() => {
setEditedContent(content)
setIsEditing(false)
}, [content])
const handleTranslated = useCallback(
(translatedText: string) => {
if (isEditing) {
setEditedContent(translatedText)
} else if (onContentChange) {
onContentChange(translatedText, filePath)
}
setIsTranslating(false)
},
[isEditing, onContentChange, filePath]
)
const syntaxHighlighterStyle = useInternalLightTheme ? docco : vs2015
const isDarkThemeForRaw = !useInternalLightTheme
return (
<FileViewerContainer token={token}>
<FileHeader token={token}>
<FileTitle level={4} title={path.basename(filePath)}>
{path.basename(filePath)}
</FileTitle>
<Space>
<Switch
checkedChildren={<BulbOutlined />}
unCheckedChildren={<BulbOutlined />}
checked={useInternalLightTheme}
onChange={toggleSyntaxTheme}
title={t('common.toggleTheme')}
/>
<Button type="text" icon={<CloseOutlined />} onClick={handleClose} />
</Space>
</FileHeader>
<FileContent>
<FullHeightTabs defaultActiveKey="code">
{/* 代码 Tab: 使用 SyntaxHighlighter 或 TextArea */}
<TabPane tab={t('workspace.code')} key="code">
<CodeScrollContainer>
{isEditing ? (
<EditorTextArea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
isDark={!useInternalLightTheme}
ref={textAreaRef}
spellCheck={false}
/>
) : (
<SyntaxHighlighter
language={language}
style={syntaxHighlighterStyle} // 应用所选主题
showLineNumbers
wrapLines={true}
lineProps={{ style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' } }}
customStyle={{
margin: 0,
padding: '16px',
borderRadius: 0,
minHeight: '100%',
fontSize: token.fontSizeSM // 可以用小号字体
// 背景色由 style prop 的主题决定
}}
codeTagProps={{ style: { display: 'block', fontFamily: token.fontFamilyCode } }} // 明确使用代码字体
>
{content}
</SyntaxHighlighter>
)}
</CodeScrollContainer>
</TabPane>
{/* 原始内容 Tab: 只使用 RawScrollContainer 显示纯文本 */}
<TabPane tab={t('workspace.raw')} key="raw">
<RawScrollContainer isDark={isDarkThemeForRaw} token={token}>
{content} {/* 直接渲染文本内容,没有 SyntaxHighlighter */}
</RawScrollContainer>
</TabPane>
</FullHeightTabs>
</FileContent>
<ActionBar token={token}>
<Space>
{isEditing ? (
<>
<Button icon={<CheckOutlined />} type="primary" onClick={handleSave}>
{t('common.save')}
</Button>
<Button onClick={handleCancelEdit}>{t('common.cancel')}</Button>
</>
) : (
<Button icon={<EditOutlined />} onClick={handleEdit} disabled={!onContentChange}>
{t('common.edit')}
</Button>
)}
</Space>
<Space>
<Button icon={<CopyOutlined />} onClick={handleCopy}>
{t('common.copy')}
</Button>
<TranslateButton
text={isEditing ? editedContent : content}
onTranslated={handleTranslated}
isLoading={isTranslating}
style={{ borderRadius: '6px' }}
disabled={isEditing && !onContentChange}
/>
{onSendToChat && (
<Button type="primary" icon={<SendOutlined />} onClick={handleSendToChat}>
{t('workspace.sendToChat')}
</Button>
)}
{onSendFileToChat && (
<Button icon={<PaperClipOutlined />} onClick={handleSendFileAsAttachment}>
{t('workspace.sendAsAttachment')}
</Button>
)}
</Space>
</ActionBar>
</FileViewerContainer>
)
}
export default WorkspaceFileViewer

View File

@ -0,0 +1,18 @@
import React, { useEffect } from 'react'
import WorkspaceService from '@renderer/services/WorkspaceService'
/**
*
*
*/
const WorkspaceInitializer: React.FC = () => {
useEffect(() => {
// 初始化工作区
WorkspaceService.initWorkspaces()
console.log('[WorkspaceInitializer] 工作区初始化完成')
}, [])
return null
}
export default WorkspaceInitializer

View File

@ -0,0 +1,248 @@
import {
DeleteOutlined,
EditOutlined,
EyeInvisibleOutlined,
EyeOutlined,
FolderOpenOutlined,
PlusOutlined
} from '@ant-design/icons'
import WorkspaceService from '@renderer/services/WorkspaceService'
import { RootState } from '@renderer/store'
import type { MenuProps } from 'antd'
import { Button, Dropdown, Empty, Input, message, Modal, Space, Tooltip } from 'antd'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import styled from 'styled-components'
const WorkspaceSelectorContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 16px;
`
const WorkspaceLabel = styled.span`
margin-right: 8px;
font-weight: 500;
`
const WorkspacePath = styled.span`
color: #999;
font-size: 12px;
`
const WorkspaceSelector: React.FC = () => {
const { t } = useTranslation()
const [isModalVisible, setIsModalVisible] = useState(false)
const [isEditMode, setIsEditMode] = useState(false)
const [workspaceName, setWorkspaceName] = useState('')
const [editingWorkspace, setEditingWorkspace] = useState<string | null>(null)
// 从Redux获取工作区状态
const workspaces = useSelector((state: RootState) => state.workspace.workspaces)
const currentWorkspaceId = useSelector((state: RootState) => state.workspace.currentWorkspaceId)
const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) || null
// 初始化工作区
useEffect(() => {
WorkspaceService.initWorkspaces()
}, [])
// 选择工作区文件夹
const handleSelectFolder = async () => {
try {
console.log('开始选择工作区文件夹...')
const folderPath = await WorkspaceService.selectWorkspaceFolder()
console.log('选择的工作区文件夹路径:', folderPath)
if (folderPath) {
// 如果是编辑模式,更新现有工作区
if (isEditMode && editingWorkspace) {
console.log('更新工作区:', editingWorkspace, folderPath)
WorkspaceService.updateWorkspace(editingWorkspace, {
name: workspaceName,
path: folderPath,
updatedAt: Date.now()
})
message.success(t('workspace.updated'))
} else {
// 否则创建新工作区
console.log('创建新工作区:', workspaceName, folderPath)
const workspaceName2 = workspaceName || folderPath.split(/[\\/]/).pop() || 'Workspace'
console.log('工作区名称:', workspaceName2)
await WorkspaceService.createWorkspace(workspaceName2, folderPath)
message.success(t('workspace.created'))
}
setIsModalVisible(false)
setWorkspaceName('')
setIsEditMode(false)
setEditingWorkspace(null)
} else {
console.log('没有选择文件夹或选择被取消')
}
} catch (error) {
console.error('Failed to select workspace folder:', error)
message.error(t('workspace.selectFolderError'))
}
}
// 切换工作区
const handleSwitchWorkspace = (workspaceId: string) => {
WorkspaceService.setCurrentWorkspace(workspaceId)
}
// 编辑工作区
const handleEditWorkspace = (workspace: any) => {
setIsEditMode(true)
setEditingWorkspace(workspace.id)
setWorkspaceName(workspace.name)
setIsModalVisible(true)
}
// 删除工作区
const handleDeleteWorkspace = (workspaceId: string) => {
Modal.confirm({
title: t('workspace.confirmDelete'),
content: t('workspace.deleteWarning'),
okText: t('common.delete'),
okType: 'danger',
cancelText: t('common.cancel'),
onOk: () => {
WorkspaceService.deleteWorkspace(workspaceId)
message.success(t('workspace.deleted'))
}
})
}
// 切换工作区对AI的可见性
const handleToggleVisibility = (workspaceId: string, currentVisibility?: boolean) => {
// 切换可见性状态
const newVisibility = !(currentVisibility !== false) // 如果当前是undefined或true则设置为false否则设置为true
WorkspaceService.updateWorkspace(workspaceId, {
visibleToAI: newVisibility,
updatedAt: Date.now()
})
message.success(newVisibility ? t('workspace.visibilityEnabled') : t('workspace.visibilityDisabled'))
}
// 工作区下拉菜单项
const items: MenuProps['items'] = [
...(workspaces.length > 0
? workspaces.map((workspace) => ({
key: workspace.id,
onClick: () => handleSwitchWorkspace(workspace.id),
label: (
<Space>
<span>{workspace.name}</span>
<WorkspacePath>{workspace.path}</WorkspacePath>
<Tooltip title={workspace.visibleToAI !== false ? t('workspace.hideFromAI') : t('workspace.showToAI')}>
<Button
type="text"
size="small"
icon={workspace.visibleToAI !== false ? <EyeOutlined /> : <EyeInvisibleOutlined />}
onClick={(e) => {
e.stopPropagation()
handleToggleVisibility(workspace.id, workspace.visibleToAI)
}}
/>
</Tooltip>
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation()
handleEditWorkspace(workspace)
}}
/>
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation()
handleDeleteWorkspace(workspace.id)
}}
/>
</Space>
)
}))
: [
{
key: 'empty',
disabled: true,
label: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('workspace.noWorkspaces')} />
}
]),
{
type: 'divider'
},
{
key: 'add',
icon: <PlusOutlined />,
label: t('workspace.addNew'),
onClick: () => {
setIsEditMode(false)
setWorkspaceName('')
setEditingWorkspace(null)
setIsModalVisible(true)
}
}
]
return (
<>
<WorkspaceSelectorContainer>
<WorkspaceLabel>{t('workspace.current')}:</WorkspaceLabel>
<Dropdown menu={{ items }} trigger={['click']}>
<Button>
{currentWorkspace ? currentWorkspace.name : t('workspace.select')} <FolderOpenOutlined />
</Button>
</Dropdown>
<Tooltip title={t('workspace.addNew')}>
<Button
type="primary"
icon={<PlusOutlined />}
style={{ marginLeft: 8 }}
onClick={() => {
setIsEditMode(false)
setWorkspaceName('')
setEditingWorkspace(null)
setIsModalVisible(true)
}}
/>
</Tooltip>
</WorkspaceSelectorContainer>
<Modal
title={isEditMode ? t('workspace.edit') : t('workspace.add')}
open={isModalVisible}
onOk={handleSelectFolder}
onCancel={() => {
setIsModalVisible(false)
setWorkspaceName('')
setIsEditMode(false)
setEditingWorkspace(null)
}}
okText={t('common.confirm')}
cancelText={t('common.cancel')}>
<Space direction="vertical" style={{ width: '100%' }}>
<Input
placeholder={t('workspace.nameHint')}
value={workspaceName}
onChange={(e) => setWorkspaceName(e.target.value)}
/>
<Button icon={<FolderOpenOutlined />} onClick={handleSelectFolder}>
{t('workspace.selectFolder')}
</Button>
</Space>
</Modal>
</>
)
}
export default WorkspaceSelector

View File

@ -21,7 +21,8 @@ import {
Palette,
Settings,
Sparkle,
Sun
Sun,
FolderGit
} from 'lucide-react'
import { FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -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" />,
workspace: <FolderGit size={17} className="icon" />
}
const pathMap = {
@ -146,7 +148,8 @@ const MainMenus: FC = () => {
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
files: '/files',
workspace: '/workspace'
}
return sidebarIcons.visible.map((icon) => {

View File

@ -1,4 +1,5 @@
import { FileType, KnowledgeItem, QuickPhrase, Topic, TranslateHistory } from '@renderer/types'
import { Workspace } from '@renderer/store/workspace'
import { Dexie, type EntityTable } from 'dexie'
import { upgradeToV5 } from './upgrades'
@ -11,6 +12,7 @@ export const db = new Dexie('CherryStudio') as Dexie & {
knowledge_notes: EntityTable<KnowledgeItem, 'id'>
translate_history: EntityTable<TranslateHistory, 'id'>
quick_phrases: EntityTable<QuickPhrase, 'id'>
workspaces: EntityTable<Workspace, 'id'>
}
db.version(1).stores({
@ -57,4 +59,14 @@ db.version(6).stores({
quick_phrases: 'id'
})
db.version(7).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
topics: '&id, messages',
settings: '&id, value',
knowledge_notes: '&id, baseId, type, content, created_at, updated_at',
translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt',
quick_phrases: 'id',
workspaces: '&id, name, path, createdAt, updatedAt'
})
export default db

View File

@ -93,7 +93,7 @@ export function useAppInit() {
useEffect(() => {
// set files path
window.api.getAppInfo().then((info) => {
window.api.getAppInfo().then((info: { filesPath: string; resourcesPath: string }) => {
dispatch(setFilesPath(info.filesPath))
dispatch(setResourcesPath(info.resourcesPath))
})

View File

@ -1,20 +1,12 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useRuntime } from './useRuntime'
import { useSettings } from './useSettings'
function useNavBackgroundColor() {
const { windowStyle } = useSettings()
const { theme } = useTheme()
const { minappShow } = useRuntime()
const macTransparentWindow = isMac && windowStyle === 'transparent'
if (minappShow) {
return theme === 'dark' ? 'var(--navbar-background)' : 'var(--color-white)'
}
if (macTransparentWindow) {
return 'transparent'
}

View File

@ -0,0 +1,9 @@
import { useSelector } from 'react-redux'
import { RootState } from '@renderer/store'
export const useTheme = () => {
const theme = useSelector((state: RootState) => state.settings.theme)
const isDark = theme === 'dark'
return { theme, isDark }
}

View File

@ -493,6 +493,45 @@
"url_placeholder": "Enter URL, multiple URLs separated by Enter",
"urls": "URLs"
},
"workspace": {
"title": "Workspace",
"current": "Current Workspace",
"select": "Select Workspace",
"addNew": "Add Workspace",
"selectWorkspace": "Please select a workspace",
"selectFile": "Please select a file",
"selectFolder": "Select Folder",
"nameHint": "Workspace Name",
"add": "Add Workspace",
"edit": "Edit Workspace",
"updated": "Workspace updated",
"created": "Workspace created",
"deleted": "Workspace deleted",
"confirmDelete": "Confirm Delete Workspace",
"deleteWarning": "Deleting a workspace cannot be undone. Are you sure?",
"noWorkspaces": "No workspaces",
"noFiles": "No files",
"loadError": "Failed to load workspace files",
"selectFolderError": "Failed to select folder",
"readFileError": "Failed to read file",
"explorer": "File Explorer",
"hideFromAI": "Hide from AI",
"showToAI": "Show to AI",
"visibilityEnabled": "Workspace is now visible to AI",
"visibilityDisabled": "Workspace is now hidden from AI",
"useVirtualized": "Use Virtualized View",
"search": "Search files",
"code": "Code",
"raw": "Raw Content",
"fileViewer": "File Viewer",
"sendToChat": "Send to Chat",
"sendAsAttachment": "Send as Attachment",
"fileSentAsAttachment": "File sent as attachment",
"toggle": "Toggle Workspace Panel",
"workspaceInfo": "Workspace info added to system prompt",
"saveFileError": "Failed to save file",
"saveFileSuccess": "File saved successfully"
},
"languages": {
"arabic": "Arabic",
"chinese": "Chinese",
@ -1108,6 +1147,27 @@
"enable": "Enable Historical Dialog Context",
"enableTip": "When enabled, AI will automatically analyze and reference historical dialogs when needed, to provide more coherent answers",
"analyzeModelTip": "Select the model used for historical dialog context analysis, it's recommended to choose a model with faster response"
},
"promptSettings": {
"title": "Prompt Settings",
"description": "Customize prompts used by memory functions",
"longTermMemoryPrompt": "Long-term Memory Prompt",
"shortTermMemoryPrompt": "Short-term Memory Prompt",
"contextualMemoryPrompt": "Contextual Memory Prompt",
"assistantMemoryPrompt": "Assistant Memory Prompt",
"historicalContextPrompt": "Historical Context Prompt",
"resetToDefault": "Reset to Default",
"savePrompt": "Save Prompt",
"saveSuccess": "Prompt saved successfully",
"saveError": "Failed to save prompt",
"enterPrompt": "Enter prompt...",
"promptReset": "Prompt has been reset",
"promptSaved": "Prompt has been saved",
"longTermPromptDescription": "Long-term memory analysis prompt is used to extract user's long-term preferences, habits, and background information from conversations.",
"shortTermPromptDescription": "Short-term memory analysis prompt is used to extract important contextual information from the current conversation, helping AI understand the coherence of the dialogue.",
"contextualPromptDescription": "Contextual memory analysis prompt is used to extract key topics and information points from the current conversation to find relevant memories.",
"assistantPromptDescription": "Assistant memory analysis prompt is used to extract user preferences and needs related to specific assistants.",
"historicalPromptDescription": "Historical context analysis prompt is used to determine whether the current conversation needs to reference historical conversations to provide a more complete answer."
},
"title": "Memory Function",
"description": "Manage AI assistant's long-term memory, automatically analyze conversations and extract important information",
@ -1299,6 +1359,7 @@
"installHelp": "Get Installation Help",
"tabs": {
"general": "General",
"description": "Description",
"tools": "Tools",
"prompts": "Prompts",
"resources": "Resources"
@ -1327,6 +1388,18 @@
"blobInvisible": "Blob Invisible",
"text": "Text"
},
"workspaceFileTool": {
"title": "Workspace File Tool",
"description": "Provides functions to read, write, search, list, create and edit files in the workspace",
"readFile": "Read File",
"writeFile": "Write File",
"searchFiles": "Search Files",
"listFiles": "List Files",
"createFile": "Create File",
"editFile": "Edit File",
"success": "Operation successful",
"error": "Operation failed"
},
"deleteServer": "Delete Server",
"deleteServerConfirm": "Are you sure you want to delete this server?",
"registry": "Package Registry",

View File

@ -1121,6 +1121,7 @@
"installHelp": "インストールヘルプを取得",
"tabs": {
"general": "一般",
"description": "説明",
"tools": "ツール",
"prompts": "プロンプト",
"resources": "リソース"

View File

@ -1124,6 +1124,7 @@
"installHelp": "Получить помощь по установке",
"tabs": {
"general": "Общие",
"description": "Описание",
"tools": "Инструменты",
"prompts": "Подсказки",
"resources": "Ресурсы"

View File

@ -325,7 +325,7 @@
"duplicate": "复制",
"edit": "编辑",
"expand": "展开",
"collapse": "折叠",
"collapse": "收起",
"footnote": "引用内容",
"footnotes": "引用内容",
"fullscreen": "已进入全屏模式,按 F11 退出",
@ -339,6 +339,7 @@
"prompt": "提示词",
"provider": "提供商",
"regenerate": "重新生成",
"rerun": "重新运行",
"rename": "重命名",
"reset": "重置",
"save": "保存",
@ -494,6 +495,45 @@
"url_placeholder": "请输入网址, 多个网址用回车分隔",
"urls": "网址"
},
"workspace": {
"title": "工作区",
"current": "当前工作区",
"select": "选择工作区",
"addNew": "添加工作区",
"selectWorkspace": "请选择工作区",
"selectFile": "请选择文件",
"selectFolder": "选择文件夹",
"nameHint": "工作区名称",
"add": "添加工作区",
"edit": "编辑工作区",
"updated": "工作区已更新",
"created": "工作区已创建",
"deleted": "工作区已删除",
"confirmDelete": "确认删除工作区",
"deleteWarning": "删除工作区将无法恢复,确定要删除吗?",
"noWorkspaces": "暂无工作区",
"noFiles": "暂无文件",
"loadError": "加载工作区文件失败",
"selectFolderError": "选择文件夹失败",
"readFileError": "读取文件失败",
"explorer": "文件浏览器",
"search": "搜索文件",
"code": "代码",
"raw": "原始内容",
"fileViewer": "文件查看器",
"sendToChat": "发送到聊天",
"sendAsAttachment": "作为附件发送",
"fileSentAsAttachment": "文件已作为附件发送",
"toggle": "切换工作区面板",
"workspaceInfo": "工作区信息已添加到系统提示词",
"hideFromAI": "对AI隐藏此工作区",
"showToAI": "对AI显示此工作区",
"visibilityEnabled": "工作区已对AI可见",
"visibilityDisabled": "工作区已对AI隐藏",
"useVirtualized": "使用虚拟化",
"saveFileError": "保存文件失败",
"saveFileSuccess": "文件保存成功"
},
"languages": {
"arabic": "阿拉伯文",
"chinese": "简体中文",
@ -611,7 +651,10 @@
"tools": {
"completed": "已完成",
"invoking": "调用中",
"error": "发生错误"
"error": "发生错误",
"parameters": "参数",
"no_params": "无参数",
"results": "结果"
},
"topic.added": "话题添加成功",
"upgrade.success.button": "重启",
@ -1161,7 +1204,28 @@
"description": "允许AI在需要时自动引用历史对话以提供更连贯的回答。",
"enable": "启用历史对话上下文",
"enableTip": "启用后AI会在需要时自动分析并引用历史对话以提供更连贯的回答",
"analyzeModelTip": "选择用于历史对话上下文分析的模型,建议选择响应较快的模型"
"analyzeModelTip": "选择用于历史对话上下文分析的模型,建议选择响应较快的模型"
},
"promptSettings": {
"title": "提示词设置",
"description": "自定义记忆功能使用的提示词",
"longTermMemoryPrompt": "长期记忆提示词",
"shortTermMemoryPrompt": "短期记忆提示词",
"contextualMemoryPrompt": "上下文记忆提示词",
"assistantMemoryPrompt": "助手记忆提示词",
"historicalContextPrompt": "历史上下文提示词",
"resetToDefault": "重置为默认值",
"savePrompt": "保存提示词",
"saveSuccess": "提示词保存成功",
"saveError": "提示词保存失败",
"enterPrompt": "输入提示词...",
"promptReset": "提示词已重置",
"promptSaved": "提示词已保存",
"longTermPromptDescription": "长期记忆分析提示词用于从对话中提取用户的长期偏好、习惯和背景信息。",
"shortTermPromptDescription": "短期记忆分析提示词用于从当前对话中提取重要的上下文信息帮助AI理解对话的连贯性。",
"contextualPromptDescription": "上下文记忆分析提示词用于从当前对话中提取关键主题和信息点,以便找到相关的记忆。",
"assistantPromptDescription": "助手记忆分析提示词用于提取与特定助手相关的用户偏好和需求。",
"historicalPromptDescription": "历史上下文分析提示词用于判断当前对话是否需要引用历史对话来提供更完整的回答。"
},
"title": "记忆功能",
"description": "管理AI助手的长期记忆自动分析对话并提取重要信息",
@ -1432,6 +1496,7 @@
"installHelp": "获取安装帮助",
"tabs": {
"general": "通用",
"description": "描述",
"tools": "工具",
"prompts": "提示",
"resources": "资源"
@ -1460,6 +1525,18 @@
"blobInvisible": "隐藏二进制数据",
"text": "文本"
},
"workspaceFileTool": {
"title": "工作区文件工具",
"description": "提供读取、写入、搜索、列出、创建和编辑工作区文件的功能",
"readFile": "读取文件",
"writeFile": "写入文件",
"searchFiles": "搜索文件",
"listFiles": "列出文件",
"createFile": "创建文件",
"editFile": "编辑文件",
"success": "操作成功",
"error": "操作失败"
},
"deleteServer": "删除服务器",
"deleteServerConfirm": "确定要删除此服务器吗?",
"registry": "包管理源",

View File

@ -1121,6 +1121,7 @@
"installHelp": "獲取安裝幫助",
"tabs": {
"general": "通用",
"description": "描述",
"tools": "工具",
"prompts": "提示",
"resources": "資源"

View File

@ -0,0 +1,31 @@
{
"title": "Workspace",
"current": "Current Workspace",
"select": "Select Workspace",
"addNew": "Add Workspace",
"selectWorkspace": "Please select a workspace",
"selectFile": "Please select a file",
"selectFolder": "Select Folder",
"nameHint": "Workspace Name",
"add": "Add Workspace",
"edit": "Edit Workspace",
"updated": "Workspace updated",
"created": "Workspace created",
"deleted": "Workspace deleted",
"confirmDelete": "Confirm Delete Workspace",
"deleteWarning": "Deleting a workspace cannot be undone. Are you sure?",
"noWorkspaces": "No workspaces",
"noFiles": "No files",
"loadError": "Failed to load workspace files",
"selectFolderError": "Failed to select folder",
"readFileError": "Failed to read file",
"explorer": "File Explorer",
"search": "Search files",
"code": "Code",
"raw": "Raw Content",
"fileViewer": "File Viewer",
"sendToChat": "Send to Chat",
"sendAsAttachment": "Send as Attachment",
"fileSentAsAttachment": "File sent as attachment",
"toggle": "Toggle Workspace Panel"
}

View File

@ -0,0 +1,35 @@
{
"title": "工作区",
"current": "当前工作区",
"select": "选择工作区",
"addNew": "添加工作区",
"selectWorkspace": "请选择工作区",
"selectFile": "请选择文件",
"selectFolder": "选择文件夹",
"nameHint": "工作区名称",
"add": "添加工作区",
"edit": "编辑工作区",
"updated": "工作区已更新",
"created": "工作区已创建",
"deleted": "工作区已删除",
"confirmDelete": "确认删除工作区",
"deleteWarning": "删除工作区将无法恢复,确定要删除吗?",
"noWorkspaces": "暂无工作区",
"noFiles": "暂无文件",
"loadError": "加载工作区文件失败",
"selectFolderError": "选择文件夹失败",
"readFileError": "读取文件失败",
"explorer": "文件浏览器",
"search": "搜索文件",
"code": "代码",
"raw": "原始内容",
"fileViewer": "文件查看器",
"sendToChat": "发送到聊天",
"sendAsAttachment": "作为附件发送",
"fileSentAsAttachment": "文件已作为附件发送",
"toggle": "切换工作区面板",
"hideFromAI": "对AI隐藏此工作区",
"showToAI": "对AI显示此工作区",
"visibilityEnabled": "工作区已对AI可见",
"visibilityDisabled": "工作区已对AI隐藏"
}

View File

@ -1,10 +1,12 @@
import ChatWorkspacePanel from '@renderer/components/ChatWorkspacePanel'
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { Flex } from 'antd'
import { FC, memo, useMemo } from 'react'
import { FC, memo, useMemo, useState } from 'react' // Keep useState
import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar'
@ -24,6 +26,12 @@ const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle } = useSettings()
const { showTopics } = useShowTopics()
const [isWorkspacePanelVisible, setIsWorkspacePanelVisible] = useState(false) // Add state for panel visibility
// Function to toggle the workspace panel
const toggleWorkspacePanel = () => {
setIsWorkspacePanelVisible(!isWorkspacePanelVisible)
}
// 使用 useMemo 优化渲染,只有当相关依赖变化时才重新创建元素
const messagesComponent = useMemo(
@ -41,10 +49,15 @@ const Chat: FC<Props> = (props) => {
const inputbarComponent = useMemo(
() => (
<QuickPanelProvider>
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
<Inputbar
assistant={assistant}
setActiveTopic={props.setActiveTopic}
topic={props.activeTopic}
onToggleWorkspacePanel={toggleWorkspacePanel} // Pass toggle function to Inputbar
/>
</QuickPanelProvider>
),
[assistant, props.setActiveTopic, props.activeTopic]
[assistant, props.setActiveTopic, props.activeTopic, toggleWorkspacePanel] // Add toggleWorkspacePanel to dependencies
)
const tabsComponent = useMemo(() => {
@ -61,6 +74,25 @@ const Chat: FC<Props> = (props) => {
)
}, [topicPosition, showTopics, assistant, props.activeTopic, props.setActiveAssistant, props.setActiveTopic])
// 处理从工作区发送文件内容到聊天输入框
const handleSendFileToChat = (content: string) => {
// Emit an event to set the chat input value
EventEmitter.emit(EVENT_NAMES.SET_CHAT_INPUT, content)
// Optionally, close the workspace panel after sending
// toggleWorkspacePanel(); // Or use onClose() if passed down correctly for this purpose
setIsWorkspacePanelVisible(false) // Close the panel
console.log('Emitted SET_CHAT_INPUT event with content.')
}
// 处理从工作区发送文件作为附件
const handleSendFileAsAttachment = (file: any) => {
// 触发事件发送文件附件
EventEmitter.emit(EVENT_NAMES.SEND_FILE_ATTACHMENT, file)
// 关闭工作区面板
setIsWorkspacePanelVisible(false)
console.log('已发送文件作为附件:', file.name)
}
return (
<Container id="chat" className={messageStyle}>
<Main id="chat-main" vertical flex={1} justify="space-between">
@ -68,6 +100,12 @@ const Chat: FC<Props> = (props) => {
{inputbarComponent}
</Main>
{tabsComponent}
<ChatWorkspacePanel
visible={isWorkspacePanelVisible} // Pass state to visible prop
onClose={toggleWorkspacePanel} // Pass toggle function as onClose prop
onSendToChat={handleSendFileToChat}
onSendFileToChat={handleSendFileAsAttachment}
/>
</Container>
)
}

View File

@ -1,4 +1,4 @@
import { HolderOutlined } from '@ant-design/icons'
import { FolderOutlined, HolderOutlined } from '@ant-design/icons' // Add FolderOutlined
import ASRButton from '@renderer/components/ASRButton'
import { QuickPanelListItem, QuickPanelView, useQuickPanel } from '@renderer/components/QuickPanel'
import TranslateButton from '@renderer/components/TranslateButton'
@ -74,12 +74,14 @@ interface Props {
assistant: Assistant
setActiveTopic: (topic: Topic) => void
topic: Topic
onToggleWorkspacePanel?: () => void // Add prop for toggling workspace panel
}
let _text = ''
let _files: FileType[] = []
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic, onToggleWorkspacePanel }) => {
// Destructure the new prop
const [text, setText] = useState(_text)
// 用于存储语音识别的中间结果,不直接显示在输入框中
const [, setAsrCurrentText] = useState('')
@ -1137,6 +1139,51 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
})
}, [])
// Add useEffect to listen for SET_CHAT_INPUT event
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SET_CHAT_INPUT, (content: string) => {
setText(content) // Update the input text state
setTimeout(() => {
resizeTextArea() // Adjust textarea height
textareaRef.current?.focus() // Focus the textarea
}, 0)
})
// Cleanup function to unsubscribe when the component unmounts
return () => {
unsubscribe()
}
}, [resizeTextArea]) // Add resizeTextArea to dependency array
// Add useEffect to listen for SEND_FILE_ATTACHMENT event
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_FILE_ATTACHMENT, (file: FileType) => {
// Add file to files array
setFiles((prevFiles) => {
// Check if file already exists in array
const fileExists = prevFiles.some((f) => f.path === file.path)
if (fileExists) {
return prevFiles // File already exists, don't add it again
} else {
return [...prevFiles, file] // Add new file to array
}
})
// Focus the textarea
setTimeout(() => {
textareaRef.current?.focus()
}, 0)
// Show success message
window.message.success(t('workspace.fileSentAsAttachment'))
})
// Cleanup function to unsubscribe when the component unmounts
return () => {
unsubscribe()
}
}, [t]) // Add t to dependency array
useEffect(() => {
// if assistant knowledge bases are undefined return []
setSelectedKnowledgeBases(showKnowledgeIcon ? (assistant.knowledge_bases ?? []) : [])
@ -1292,6 +1339,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
)}
<Textarea
id="chat-input"
value={text}
onChange={onChange}
onKeyDown={handleKeyDown}
@ -1331,6 +1379,14 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
<MessageSquareDiff size={19} />
</ToolbarButton>
</Tooltip>
{/* Add Workspace Toggle Button here */}
{onToggleWorkspacePanel && (
<Tooltip placement="top" title={t('workspace.toggle')} arrow>
<ToolbarButton type="text" onClick={onToggleWorkspacePanel}>
<FolderOutlined />
</ToolbarButton>
</Tooltip>
)}
<AttachmentButton
ref={attachmentButtonRef}
model={model}

View File

@ -11,7 +11,7 @@ import { findCitationInChildren, sanitizeSchema } from '@renderer/utils/markdown
import { isEmpty } from 'lodash'
import { type FC, memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown, { type Components } from 'react-markdown'
import ReactMarkdown, { type Components } from 'react-markdown' // Keep Components type here
import rehypeKatex from 'rehype-katex'
// @ts-ignore next-line
import rehypeMathjax from 'rehype-mathjax'
@ -22,7 +22,7 @@ import remarkCjkFriendly from 'remark-cjk-friendly'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import InlineToolBlock from '../Messages/InlineToolBlock'
// Removed InlineToolBlock import
import EditableCodeBlock from './EditableCodeBlock'
import ImagePreview from './ImagePreview'
import Link from './Link'
@ -48,86 +48,13 @@ const Markdown: FC<Props> = ({ message }) => {
return [rehypeRaw, [rehypeSanitize, sanitizeSchema], mathEngine === 'KaTeX' ? rehypeKatex : rehypeMathjax]
}, [mathEngine])
// 处理工具调用 - 采用通用方法
const processToolUse = (content: string) => {
// 使用正则表达式匹配所有工具调用标签
const toolUseRegex = /<tool_use>([\s\S]*?)<\/tool_use>|<tool_use>([\s\S]*?)(?:<\/tool|$)/g
// 工具结果正则表达式
const toolResultRegex =
/<tool_use_result>[\s\S]*?<n>([\s\S]*?)<\/n>[\s\S]*?<r>([\s\S]*?)<\/r>[\s\S]*?<\/tool_use_result>/g
// 替换所有工具调用标签为自定义标记
let processedContent = content.replace(toolUseRegex, (_, content1, content2) => {
// 工具调用内容可能在content1或content2中
const toolContent = content1 || content2 || ''
// 尝试提取工具ID和参数
const lines = toolContent.trim().split('\n')
// 如果至少有两行则第一行可能是工具ID
if (lines.length >= 2) {
const toolId = lines[0].trim()
// 将剩余行作为参数
const argsText = lines.slice(1).join('\n').trim()
// 尝试解析参数为JSON
try {
// 尝试处理常见的JSON格式问题
let fixedArgsText = argsText
// 如果是非标准JSON格式尝试修复
if (fixedArgsText.startsWith('[') || fixedArgsText.startsWith('{')) {
// 将单引号替换为双引号
fixedArgsText = fixedArgsText.replace(/(['"])([^'"]*)\1/g, '"$2"')
// 将单引号键值对替换为双引号键值对
fixedArgsText = fixedArgsText.replace(/([\w]+):/g, '"$1":')
}
// 尝试解析JSON
let parsedArgs
try {
parsedArgs = JSON.parse(fixedArgsText)
} catch (e) {
// 如果解析失败,尝试添加缺失的右大括号
if (fixedArgsText.includes('{') && !fixedArgsText.endsWith('}')) {
fixedArgsText = fixedArgsText + '}'
try {
parsedArgs = JSON.parse(fixedArgsText)
} catch (e2) {
// 如果仍然失败,使用原始文本
parsedArgs = argsText
}
} else {
parsedArgs = argsText
}
}
// 返回工具调用标记
return `<div class="tool-use-marker" data-tool-name="${toolId}" data-tool-args='${typeof parsedArgs === 'object' ? JSON.stringify(parsedArgs) : parsedArgs}'></div>`
} catch (e) {
// 如果解析失败,使用原始文本
console.error('Failed to parse tool args:', e)
return `<div class="tool-use-marker" data-tool-name="${toolId}" data-tool-args='${argsText}'></div>`
}
} else {
// 如果只有一行,则将整个内容作为工具调用
return `<div class="tool-use-marker" data-tool-name="unknown" data-tool-args='${toolContent}'></div>`
}
})
// 替换工具结果标签为自定义标记
processedContent = processedContent.replace(toolResultRegex, (_, toolName, result) => {
return `<div class="tool-result-marker" data-tool-name="${toolName.trim()}" data-result='${result.trim()}'></div>`
})
return processedContent
}
// Remove processToolUse function as it's based on XML tags in content,
// which won't exist with native function calling.
// const processToolUse = (content: string) => { ... }
// 处理后的消息内容
const processedMessageContent = useMemo(() => {
return processToolUse(messageContent)
}, [messageContent])
// Use the original message content directly, without XML processing.
const processedMessageContent = messageContent
const components = useMemo(() => {
const baseComponents = {
@ -155,91 +82,11 @@ const Markdown: FC<Props> = ({ message }) => {
{props.children}
</span>
)
},
// 处理工具调用标记
div: (props: any) => {
if (props.className === 'tool-use-marker') {
const toolName = props['data-tool-name']
let toolArgs
try {
toolArgs = JSON.parse(props['data-tool-args'])
} catch (e) {
toolArgs = props['data-tool-args']
}
// 如果消息中包含工具调用结果,则显示实际结果
const mcpTools = message?.metadata?.mcpTools || []
// 调试信息
console.log('Tool name:', toolName)
console.log('Message metadata:', message?.metadata)
console.log('MCP Tools:', mcpTools)
// 尝试多种方式匹配工具
let toolResponse = mcpTools.find((tool) => tool.id === toolName)
// 如果没有找到,尝试使用工具名称匹配
if (!toolResponse && mcpTools.length > 0) {
toolResponse = mcpTools[mcpTools.length - 1] // 使用最后一个工具调用
}
// 创建默认响应
const defaultResponse = {
isError: false,
content: [
{
type: 'text',
text: '工具调用已完成'
}
]
}
if (toolResponse) {
console.log('Found tool response:', toolResponse)
return (
<InlineToolBlock
toolName={toolName}
toolArgs={toolArgs}
status="done"
response={toolResponse.response || defaultResponse}
/>
)
} else {
// 如果没有找到工具调用结果,则显示完成状态和默认响应
return (
<InlineToolBlock
toolName={toolName}
toolArgs={toolArgs}
status="done"
response={{ isError: false, content: [{ type: 'text' as const, text: '工具调用已完成' }] }}
/>
)
}
} else if (props.className === 'tool-result-marker') {
const toolName = props['data-tool-name']
const result = props['data-result']
return (
<InlineToolBlock
toolName={toolName}
toolArgs={null}
status="done"
response={{
isError: false,
content: [
{
type: 'text',
text: result
}
]
}}
/>
)
}
return <div {...props} />
}
} as Partial<Components>
// Removed custom div renderer for tool markers
} as Partial<Components> // Keep Components type here
return baseComponents
}, [message?.metadata])
}, []) // Removed message.metadata dependency as it's no longer used here
if (message.role === 'user' && !renderInputMessageAsMarkdown) {
return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>

View File

@ -28,56 +28,17 @@ const MessageAttachments: FC<Props> = ({ message }) => {
await navigator.clipboard.write([item])
}, [])
if (!message.files) {
if (!message.files || message.files.length === 0) {
return null
}
if (message?.files && message.files[0]?.type === FileTypes.IMAGE) {
return (
<Container style={{ marginBottom: 8 }}>
{message.files?.map((image) => {
// 使用 useCallback 记忆化工具栏渲染函数,避免不必要的重新创建
const memoizedToolbarRender = useCallback(
(
_,
{
transform: { scale },
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
}
) => (
<ToobarWrapper size={12} className="toolbar-wrapper">
<SwapOutlined rotate={90} onClick={onFlipY} />
<SwapOutlined onClick={onFlipX} />
<RotateLeftOutlined onClick={onRotateLeft} />
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
<CopyOutlined onClick={() => handleCopyImage(image)} />
<DownloadOutlined onClick={() => download(FileManager.getFileUrl(image))} />
</ToobarWrapper>
),
[image, handleCopyImage] // 依赖于当前循环的 image 对象和 handleCopyImage 函数
)
// 将文件按类型分组
const imageFiles = message.files.filter(file => file.type === FileTypes.IMAGE)
const nonImageFiles = message.files.filter(file => file.type !== FileTypes.IMAGE)
return (
<Image
src={FileManager.getFileUrl(image)}
key={image.id}
width="33%"
preview={{
toolbarRender: memoizedToolbarRender // 使用记忆化的函数
}}
/>
)
})}
</Container>
)
}
// 使用 useMemo 记忆化文件列表,避免不必要的重新计算
// 使用 useMemo 记忆化非图片文件列表,避免不必要的重新计算
const memoizedFileList = useMemo(() => {
return message.files?.map((file) => {
return nonImageFiles.map((file) => {
// 使用 FileManager.getFileUrl 来获取文件URL它会处理路径问题
const fileUrl = FileManager.getFileUrl(file)
console.log('消息附件URL:', fileUrl)
@ -89,22 +50,80 @@ const MessageAttachments: FC<Props> = ({ message }) => {
name: FileManager.formatFileName(file)
}
})
}, [message.files])
}, [nonImageFiles])
return (
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
<Upload listType="text" disabled fileList={memoizedFileList} />
<Container style={{ marginBottom: 8 }}>
{/* 渲染图片文件 */}
{imageFiles.length > 0 && (
<ImageContainer>
{imageFiles.map((image) => {
// 使用 useCallback 记忆化工具栏渲染函数,避免不必要的重新创建
const memoizedToolbarRender = useCallback(
(
_,
{
transform: { scale },
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
}
) => (
<ToobarWrapper size={12} className="toolbar-wrapper">
<SwapOutlined rotate={90} onClick={onFlipY} />
<SwapOutlined onClick={onFlipX} />
<RotateLeftOutlined onClick={onRotateLeft} />
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
<CopyOutlined onClick={() => handleCopyImage(image)} />
<DownloadOutlined onClick={() => download(FileManager.getFileUrl(image))} />
</ToobarWrapper>
),
[image, handleCopyImage] // 依赖于当前循环的 image 对象和 handleCopyImage 函数
)
return (
<Image
src={FileManager.getFileUrl(image)}
key={image.id}
width="33%"
preview={{
toolbarRender: memoizedToolbarRender // 使用记忆化的函数
}}
/>
)
})}
</ImageContainer>
)}
{/* 渲染非图片文件 */}
{nonImageFiles.length > 0 && (
<FileContainer className="message-attachments">
<Upload listType="text" disabled fileList={memoizedFileList} />
</FileContainer>
)}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: row;
flex-direction: column;
gap: 10px;
margin-top: 8px;
`
const ImageContainer = styled.div`
display: flex;
flex-direction: row;
gap: 10px;
flex-wrap: wrap;
`
const FileContainer = styled.div`
margin-top: 2px;
`
const Image = styled(AntdImage)`
border-radius: 10px;
`

View File

@ -20,6 +20,7 @@ import MessageAttachments from './MessageAttachments'
import MessageError from './MessageError'
import MessageImage from './MessageImage'
import MessageThought from './MessageThought'
import { default as MessageTools } from './MessageTools' // Change to named import (using default alias)
interface Props {
message: Message
@ -330,6 +331,8 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
<div className="message-content-tools">
{/* Only display thought info at the top */}
<MessageThought message={message} />
{/* Render MessageTools to display tool blocks based on metadata */}
<MessageTools message={message} />
</div>
{isSegmentedPlayback ? (
// Apply regex replacement here for TTS

View File

@ -1,13 +1,23 @@
import { CheckOutlined, ExpandOutlined, LoadingOutlined, WarningOutlined } from '@ant-design/icons'
import {
CheckOutlined,
CopyOutlined,
EditOutlined,
ExpandAltOutlined,
LoadingOutlined,
ReloadOutlined,
WarningOutlined
} from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types'
import { message as antdMessage, Modal, Tooltip } from 'antd'
import { FC, memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useAppDispatch } from '@renderer/store'
import { updateMessageThunk } from '@renderer/store/messages'
import { MCPToolResponse, Message } from '@renderer/types'
import { message as antdMessage, Tooltip } from 'antd' // Removed Modal
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' // Removed useLayoutEffect, useRef
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import CustomCollapse from './CustomCollapse'
import ExpandedResponseContent from './ExpandedResponseContent'
// Removed ExpandedResponseContent import
import ToolResponseContent from './ToolResponseContent'
interface Props {
@ -17,16 +27,32 @@ interface Props {
const MessageTools: FC<Props> = ({ message }) => {
const [activeKeys, setActiveKeys] = useState<string[]>([])
const [copiedMap, setCopiedMap] = useState<Record<string, boolean>>({})
const [expandedResponse, setExpandedResponse] = useState<{ content: string; title: string } | null>(null)
// Removed expandedResponse state
const [editingToolId, setEditingToolId] = useState<string | null>(null)
const [editedParams, setEditedParams] = useState<string>('')
const { t } = useTranslation()
const { messageFont, fontSize } = useSettings()
const { messageFont } = useSettings() // Removed fontSize
const dispatch = useAppDispatch()
// Local state for immediate UI updates, synced with message metadata
const [localToolResponses, setLocalToolResponses] = useState<MCPToolResponse[]>(message.metadata?.mcpTools || [])
// Effect to sync local state when message metadata changes externally
useEffect(() => {
// Only update local state if the incoming metadata is actually different
// This prevents unnecessary re-renders if the message object reference changes but content doesn't
const incomingTools = message.metadata?.mcpTools || []
if (JSON.stringify(incomingTools) !== JSON.stringify(localToolResponses)) {
setLocalToolResponses(incomingTools)
}
}, [message.metadata?.mcpTools]) // Removed localToolResponses from dependency array
const fontFamily = useMemo(() => {
return messageFont === 'serif'
? 'serif'
: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans","Helvetica Neue", sans-serif'
}, [messageFont])
// 使用 useCallback 记忆化 copyContent 函数,避免不必要的重新创建
const copyContent = useCallback(
(content: string, toolId: string) => {
navigator.clipboard.writeText(content)
@ -37,57 +63,201 @@ const MessageTools: FC<Props> = ({ message }) => {
[t]
)
// 使用 activeKeys 状态管理折叠面板的展开/折叠状态
// --- Handlers for Edit/Rerun ---
const handleRerun = useCallback(
(toolCall: MCPToolResponse, currentParamsString: string) => {
console.log('Rerunning tool:', toolCall.id, 'with params:', currentParamsString)
try {
const paramsToRun = JSON.parse(currentParamsString)
const toolResponses = message.metadata?.mcpTools || []
// Proactively update local state for immediate UI feedback
setLocalToolResponses((prevResponses) =>
prevResponses.map((tc) =>
tc.id === toolCall.id ? { ...tc, args: paramsToRun, status: 'invoking', response: undefined } : tc
)
)
// 预处理响应数据,避免在展开时计算
const responseStringsRef = useRef<Record<string, string>>({})
// 使用 useLayoutEffect 在渲染前预处理数据
useLayoutEffect(() => {
const strings: Record<string, string> = {}
let hasChanges = false
for (const toolResponse of toolResponses) {
if (toolResponse.status === 'done' && toolResponse.response) {
// 如果该响应已经处理过,则跳过
if (responseStringsRef.current[toolResponse.id]) {
strings[toolResponse.id] = responseStringsRef.current[toolResponse.id]
continue
const serverConfig = message.enabledMCPs?.find((server) => server.id === toolCall.tool.serverId)
if (!serverConfig) {
console.error(`[MessageTools] Server config not found for ID ${toolCall.tool.serverId}`)
antdMessage.error({ content: t('common.rerun_failed_server_not_found'), key: 'rerun-tool' })
return
}
try {
strings[toolResponse.id] = JSON.stringify(toolResponse.response, null, 2)
hasChanges = true
} catch (error) {
console.error('Error stringifying response:', error)
strings[toolResponse.id] = String(toolResponse.response)
hasChanges = true
window.api.mcp
.rerunTool(message.id, toolCall.id, serverConfig, toolCall.tool.name, paramsToRun)
.then(() => antdMessage.success({ content: t('common.rerun_started'), key: 'rerun-tool' }))
.catch((err) => {
console.error('Rerun failed:', err)
antdMessage.error({ content: t('common.rerun_failed'), key: 'rerun-tool' })
// Optionally revert local state on failure
setLocalToolResponses(
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
)
})
} catch (e) {
console.error('Invalid JSON parameters for rerun:', e)
antdMessage.error(t('common.invalid_json'))
// Revert local state if JSON parsing fails
setLocalToolResponses(
(prevResponses) => prevResponses.map((tc) => (tc.id === toolCall.id ? { ...tc, status: 'done' } : tc)) // Revert status
)
}
},
[message.id, message.enabledMCPs, t, dispatch] // Added dispatch
)
const handleEdit = useCallback((toolCall: MCPToolResponse) => {
setEditingToolId(toolCall.id)
setEditedParams(JSON.stringify(toolCall.args || {}, null, 2))
}, [])
const handleCancelEdit = useCallback(() => {
setEditingToolId(null)
setEditedParams('')
}, [])
const handleSaveEdit = useCallback(
(toolCall: MCPToolResponse) => {
handleRerun(toolCall, editedParams)
setEditingToolId(null)
setEditedParams('')
},
[editedParams, handleRerun]
)
const handleParamsChange = useCallback((newParams: string) => {
setEditedParams(newParams)
}, [])
// --- End Handlers ---
// --- Listener for Rerun Updates & Persistence ---
useEffect(() => {
const cleanupListener = window.api.mcp.onToolRerunUpdate((update) => {
if (update.messageId !== message.id) return // Ignore updates for other messages
console.log('[MessageTools] Received rerun update:', update)
// --- Update Local State for Immediate UI Feedback ---
setLocalToolResponses((currentLocalResponses) => {
return currentLocalResponses.map((toolCall) => {
if (toolCall.id === update.toolCallId) {
let updatedCall: MCPToolResponse
switch (update.status) {
case 'rerunning':
// Note: 'rerunning' status from IPC translates to 'invoking' in UI
updatedCall = { ...toolCall, status: 'invoking', args: update.args, response: undefined }
break
case 'done':
updatedCall = {
...toolCall,
status: 'done',
response: update.response,
// Persist the args used for the successful rerun
args: update.args !== undefined ? update.args : toolCall.args
}
break
case 'error':
updatedCall = {
...toolCall,
status: 'done', // Keep UI status as 'done' even on error
response: { content: [{ type: 'text', text: update.error }], isError: true },
// Persist the args used for the failed rerun
args: update.args !== undefined ? update.args : toolCall.args
}
break
default:
updatedCall = toolCall // Should not happen
}
return updatedCall
}
return toolCall
})
})
// --- End Local State Update ---
// --- Persist Changes to Global Store and DB (only on final states) ---
if (update.status === 'done' || update.status === 'error') {
// IMPORTANT: Use the message prop directly to get the state *before* this update cycle
const previousMcpTools = message.metadata?.mcpTools || []
console.log(
'[MessageTools Persistence] Previous MCP Tools from message.metadata:',
JSON.stringify(previousMcpTools, null, 2)
) // Log previous state
const updatedMcpToolsForPersistence = previousMcpTools.map((toolCall) => {
if (toolCall.id === update.toolCallId) {
console.log(
`[MessageTools Persistence] Updating tool ${toolCall.id} with status ${update.status}, args:`,
update.args,
'response:',
update.response || update.error
) // Log update details
// Apply the final state directly from the update object
return {
...toolCall, // Keep existing id, tool info
status: 'done', // Final status is always 'done' for persistence
args: update.args !== undefined ? update.args : toolCall.args, // Persist the args used for the rerun
response:
update.status === 'error'
? { content: [{ type: 'text', text: update.error }], isError: true } // Create error response object
: update.response // Use the successful response
}
}
return toolCall // Keep other tool calls as they were
})
console.log(
'[MessageTools Persistence] Calculated MCP Tools for Persistence:',
JSON.stringify(updatedMcpToolsForPersistence, null, 2)
) // Log calculated state
// Dispatch the thunk to update the message globally
// Ensure we have the necessary IDs
if (message.topicId && message.id) {
console.log(
`[MessageTools Persistence] Dispatching updateMessageThunk for message ${message.id} in topic ${message.topicId}`
) // Log dispatch attempt
dispatch(
updateMessageThunk(message.topicId, message.id, {
metadata: {
...message.metadata, // Keep other metadata
mcpTools: updatedMcpToolsForPersistence // Provide the correctly calculated final array
}
})
)
console.log(
'[MessageTools] Dispatched updateMessageThunk with calculated persistence data for tool:',
update.toolCallId
)
} else {
console.error('[MessageTools] Missing topicId or messageId, cannot dispatch update.')
}
}
}
// --- End Persistence Logic ---
})
if (hasChanges) {
responseStringsRef.current = { ...responseStringsRef.current, ...strings }
}
}, [toolResponses])
return () => cleanupListener()
// Ensure all necessary dependencies are included
}, [message.id, message.topicId, message.metadata, dispatch]) // message.metadata is crucial here
// --- End Listener ---
// 使用 useMemo 记忆化 getCollapseItems 函数返回的 items 数组,避免不必要的重新计算
// Use localToolResponses for rendering
const toolResponses = localToolResponses
// Removed responseStringsRef and its useLayoutEffect
// Memoize collapse items
const collapseItems = useMemo(() => {
const items: { key: string; label: React.ReactNode; children: React.ReactNode }[] = []
// Add tool responses
for (const toolResponse of toolResponses) {
const { id, tool, status, response } = toolResponse
return toolResponses.map((toolResponse) => {
const { id, tool, args, status, response } = toolResponse
const isInvoking = status === 'invoking'
const isDone = status === 'done'
const hasError = isDone && response?.isError === true
const result = {
params: tool.inputSchema,
response: toolResponse.response
}
const params = args || {}
const toolResult = response
items.push({
return {
key: id,
label: (
<MessageTitleLabel>
@ -107,33 +277,50 @@ const MessageTools: FC<Props> = ({ message }) => {
<ActionButtonsContainer>
{isDone && response && (
<>
<Tooltip title={t('common.expand')} mouseEnterDelay={0.5}>
<Tooltip
title={activeKeys.includes(id) ? t('common.collapse') : t('common.expand')}
mouseEnterDelay={0.5}>
<ActionButton
className="message-action-button"
onClick={(e) => {
e.stopPropagation()
// 使用预处理的响应数据
setExpandedResponse({
content: responseStringsRef.current[id] || '',
title: tool.name
})
}}
aria-label={t('common.expand')}>
<ExpandOutlined />
// Toggle the active key for this item
setActiveKeys((prev) => (prev.includes(id) ? prev.filter((k) => k !== id) : [...prev, id]))
}}>
<ExpandAltOutlined />
{activeKeys.includes(id) ? t('common.collapse') : t('common.expand')}
</ActionButton>
</Tooltip>
<Tooltip title={t('common.rerun')} mouseEnterDelay={0.5}>
<ActionButton
onClick={(e) => {
e.stopPropagation()
const paramsToRun = editingToolId === id ? editedParams : JSON.stringify(args || {}, null, 2)
handleRerun(toolResponse, paramsToRun)
}}>
<ReloadOutlined />
{t('common.rerun')}
</ActionButton>
</Tooltip>
<Tooltip title={t('common.edit')} mouseEnterDelay={0.5}>
<ActionButton
onClick={(e) => {
e.stopPropagation()
handleEdit(toolResponse)
if (!activeKeys.includes(id)) setActiveKeys((prev) => [...prev, id])
}}>
<EditOutlined />
{t('common.edit')}
</ActionButton>
</Tooltip>
<Tooltip title={t('common.copy')} mouseEnterDelay={0.5}>
<ActionButton
className="message-action-button"
onClick={(e) => {
e.stopPropagation()
// 使用预处理的响应数据
const resultString = JSON.stringify(result, null, 2)
copyContent(resultString, id)
}}
aria-label={t('common.copy')}>
{!copiedMap[id] && <i className="iconfont icon-copy"></i>}
{copiedMap[id] && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
const combinedData = { params: params, response: toolResult }
copyContent(JSON.stringify(combinedData, null, 2), id)
}}>
{copiedMap[id] ? <CheckOutlined /> : <CopyOutlined />}
{copiedMap[id] ? t('common.copied') : t('common.copy')}
</ActionButton>
</Tooltip>
</>
@ -141,17 +328,38 @@ const MessageTools: FC<Props> = ({ message }) => {
</ActionButtonsContainer>
</MessageTitleLabel>
),
children: isDone && result && <ToolResponseContent result={result} fontFamily={fontFamily} fontSize="12px" />
})
}
children: isDone ? (
<ToolResponseContent
params={params} // Use derived params
response={toolResult}
fontFamily={fontFamily}
fontSize="12px"
isEditing={editingToolId === id}
editedParamsString={editedParams}
onParamsChange={handleParamsChange}
onSave={() => handleSaveEdit(toolResponse)}
onCancel={handleCancelEdit}
/>
) : null
}
})
}, [
toolResponses,
t,
copiedMap,
copyContent,
editingToolId,
editedParams,
handleEdit,
handleRerun,
handleSaveEdit,
handleCancelEdit,
handleParamsChange,
activeKeys,
fontFamily // Added fontFamily
])
return items
}, [toolResponses, t, copiedMap, copyContent])
// 如果没有工具响应,则不渲染组件
if (toolResponses.length === 0) {
return null
}
if (toolResponses.length === 0) return null
return (
<>
@ -163,54 +371,21 @@ const MessageTools: FC<Props> = ({ message }) => {
title={item.label}
isActive={activeKeys.includes(item.key as string)}
onToggle={() => {
if (activeKeys.includes(item.key as string)) {
setActiveKeys(activeKeys.filter((k) => k !== item.key))
} else {
setActiveKeys([...activeKeys, item.key as string])
}
setActiveKeys((prev) =>
prev.includes(item.key as string) ? prev.filter((k) => k !== item.key) : [...prev, item.key as string]
)
}}>
{item.children}
</CustomCollapse>
))}
</ToolsContainer>
<Modal
title={expandedResponse?.title}
open={!!expandedResponse}
onCancel={() => setExpandedResponse(null)}
footer={null}
width="80%"
centered
destroyOnClose={true} // 关闭时销毁内容,减少内存占用
maskClosable={true} // 点击遮罩关闭
styles={{
body: {
maxHeight: '80vh',
overflow: 'auto',
padding: '0', // 减少内边距
contain: 'content' // 优化渲染
},
mask: {
backgroundColor: 'rgba(0, 0, 0, 0.45)', // 调整遮罩透明度
backdropFilter: 'blur(2px)' // 模糊效果,提升视觉体验
}
}}>
{expandedResponse && (
<ExpandedResponseContent
content={expandedResponse.content}
fontFamily={fontFamily}
fontSize={fontSize}
onCopy={() => {
navigator.clipboard.writeText(expandedResponse.content)
antdMessage.success({ content: t('message.copied'), key: 'copy-expanded' })
}}
/>
)}
</Modal>
{/* Removed Modal component */}
</>
)
}
// --- Styled Components --- (Keep existing styled components definitions)
const MessageTitleLabel = styled.div`
display: flex;
flex-direction: row;
@ -265,21 +440,25 @@ const ToolsContainer = styled.div`
const ActionButton = styled.button`
background: none;
border: none;
color: var(--color-text-2);
border: 1px solid var(--color-border); /* Add border */
color: var(--color-text); /* Use primary text color for better contrast */
cursor: pointer;
padding: 4px 8px;
display: flex;
padding: 1px 5px; /* Adjust padding for border */
font-size: 12px; /* Smaller font size */
font-weight: 500; /* Increase font weight */
display: inline-flex; /* Use inline-flex for icon + text */
align-items: center;
gap: 4px; /* Add gap between icon and text */
justify-content: center;
opacity: 0.7;
user-select: none; /* Prevent text selection */
opacity: 0.8; /* Slightly increase opacity */
transition: all 0.2s;
border-radius: 4px;
&:hover {
opacity: 1;
color: var(--color-text);
background-color: var(--color-bg-1);
background-color: var(--color-bg-1); /* Use a subtle background on hover */
}
&:focus-visible {
@ -292,6 +471,6 @@ const ActionButton = styled.button`
font-size: 14px;
}
`
// --- End Styled Components ---
// 使用 memo 包装组件,避免不必要的重渲染
export default memo(MessageTools)

View File

@ -248,6 +248,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
processedIds.add(message.id) // 标记此消息ID为已处理
// 如果是工具调用相关消息,直接添加到显示列表中
if (message.metadata?.isToolResultQuery || message.metadata?.isToolResultResponse) {
batchDisplayMessages.push(message)
messageIdMap.set(message.id, true)
return
}
const idSet = message.role === 'user' ? userIdSet : assistantIdSet
const messageId = message.role === 'user' ? message.id : message.askId
@ -353,6 +360,11 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display
const processMessage = (message: Message) => {
if (!message) return
// 跳过隐藏的消息(系统消息)
if (message.isHidden) {
return
}
// 跳过已处理的消息ID
if (processedIds.has(message.id)) {
return
@ -360,6 +372,13 @@ const computeDisplayMessages = (messages: Message[], startIndex: number, display
processedIds.add(message.id) // 标记此消息ID为已处理
// 如果是工具调用相关消息,直接添加到显示列表中
if (message.metadata?.isToolResultQuery || message.metadata?.isToolResultResponse) {
displayMessages.push(message)
messageIdMap.set(message.id, true)
return
}
const idSet = message.role === 'user' ? userIdSet : assistantIdSet
const messageId = message.role === 'user' ? message.id : message.askId

View File

@ -1,33 +1,139 @@
import { Button, Input } from 'antd' // Import Button and Input
import { FC, memo, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
// --- Styled Components Definitions ---
// Add FlexContainer style and modify Section style
const FlexContainer = styled.div`
display: flex;
gap: 16px;
align-items: stretch; /* Ensure items stretch to fill height */
`
// Add Divider style
const Divider = styled.div`
width: 1px;
background-color: var(--color-border); /* Use border color for divider */
align-self: stretch; /* Make divider stretch full height */
`
const Section = styled.div<{ flexBasis?: string }>`
flex: 1; /* Allow sections to grow/shrink */
flex-basis: ${(props) => props.flexBasis || 'auto'}; /* Set flex-basis if provided */
min-width: 0; /* Prevent overflow issues with flex items */
`
const SectionLabel = styled.div`
font-weight: 500;
color: var(--color-text-2);
margin-bottom: 4px;
font-size: 11px; /* Slightly smaller label */
`
const ToolResponseContainer = styled.div`
background: var(--color-bg-1);
border-radius: 0 0 4px 4px;
padding: 12px 16px;
/* overflow: auto; Remove overflow here, let sections handle scrolling if needed */
/* max-height: 300px; Remove fixed max-height for the container */
border-top: none;
position: relative;
will-change: transform; /* 优化渲染性能 */
transform: translateZ(0); /* 启用硬件加速 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
perspective: 1000;
-webkit-perspective: 1000;
contain: content; /* 限制重绘范围 */
background-color: var(--color-bg-1); /* 确保背景色 */
`
const CodeBlock = styled.pre`
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text);
font-family: ubuntu;
contain: content;
max-height: 280px; /* Limit height of code block */
overflow-y: auto; /* Add scrollbar if content exceeds max height */
/* transform: translateZ(0); 移除硬件加速,可能导致编辑时闪烁 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
`
const LoadingPlaceholder = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 50px;
color: var(--color-text-2);
font-size: 14px;
transform: translateZ(0); /* 启用硬件加速 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
`
// --- Component Definition ---
// Update props interface to accept editing props
interface ToolResponseContentProps {
result: any
params: any
response: any
fontFamily: string
fontSize: string | number
isEditing: boolean // New prop
editedParamsString: string // New prop
onParamsChange: (newParams: string) => void // New prop
onSave: () => void // New prop
onCancel: () => void // New prop
}
const ToolResponseContent: FC<ToolResponseContentProps> = ({ result, fontFamily, fontSize }) => {
const ToolResponseContent: FC<ToolResponseContentProps> = ({
params,
response,
fontFamily,
fontSize,
isEditing,
editedParamsString,
onParamsChange,
onSave,
onCancel
}) => {
console.log('[ToolResponseContent] Rendering with props:', { isEditing, editedParamsString }) // Log received props
const { t } = useTranslation() // Get translation function
const [isVisible, setIsVisible] = useState(false)
const [isContentReady, setIsContentReady] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<string>('')
// Use separate refs for params and response strings
const paramsStringRef = useRef<string>('')
const responseStringRef = useRef<string>('')
// 预处理 JSON 数据,使用 useLayoutEffect 在渲染前完成
// Preprocess params and response JSON data
useLayoutEffect(() => {
// 使用 setTimeout 将处理移到下一个微任务,避免阻塞主线程
const timer = setTimeout(() => {
try {
contentRef.current = JSON.stringify(result, null, 2)
// Stringify params, handle potential empty objects/null/undefined
paramsStringRef.current =
params && Object.keys(params).length > 0 ? JSON.stringify(params, null, 2) : t('message.tools.no_params') // Display message if no params
} catch (error) {
console.error('Error stringifying result:', error)
contentRef.current = String(result)
console.error('Error stringifying params:', error)
paramsStringRef.current = String(params)
}
try {
// Stringify response
responseStringRef.current = JSON.stringify(response, null, 2)
} catch (error) {
console.error('Error stringifying response:', error)
responseStringRef.current = String(response)
}
setIsContentReady(true)
}, 0)
return () => clearTimeout(timer)
}, [result])
}, [params, response, t]) // Add t to dependencies
// 使用 IntersectionObserver 检测组件是否可见
useEffect(() => {
@ -48,57 +154,79 @@ const ToolResponseContent: FC<ToolResponseContentProps> = ({ result, fontFamily,
return () => observer.disconnect()
}, [])
// Render separate sections for params and response in a flex container
return (
<ToolResponseContainer ref={containerRef} style={{ fontFamily, fontSize }}>
{isVisible && isContentReady ? (
<CodeBlock>{contentRef.current}</CodeBlock>
<FlexContainer>
<Section flexBasis="40%">
<SectionLabel>{t('message.tools.parameters')}:</SectionLabel>
{isEditing ? (
<EditContainer>
<StyledTextArea
autoSize={{ minRows: 3, maxRows: 10 }}
value={editedParamsString}
onChange={(e) => onParamsChange(e.target.value)}
style={{ fontFamily: 'ubuntu', fontSize: '12px' }} // Ensure consistent font
/>
<EditActions>
<Button size="small" onClick={onCancel}>
{t('common.cancel')}
</Button>
<Button type="primary" size="small" onClick={onSave}>
{t('common.save_rerun', 'Save & Rerun')} {/* TODO: Add translation */}
</Button>
</EditActions>
</EditContainer>
) : (
<CodeBlock>{paramsStringRef.current}</CodeBlock>
)}
</Section>
<Divider />
<Section flexBasis="60%">
{' '}
{/* Adjust flex-basis to 60% */}
<SectionLabel>{t('message.tools.results')}:</SectionLabel>
<CodeBlock>{responseStringRef.current}</CodeBlock>
</Section>
</FlexContainer>
) : (
<LoadingPlaceholder>...</LoadingPlaceholder>
<LoadingPlaceholder>{t('common.loading')}...</LoadingPlaceholder>
)}
</ToolResponseContainer>
)
}
const ToolResponseContainer = styled.div`
background: var(--color-bg-1);
border-radius: 0 0 4px 4px;
padding: 12px 16px;
overflow: auto;
max-height: 300px;
border-top: none;
position: relative;
will-change: transform; /* 优化渲染性能 */
transform: translateZ(0); /* 启用硬件加速 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
perspective: 1000;
-webkit-perspective: 1000;
contain: content; /* 限制重绘范围 */
background-color: var(--color-bg-1); /* 确保背景色 */
`
const CodeBlock = styled.pre`
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--color-text);
font-family: ubuntu;
contain: content; /* 优化渲染性能 */
transform: translateZ(0); /* 启用硬件加速 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
`
const LoadingPlaceholder = styled.div`
// --- Additional Styled Components for Editing ---
const EditContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 50px;
color: var(--color-text-2);
font-size: 14px;
transform: translateZ(0); /* 启用硬件加速 */
backface-visibility: hidden; /* 使用 GPU 加速 */
-webkit-backface-visibility: hidden;
flex-direction: column;
/* height: 100%; Remove fixed height, let content determine height */
`
const StyledTextArea = styled(Input.TextArea)`
flex-grow: 1; /* Allow textarea to fill available space */
resize: vertical; /* Allow vertical resize */
margin-bottom: 8px;
font-family: 'Ubuntu Mono', monospace !important; /* Ensure monospace font */
font-size: 12px !important;
line-height: 1.5;
background-color: var(--color-bg-input); /* Use input background */
border: 1px solid var(--color-border);
color: var(--color-text);
border-radius: 4px;
&:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-border);
}
`
const EditActions = styled.div`
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: auto; /* Push actions to the bottom */
`
// 使用 memo 包装组件,避免不必要的重渲染

View File

@ -0,0 +1,50 @@
import { useTheme } from '@renderer/context/ThemeProvider'
import { getShikiInstance } from '@renderer/utils/shiki'
import { Card } from 'antd'
import MarkdownIt from 'markdown-it'
import { npxFinder } from 'npx-scope-finder'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface McpDescriptionProps {
searchKey: string
}
const MCPDescription = ({ searchKey }: McpDescriptionProps) => {
const [renderedMarkdown, setRenderedMarkdown] = useState('')
const [loading, setLoading] = useState(false)
const md = useRef<MarkdownIt>(
new MarkdownIt({
linkify: true, // 自动转换 URL 为链接
typographer: true // 启用印刷格式优化
})
)
const { theme } = useTheme()
const getMcpInfo = async () => {
setLoading(true)
const packages = await npxFinder(searchKey).finally(() => setLoading(false))
const readme = packages[0]?.original?.readme ?? '暂无描述'
setRenderedMarkdown(md.current.render(readme))
}
useEffect(() => {
const sk = getShikiInstance(theme)
md.current.use(sk)
getMcpInfo()
}, [theme, searchKey])
return (
<Section>
<Card loading={loading}>
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
</Card>
</Section>
)
}
const Section = styled.div`
padding-top: 8px;
`
export default MCPDescription

View File

@ -1,5 +1,6 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { Button, Flex, Form, Input, Radio, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
@ -46,6 +47,21 @@ const PipRegistry: Registry[] = [
type TabKey = 'settings' | 'tools' | 'prompts' | 'resources'
const parseKeyValueString = (str: string): Record<string, string> => {
const result: Record<string, string> = {}
str.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...value] = line.split('=')
const formatValue = value.join('=').trim()
const formatKey = key.trim()
if (formatKey && formatValue) {
result[formatKey] = formatValue
}
}
})
return result
}
const McpSettings: React.FC<Props> = ({ server }) => {
const { t } = useTranslation()
const { deleteMCPServer, updateMCPServer } = useMCPServers()
@ -211,31 +227,11 @@ const McpSettings: React.FC<Props> = ({ server }) => {
// set env variables
if (values.env) {
const env: Record<string, string> = {}
values.env.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...chunks] = line.split('=')
const value = chunks.join('=')
if (key && value) {
env[key.trim()] = value.trim()
}
}
})
mcpServer.env = env
mcpServer.env = parseKeyValueString(values.env)
}
if (values.headers) {
const headers: Record<string, string> = {}
values.headers.split('\n').forEach((line) => {
if (line.trim()) {
const [key, ...chunks] = line.split(':')
const value = chunks.join(':')
if (key && value) {
headers[key.trim()] = value.trim()
}
}
})
mcpServer.headers = headers
mcpServer.headers = parseKeyValueString(values.headers)
}
try {
@ -330,18 +326,57 @@ const McpSettings: React.FC<Props> = ({ server }) => {
try {
if (active) {
const localTools = await window.api.mcp.listTools(server)
// 如果是 workspacefile 服务,自动设置 WORKSPACE_PATH 环境变量
const serverToActivate = { ...server }
if (server.name === '@cherry/workspacefile') {
// 获取当前工作区路径
const currentWorkspace = window.store
.getState()
.workspace.workspaces.find((w) => w.id === window.store.getState().workspace.currentWorkspaceId)
// 获取对AI可见的工作区
const visibleWorkspaces = window.store.getState().workspace.workspaces.filter((w) => w.visibleToAI !== false)
// 检查当前工作区是否对AI可见
if (!currentWorkspace || !visibleWorkspaces.some((w) => w.id === currentWorkspace.id)) {
throw new Error('当前工作区对AI不可见请在工作区设置中启用AI可见性')
}
if (currentWorkspace && currentWorkspace.path) {
// 设置 WORKSPACE_PATH 环境变量
// Remove redundant || {}
const env = { ...serverToActivate.env }
env.WORKSPACE_PATH = currentWorkspace.path
serverToActivate.env = env
// 更新表单中的环境变量显示
const envText = Object.entries(env)
.map(([key, value]) => `${key}=${value}`)
.join('\n')
form.setFieldValue('env', envText)
console.log(`[MCP] Setting WORKSPACE_PATH to ${currentWorkspace.path} for @cherry/workspacefile`)
} else {
throw new Error('未找到当前工作区,请先设置工作区')
}
}
const localTools = await window.api.mcp.listTools(serverToActivate)
setTools(localTools)
const localPrompts = await window.api.mcp.listPrompts(server)
const localPrompts = await window.api.mcp.listPrompts(serverToActivate)
setPrompts(localPrompts)
const localResources = await window.api.mcp.listResources(server)
const localResources = await window.api.mcp.listResources(serverToActivate)
setResources(localResources)
// 更新服务器配置
updateMCPServer({ ...serverToActivate, isActive: active })
} else {
await window.api.mcp.stopServer(server)
updateMCPServer({ ...server, isActive: active })
}
updateMCPServer({ ...server, isActive: active })
} catch (error: any) {
window.modal.error({
title: t('settings.mcp.startError'),
@ -517,6 +552,14 @@ const McpSettings: React.FC<Props> = ({ server }) => {
}
]
if (server.searchKey) {
tabs.push({
key: 'description',
label: t('settings.mcp.tabs.description'),
children: <MCPDescription searchKey={server.searchKey} />
})
}
if (server.isActive) {
tabs.push(
{

View File

@ -106,8 +106,9 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
<Flex vertical align="flex-start">
<Flex align="center" style={{ width: '100%' }}>
<Typography.Text strong>{tool.name}</Typography.Text>
{/* Display toolKey instead of the random id */}
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
{tool.id}
{tool.toolKey}
</Typography.Text>
</Flex>
{tool.description && (

View File

@ -211,6 +211,7 @@ const NpxSearch: FC<{
env: record.configSample?.env,
isActive: false,
type: record.type,
searchKey: record.fullName,
configSample: record.configSample
}

View File

@ -367,10 +367,6 @@ const CollapsibleShortMemoryManager = () => {
// 删除短记忆 - 直接删除无需确认
const handleDeleteMemory = useCallback(
async (id: string) => {
// 先从当前状态中获取要删除的记忆之外的所有记忆
const state = store.getState().memory
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
// 在本地更新topicsWithMemories避免触发useEffect
setTopicsWithMemories((prev) => {
return prev
@ -390,19 +386,9 @@ const CollapsibleShortMemoryManager = () => {
// 执行删除操作
dispatch(deleteShortMemory(id))
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
// 使用主进程的 deleteShortMemoryById 方法删除记忆
try {
// 加载当前文件数据
const currentData = await window.api.memory.loadData()
// 替换 shortMemories 数组
const newData = {
...currentData,
shortMemories: filteredShortMemories
}
// 使用 true 参数强制覆盖文件
const result = await window.api.memory.saveData(newData, true)
const result = await window.api.memory.deleteShortMemoryById(id)
if (result) {
console.log(`[CollapsibleShortMemoryManager] Successfully deleted short memory with ID ${id}`)

View File

@ -0,0 +1,391 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
saveMemoryData,
setAssistantMemoryPrompt,
setContextualMemoryPrompt,
setHistoricalContextPrompt,
setLongTermMemoryPrompt,
setShortTermMemoryPrompt
} from '@renderer/store/memory'
// Remove Tabs from import as it's no longer used after removing TabPane
import { Button, Input, Tooltip, Typography } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingGroup, SettingHelpText, SettingTitle } from '..'
const { TextArea } = Input
const { Text } = Typography
// Remove unused TabPane destructuring
// 默认提示词
const DEFAULT_LONG_TERM_MEMORY_PROMPT = `
##
类别: 信息内容
-
-
-
-
-
用户偏好: 用户喜欢简洁直接的代码修改方式
技术需求: 用户需要修复长期记忆分析功能中的问题
个人信息: 用户自称是彭于晏
交互偏好: 用户倾向于简短直接的问答方式
其他: 用户对AI记忆功能的工作原理很感兴趣
##
##
1. "类别: 信息内容"
2. 使
3.
4.
`
const DEFAULT_SHORT_TERM_MEMORY_PROMPT = `
AI理解当前对话的完整上下文
1.
2. AI回复中的重要内容和对用户问题的解决方案
3.
4.
5.
6.
7.
8.
515-20820-30
`
const DEFAULT_ASSISTANT_MEMORY_PROMPT = `
1.
2.
3.
4.
5.
JSON数组格式返回提取的记忆
["用户喜欢简洁的回答", "用户对技术话题特别感兴趣", "用户希望得到具体的代码示例"]
`
const DEFAULT_CONTEXTUAL_MEMORY_PROMPT = `
便
1.
2.
3.
使
`
const DEFAULT_HISTORICAL_CONTEXT_PROMPT = `
:
[]
:
[]
1.
2.
3. 使
4. 使
使
:
1. (/)
2. ID
3.
JSON格式回答:
{
"needsHistoricalContext": true/false,
"selectedTopicId": "话题ID或null",
"reason": "详细解释为什么需要或不需要引用历史对话"
}
`
// Remove unused TabContainer definition
const StyledTextArea = styled(TextArea)`
font-family: monospace;
margin-bottom: 16px;
`
const ButtonGroup = styled.div`
display: flex;
gap: 8px;
margin-bottom: 16px;
`
const PromptSettingItem = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 24px;
`
const PromptSettingTitle = styled.div`
font-weight: 500;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
`
const PromptSettingDescription = styled(Text)`
margin-bottom: 8px;
`
const PromptSettings: FC = () => {
const dispatch = useAppDispatch()
const { t } = useTranslation()
// 从Redux获取提示词
const longTermMemoryPrompt = useAppSelector((state) => state.memory.longTermMemoryPrompt)
const shortTermMemoryPrompt = useAppSelector((state) => state.memory.shortTermMemoryPrompt)
const assistantMemoryPrompt = useAppSelector((state) => state.memory.assistantMemoryPrompt)
const contextualMemoryPrompt = useAppSelector((state) => state.memory.contextualMemoryPrompt)
const historicalContextPrompt = useAppSelector((state) => state.memory.historicalContextPrompt)
// 本地状态
const [longTermPrompt, setLongTermPrompt] = useState(longTermMemoryPrompt || DEFAULT_LONG_TERM_MEMORY_PROMPT)
const [shortTermPrompt, setShortTermPrompt] = useState(shortTermMemoryPrompt || DEFAULT_SHORT_TERM_MEMORY_PROMPT)
const [assistantPrompt, setAssistantPrompt] = useState(assistantMemoryPrompt || DEFAULT_ASSISTANT_MEMORY_PROMPT)
const [contextualPrompt, setContextualPrompt] = useState(contextualMemoryPrompt || DEFAULT_CONTEXTUAL_MEMORY_PROMPT)
const [historicalPrompt, setHistoricalPrompt] = useState(historicalContextPrompt || DEFAULT_HISTORICAL_CONTEXT_PROMPT)
// 保存提示词
const handleSaveLongTermPrompt = async () => {
dispatch(setLongTermMemoryPrompt(longTermPrompt))
await dispatch(saveMemoryData({ longTermMemoryPrompt: longTermPrompt }))
window.message.success(t('settings.memory.promptSettings.promptSaved') || '提示词已保存')
}
const handleSaveShortTermPrompt = async () => {
dispatch(setShortTermMemoryPrompt(shortTermPrompt))
await dispatch(saveMemoryData({ shortTermMemoryPrompt: shortTermPrompt }))
window.message.success(t('settings.memory.promptSettings.promptSaved') || '提示词已保存')
}
const handleSaveAssistantPrompt = async () => {
dispatch(setAssistantMemoryPrompt(assistantPrompt))
await dispatch(saveMemoryData({ assistantMemoryPrompt: assistantPrompt }))
window.message.success(t('settings.memory.promptSettings.promptSaved') || '提示词已保存')
}
const handleSaveContextualPrompt = async () => {
dispatch(setContextualMemoryPrompt(contextualPrompt))
await dispatch(saveMemoryData({ contextualMemoryPrompt: contextualPrompt }))
window.message.success(t('settings.memory.promptSettings.promptSaved') || '提示词已保存')
}
const handleSaveHistoricalPrompt = async () => {
dispatch(setHistoricalContextPrompt(historicalPrompt))
await dispatch(saveMemoryData({ historicalContextPrompt: historicalPrompt }))
window.message.success(t('settings.memory.promptSettings.promptSaved') || '提示词已保存')
}
// 重置提示词
const handleResetLongTermPrompt = () => {
setLongTermPrompt(DEFAULT_LONG_TERM_MEMORY_PROMPT)
dispatch(setLongTermMemoryPrompt(null))
dispatch(saveMemoryData({ longTermMemoryPrompt: null }))
window.message.success(t('settings.memory.promptSettings.promptReset') || '提示词已重置')
}
const handleResetShortTermPrompt = () => {
setShortTermPrompt(DEFAULT_SHORT_TERM_MEMORY_PROMPT)
dispatch(setShortTermMemoryPrompt(null))
dispatch(saveMemoryData({ shortTermMemoryPrompt: null }))
window.message.success(t('settings.memory.promptSettings.promptReset') || '提示词已重置')
}
const handleResetAssistantPrompt = () => {
setAssistantPrompt(DEFAULT_ASSISTANT_MEMORY_PROMPT)
dispatch(setAssistantMemoryPrompt(null))
dispatch(saveMemoryData({ assistantMemoryPrompt: null }))
window.message.success(t('settings.memory.promptSettings.promptReset') || '提示词已重置')
}
const handleResetContextualPrompt = () => {
setContextualPrompt(DEFAULT_CONTEXTUAL_MEMORY_PROMPT)
dispatch(setContextualMemoryPrompt(null))
dispatch(saveMemoryData({ contextualMemoryPrompt: null }))
window.message.success(t('settings.memory.promptSettings.promptReset') || '提示词已重置')
}
const handleResetHistoricalPrompt = () => {
setHistoricalPrompt(DEFAULT_HISTORICAL_CONTEXT_PROMPT)
dispatch(setHistoricalContextPrompt(null))
dispatch(saveMemoryData({ historicalContextPrompt: null }))
window.message.success(t('settings.memory.promptSettings.promptReset') || '提示词已重置')
}
return (
<SettingGroup>
<SettingTitle>
{t('settings.memory.promptSettings.title') || '提示词设置'}
<Tooltip title={t('settings.memory.promptSettings.description') || '自定义记忆分析使用的提示词'}>
<InfoCircleOutlined style={{ marginLeft: 8 }} />
</Tooltip>
</SettingTitle>
<SettingHelpText>
{t('settings.memory.promptSettings.description') ||
'自定义记忆分析使用的提示词,可以根据需要调整分析的方式和结果。'}
</SettingHelpText>
{/* 长期记忆提示词 */}
<PromptSettingItem>
<PromptSettingTitle>
{t('settings.memory.promptSettings.longTermMemoryPrompt') || '长期记忆提示词'}
<ButtonGroup>
<Button type="primary" onClick={handleSaveLongTermPrompt}>
{t('settings.memory.promptSettings.savePrompt') || '保存提示词'}
</Button>
<Button onClick={handleResetLongTermPrompt}>
{t('settings.memory.promptSettings.resetToDefault') || '重置为默认值'}
</Button>
</ButtonGroup>
</PromptSettingTitle>
<PromptSettingDescription type="secondary">
{t('settings.memory.promptSettings.longTermPromptDescription') ||
'长期记忆分析提示词用于从对话中提取用户的长期偏好、习惯和背景信息。'}
</PromptSettingDescription>
<StyledTextArea
value={longTermPrompt}
onChange={(e) => setLongTermPrompt(e.target.value)}
rows={15}
placeholder={t('settings.memory.promptSettings.enterPrompt') || '输入提示词...'}
/>
</PromptSettingItem>
{/* 短期记忆提示词 */}
<PromptSettingItem>
<PromptSettingTitle>
{t('settings.memory.promptSettings.shortTermMemoryPrompt') || '短期记忆提示词'}
<ButtonGroup>
<Button type="primary" onClick={handleSaveShortTermPrompt}>
{t('settings.memory.promptSettings.savePrompt') || '保存提示词'}
</Button>
<Button onClick={handleResetShortTermPrompt}>
{t('settings.memory.promptSettings.resetToDefault') || '重置为默认值'}
</Button>
</ButtonGroup>
</PromptSettingTitle>
<PromptSettingDescription type="secondary">
{t('settings.memory.promptSettings.shortTermPromptDescription') ||
'短期记忆分析提示词用于从当前对话中提取重要的上下文信息帮助AI理解对话的连贯性。'}
</PromptSettingDescription>
<StyledTextArea
value={shortTermPrompt}
onChange={(e) => setShortTermPrompt(e.target.value)}
rows={15}
placeholder={t('settings.memory.promptSettings.enterPrompt') || '输入提示词...'}
/>
</PromptSettingItem>
{/* 上下文记忆提示词 */}
<PromptSettingItem>
<PromptSettingTitle>
{t('settings.memory.promptSettings.contextualMemoryPrompt') || '上下文记忆提示词'}
<ButtonGroup>
<Button type="primary" onClick={handleSaveContextualPrompt}>
{t('settings.memory.promptSettings.savePrompt') || '保存提示词'}
</Button>
<Button onClick={handleResetContextualPrompt}>
{t('settings.memory.promptSettings.resetToDefault') || '重置为默认值'}
</Button>
</ButtonGroup>
</PromptSettingTitle>
<PromptSettingDescription type="secondary">
{t('settings.memory.promptSettings.contextualPromptDescription') ||
'上下文记忆分析提示词用于从当前对话中提取关键主题和信息点,以便找到相关的记忆。'}
</PromptSettingDescription>
<StyledTextArea
value={contextualPrompt}
onChange={(e) => setContextualPrompt(e.target.value)}
rows={15}
placeholder={t('settings.memory.promptSettings.enterPrompt') || '输入提示词...'}
/>
</PromptSettingItem>
{/* 助手记忆提示词 */}
<PromptSettingItem>
<PromptSettingTitle>
{t('settings.memory.promptSettings.assistantMemoryPrompt') || '助手记忆提示词'}
<ButtonGroup>
<Button type="primary" onClick={handleSaveAssistantPrompt}>
{t('settings.memory.promptSettings.savePrompt') || '保存提示词'}
</Button>
<Button onClick={handleResetAssistantPrompt}>
{t('settings.memory.promptSettings.resetToDefault') || '重置为默认值'}
</Button>
</ButtonGroup>
</PromptSettingTitle>
<PromptSettingDescription type="secondary">
{t('settings.memory.promptSettings.assistantPromptDescription') ||
'助手记忆分析提示词用于提取与特定助手相关的用户偏好和需求。'}
</PromptSettingDescription>
<StyledTextArea
value={assistantPrompt}
onChange={(e) => setAssistantPrompt(e.target.value)}
rows={15}
placeholder={t('settings.memory.promptSettings.enterPrompt') || '输入提示词...'}
/>
</PromptSettingItem>
{/* 历史上下文提示词 */}
<PromptSettingItem>
<PromptSettingTitle>
{t('settings.memory.promptSettings.historicalContextPrompt') || '历史上下文提示词'}
<ButtonGroup>
<Button type="primary" onClick={handleSaveHistoricalPrompt}>
{t('settings.memory.promptSettings.savePrompt') || '保存提示词'}
</Button>
<Button onClick={handleResetHistoricalPrompt}>
{t('settings.memory.promptSettings.resetToDefault') || '重置为默认值'}
</Button>
</ButtonGroup>
</PromptSettingTitle>
<PromptSettingDescription type="secondary">
{t('settings.memory.promptSettings.historicalPromptDescription') ||
'历史上下文分析提示词用于判断当前对话是否需要引用历史对话来提供更完整的回答。'}
</PromptSettingDescription>
<StyledTextArea
value={historicalPrompt}
onChange={(e) => setHistoricalPrompt(e.target.value)}
rows={15}
placeholder={t('settings.memory.promptSettings.enterPrompt') || '输入提示词...'}
/>
</PromptSettingItem>
</SettingGroup>
)
}
export default PromptSettings

View File

@ -1,7 +1,6 @@
import { DeleteOutlined } from '@ant-design/icons'
import { addShortMemoryItem } from '@renderer/services/MemoryService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import store from '@renderer/store'
import { deleteShortMemory, setShortMemoryActive } from '@renderer/store/memory'
import { Button, Empty, Input, List, Switch, Tooltip, Typography } from 'antd'
import _ from 'lodash'
@ -48,26 +47,12 @@ const ShortMemoryManager = () => {
// 删除短记忆 - 直接删除无需确认,使用节流避免频繁删除操作
const handleDeleteMemory = useCallback(
_.throttle(async (id: string) => {
// 先从当前状态中获取要删除的记忆之外的所有记忆
const state = store.getState().memory
const filteredShortMemories = state.shortMemories.filter((memory) => memory.id !== id)
// 执行删除操作
dispatch(deleteShortMemory(id))
// 直接使用 window.api.memory.saveData 方法保存过滤后的列表
// 使用主进程的 deleteShortMemoryById 方法删除记忆
try {
// 加载当前文件数据
const currentData = await window.api.memory.loadData()
// 替换 shortMemories 数组
const newData = {
...currentData,
shortMemories: filteredShortMemories
}
// 使用 true 参数强制覆盖文件
const result = await window.api.memory.saveData(newData, true)
const result = await window.api.memory.deleteShortMemoryById(id)
if (result) {
console.log(`[ShortMemoryManager] Successfully deleted short memory with ID ${id}`)

View File

@ -58,6 +58,7 @@ import MemoryDeduplicationPanel from './MemoryDeduplicationPanel'
import MemoryListManager from './MemoryListManager'
import MemoryMindMap from './MemoryMindMap'
import PriorityManagementSettings from './PriorityManagementSettings'
import PromptSettings from './PromptSettings'
const MemorySettings: FC = () => {
const { t } = useTranslation()
@ -382,8 +383,8 @@ const MemorySettings: FC = () => {
return
}
// 调用长期记忆分析函数
analyzeAndAddMemories(selectedTopicId)
// 调用长期记忆分析函数,并指定为手动分析
analyzeAndAddMemories(selectedTopicId, true)
}
}
@ -676,6 +677,20 @@ const MemorySettings: FC = () => {
size="large"
animated={{ inkBar: true, tabPane: true }}
items={[
{
key: 'promptSettings',
label: (
<TabLabelContainer>
<TabDot color="#52c41a"></TabDot>
{t('settings.memory.promptSettings.title') || '提示词设置'} {/* Use the specific title key */}
</TabLabelContainer>
),
children: (
<TabPaneSettingGroup theme={theme}>
<PromptSettings />
</TabPaneSettingGroup>
)
},
{
key: 'assistantMemory',
label: (

View File

@ -0,0 +1,107 @@
import React, { useState } from 'react'
import { Layout, Typography, Divider, message } from 'antd'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import WorkspaceSelector from '@renderer/components/WorkspaceSelector'
import WorkspaceExplorer from '@renderer/components/WorkspaceExplorer'
import WorkspaceFileViewer from '@renderer/components/WorkspaceFileViewer'
const { Content, Sider } = Layout
const { Title } = Typography
const WorkspaceContainer = styled(Layout)`
height: 100%;
`
const WorkspaceSider = styled(Sider)`
background: #fff;
border-right: 1px solid #f0f0f0;
overflow: hidden;
height: 100%;
display: flex;
flex-direction: column;
`
const WorkspaceContent = styled(Content)`
background: #fff;
padding: 0;
height: 100%;
overflow: hidden;
`
const WorkspaceHeader = styled.div`
padding: 16px;
border-bottom: 1px solid #f0f0f0;
`
const WorkspaceTitle = styled(Title)`
margin: 0 !important;
`
const EmptyContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 16px;
`
const WorkspacePage: React.FC = () => {
const { t } = useTranslation()
const [selectedFile, setSelectedFile] = useState<{ path: string; content: string } | null>(null)
// 处理文件选择
const handleFileSelect = (filePath: string, content: string) => {
setSelectedFile({ path: filePath, content })
}
// 关闭文件查看器
const handleCloseViewer = () => {
setSelectedFile(null)
}
// 处理文件内容更改
const handleContentChange = async (newContent: string, filePath: string) => {
try {
await window.api.file.write(filePath, newContent)
setSelectedFile((prev) => prev ? { ...prev, content: newContent } : null)
return true
} catch (error) {
console.error('保存文件失败:', error)
message.error(t('workspace.saveFileError'))
return false
}
}
return (
<WorkspaceContainer>
<WorkspaceSider width={300} theme="light">
<WorkspaceHeader>
<WorkspaceTitle level={4}>{t('workspace.title')}</WorkspaceTitle>
<Divider style={{ margin: '12px 0' }} />
<WorkspaceSelector />
</WorkspaceHeader>
<WorkspaceExplorer onFileSelect={handleFileSelect} />
</WorkspaceSider>
<WorkspaceContent>
{selectedFile ? (
<WorkspaceFileViewer
filePath={selectedFile.path}
content={selectedFile.content}
onClose={handleCloseViewer}
onContentChange={handleContentChange}
/>
) : (
<EmptyContent>
{t('workspace.selectFile')}
</EmptyContent>
)}
</WorkspaceContent>
</WorkspaceContainer>
)
}
export default WorkspacePage

View File

@ -16,14 +16,12 @@ import {
Part,
RequestOptions,
SafetySetting,
TextPart,
Tool
TextPart
} from '@google/generative-ai'
import {
isGemmaModel,
isGenerateImageModel,
isSupportedThinkingBudgetModel,
isVisionModel,
isWebSearchModel
} from '@renderer/config/models'
import { getStoreSetting } from '@renderer/hooks/useSettings'
@ -38,13 +36,19 @@ import {
import WebSearchService from '@renderer/services/WebSearchService'
import store from '@renderer/store'
import { getActiveServers } from '@renderer/store/mcp'
import { Assistant, FileType, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types'
import { Assistant, FileType, FileTypes, MCPToolResponse, Message, Model, Provider, Suggestion } from '@renderer/types' // Re-add MCPToolResponse
import { removeSpecialCharactersForTopicName } from '@renderer/utils'
import { mcpToolCallResponseToGeminiMessage, parseAndCallTools } from '@renderer/utils/mcp-tools'
import {
callMCPTool, // Re-add import
geminiFunctionCallToMcpTool, // Re-add import
mcpToolCallResponseToGeminiFunctionResponsePart, // Re-add import
mcpToolsToGeminiTools, // Import for UI updates
upsertMCPToolResponse // Re-add import
} from '@renderer/utils/mcp-tools'
import { buildSystemPrompt } from '@renderer/utils/prompt'
import { MB } from '@shared/config/constant'
import axios from 'axios'
import { flatten, isEmpty, takeRight } from 'lodash'
import { isEmpty, takeRight } from 'lodash'
import OpenAI from 'openai'
import { ChunkCallbackData, CompletionsParams } from '.'
@ -54,10 +58,10 @@ export default class GeminiProvider extends BaseProvider {
private sdk: GoogleGenerativeAI
private requestOptions: RequestOptions
private imageSdk: GoogleGenAI
// 存储对话ID到SDK实例的映射
private conversationSdks: Map<string, GoogleGenerativeAI> = new Map()
// 存储对话ID到图像SDK实例的映射
private conversationImageSdks: Map<string, GoogleGenAI> = new Map()
// // 存储对话ID到SDK实例的映射 (当前实现未复用,仅写入)
// private conversationSdks: Map<string, GoogleGenerativeAI> = new Map()
// // 存储对话ID到图像SDK实例的映射 (当前实现未复用,仅写入)
// private conversationImageSdks: Map<string, GoogleGenAI> = new Map()
constructor(provider: Provider) {
super(provider)
@ -90,10 +94,10 @@ export default class GeminiProvider extends BaseProvider {
// 创建新的SDK实例
const newSdk = new GoogleGenerativeAI(apiKey)
// 存储SDK实例覆盖之前的实例
this.conversationSdks.set(conversationId, newSdk)
// // 存储SDK实例覆盖之前的实例 (当前实现未复用,仅写入)
// this.conversationSdks.set(conversationId, newSdk)
console.log(`[GeminiProvider] Created new SDK for conversation ${conversationId} with API key`)
// console.log(`[GeminiProvider] Created new SDK for conversation ${conversationId} with API key`)
return newSdk
}
@ -116,10 +120,10 @@ export default class GeminiProvider extends BaseProvider {
// 创建新的SDK实例
const newSdk = new GoogleGenAI({ apiKey: apiKey, httpOptions: { baseUrl: this.getBaseURL() } })
// 存储SDK实例覆盖之前的实例
this.conversationImageSdks.set(conversationId, newSdk)
// // 存储SDK实例覆盖之前的实例 (当前实现未复用,仅写入)
// this.conversationImageSdks.set(conversationId, newSdk)
console.log(`[GeminiProvider] Created new Image SDK for conversation ${conversationId} with API key`)
// console.log(`[GeminiProvider] Created new Image SDK for conversation ${conversationId} with API key`)
return newSdk
}
@ -370,9 +374,9 @@ export default class GeminiProvider extends BaseProvider {
systemInstruction = await buildSystemPrompt(enhancedPrompt, mcpTools, getActiveServers(store.getState()))
}
// const tools = mcpToolsToGeminiTools(mcpTools)
const tools: Tool[] = []
const toolResponses: MCPToolResponse[] = []
// Format MCP tools for Gemini native function calling
const tools = mcpToolsToGeminiTools(mcpTools)
const toolResponses: MCPToolResponse[] = [] // Re-add for UI updates
if (!WebSearchService.isOverwriteEnabled() && assistant.enableWebSearch && isWebSearchModel(model)) {
tools.push({
@ -396,7 +400,7 @@ export default class GeminiProvider extends BaseProvider {
model: model.id,
...(isGemmaModel(model) ? {} : { systemInstruction: systemInstruction }),
safetySettings: this.getSafetySettings(model.id),
tools: tools,
tools: tools, // Pass formatted tools here
generationConfig: {
...thinkingConfig,
maxOutputTokens: maxTokens,
@ -459,42 +463,62 @@ export default class GeminiProvider extends BaseProvider {
const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal })
let time_first_token_millsec = 0
const processToolUses = async (content: string, idx: number) => {
const toolResults = await parseAndCallTools(
content,
toolResponses,
onChunk,
idx,
mcpToolCallResponseToGeminiMessage,
mcpTools,
isVisionModel(model)
)
if (toolResults && toolResults.length > 0) {
history.push(messageContents)
const newChat = geminiModel.startChat({ history })
const newStream = await newChat.sendMessageStream(flatten(toolResults.map((ts) => (ts as Content).parts)), {
signal
})
await processStream(newStream, idx + 1)
}
}
// Remove unused processToolUses function
// const processToolUses = async (content: string, idx: number) => { ... }
const processStream = async (stream: GenerateContentStreamResult, idx: number) => {
let content = ''
// 添加最大工具调用次数限制,防止无限循环
const MAX_TOOL_CALLS = 5
/**
*
* @param stream
* @param toolCallCount
* @param isFirstCall
* @param previousToolResponses
*/
const processStreamWithToolCalls = async (
stream: GenerateContentStreamResult,
toolCallCount: number = 0,
isFirstCall: boolean = true,
previousToolResponses: { functionCall: any; response: any }[] = []
) => {
// 检查是否超过最大工具调用次数
if (toolCallCount >= MAX_TOOL_CALLS) {
console.warn(`[GeminiProvider] 达到最大工具调用次数限制 (${MAX_TOOL_CALLS}),停止处理更多工具调用`)
onChunk({
text: `\n\n注意已达到最大工具调用次数限制 (${MAX_TOOL_CALLS})。`
})
return
}
let aggregatedResponseText = ''
let functionCallParts: Part[] = [] // 存储潜在的函数调用
// 处理流式响应
for await (const chunk of stream.stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
if (time_first_token_millsec == 0) {
// 只在第一次调用时更新首个token时间
if (isFirstCall && time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
}
const time_completion_millsec = new Date().getTime() - start_time_millsec
content += chunk.text()
processToolUses(content, idx)
// 聚合文本内容
const chunkText = chunk.text()
aggregatedResponseText += chunkText
// 检查块中是否有函数调用
const functionCalls = chunk.functionCalls()
if (functionCalls && functionCalls.length > 0) {
// 存储函数调用部分以供后续处理
functionCallParts = [{ functionCall: functionCalls[0] }]
}
// 发送文本块到UI
onChunk({
text: chunk.text(),
text: chunkText,
usage: {
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
@ -506,12 +530,123 @@ export default class GeminiProvider extends BaseProvider {
time_first_token_millsec
},
search: chunk.candidates?.[0]?.groundingMetadata,
mcpToolResponse: toolResponses
mcpToolResponse: toolResponses // 传递更新的工具响应到UI
})
}
// --- 处理整个流后 ---
if (functionCallParts.length > 0) {
// 检测到函数调用
const functionCall = functionCallParts[0].functionCall
if (!functionCall) {
console.error('Error: functionCall part exists but functionCall is undefined')
return // 或适当处理错误
}
console.log(`[GeminiProvider] 检测到函数调用 #${toolCallCount + 1}:`, functionCall)
// 将Gemini函数调用转换为MCPTool格式
const mcpToolToCall = geminiFunctionCallToMcpTool(mcpTools, functionCall)
if (mcpToolToCall) {
// --- UI更新: 标记工具为调用中 ---
const toolCallIdForUI = `${functionCall.name}-${Date.now()}` // 创建用于UI跟踪的唯一ID
const actualArgs = functionCall.args || {} // 获取实际参数
upsertMCPToolResponse(
toolResponses,
{ id: toolCallIdForUI, tool: mcpToolToCall, args: actualArgs, status: 'invoking' },
onChunk
)
// 执行MCP工具
const toolResponse = await callMCPTool(mcpToolToCall)
console.log('[GeminiProvider] 收到MCP工具响应:', JSON.stringify(toolResponse, null, 2))
// --- UI更新: 标记工具为完成 ---
upsertMCPToolResponse(
toolResponses,
{ id: toolCallIdForUI, tool: mcpToolToCall, args: actualArgs, status: 'done', response: toolResponse },
onChunk
)
// 将工具响应格式化为Gemini FunctionResponse Part
const functionResponsePart = mcpToolCallResponseToGeminiFunctionResponsePart(
functionCall.name,
toolResponse
)
console.log(
'[GeminiProvider] 格式化的FunctionResponse Part:',
JSON.stringify(functionResponsePart, null, 2)
)
// --- 代理循环: 将结果发送回Gemini ---
console.log(`[GeminiProvider] 将工具响应 #${toolCallCount + 1} 发送回Gemini...`)
// 将工具调用和响应添加到历史记录中
const currentToolResponse = {
functionCall: functionCall,
response: toolResponse
}
// 将当前工具调用添加到历史中
const updatedToolResponses = [...previousToolResponses, currentToolResponse]
// 将工具调用和响应添加到历史中
const toolCallMessage: Content = {
role: 'user',
parts: [
{
text: `工具调用结果 (${functionCall.name}):\n${JSON.stringify(
toolResponse.content.map((item) => {
if (item.type === 'text') return item.text
if (item.type === 'image' && item.data) return `[图片数据]`
return JSON.stringify(item)
}),
null,
2
)}`
}
]
}
// 将工具调用结果添加到历史中
history.push(toolCallMessage)
// 打印历史记录信息,便于调试
console.log(`[GeminiProvider] 工具调用历史记录已更新,当前历史长度: ${history.length}`)
// 使用更新后的历史记录创建新的聊天实例
const updatedChat = geminiModel.startChat({ history })
// 使用sendMessageStream进行下一次API调用
const nextResponseStream = await updatedChat.sendMessageStream([functionResponsePart], { signal })
// 递归处理下一个响应流,可能包含更多工具调用
await processStreamWithToolCalls(nextResponseStream, toolCallCount + 1, false, updatedToolResponses)
} else {
console.error('[GeminiProvider] 找不到匹配的MCP工具:', functionCall.name)
// 处理找不到工具的情况
onChunk({ text: `\n\n错误: 找不到工具 ${functionCall.name}` })
}
} else {
// 没有函数调用,聚合文本是最终响应
console.log(
`[GeminiProvider] 没有检测到函数调用 (调用计数: ${toolCallCount})。最终响应:`,
aggregatedResponseText
)
// 如果需要可以在这里调用onChunk一次传递完整的聚合文本
// 但流式处理应该已经发送了所有部分。
}
}
await processStream(userMessagesStream, 0).finally(cleanup)
// 使用新的递归函数处理初始流
const processStream = async (stream: GenerateContentStreamResult) => {
await processStreamWithToolCalls(stream, 0, true, [])
}
// Start processing the initial stream
await processStream(userMessagesStream /* Remove unused 0 */).finally(cleanup)
}
}
@ -720,7 +855,7 @@ export default class GeminiProvider extends BaseProvider {
* @returns The suggestions
*/
public async suggestions(): Promise<Suggestion[]> {
return []
return [] // Placeholder/Unused interface method? Actual logic in generateImageExp
}
/**
@ -782,7 +917,7 @@ export default class GeminiProvider extends BaseProvider {
* @returns The generated image
*/
public async generateImage(): Promise<string[]> {
return []
return [] // Placeholder/Unused interface method?
}
/**

View File

@ -419,7 +419,8 @@ export default class OpenAIProvider extends BaseProvider {
let firstChunk = true
const processToolUses = async (content: string, idx: number) => {
const toolResults = await parseAndCallTools(
// 只执行工具调用,不生成第二条消息
await parseAndCallTools(
content,
toolResponses,
onChunk,
@ -429,36 +430,8 @@ export default class OpenAIProvider extends BaseProvider {
isVisionModel(model)
)
if (toolResults.length > 0) {
reqMessages.push({
role: 'assistant',
content: content
} as ChatCompletionMessageParam)
toolResults.forEach((ts) => reqMessages.push(ts as ChatCompletionMessageParam))
const newStream = await this.sdk.chat.completions
// @ts-ignore key is not typed
.create(
{
model: model.id,
messages: reqMessages,
temperature: this.getTemperature(assistant, model),
top_p: this.getTopP(assistant, model),
max_tokens: maxTokens,
keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput(),
// tools: tools,
...getOpenAIWebSearchParams(assistant, model),
...this.getReasoningEffort(assistant, model),
...this.getProviderSpecificParameters(assistant, model),
...this.getCustomParameters(assistant)
},
{
signal
}
)
await processStream(newStream, idx + 1)
}
// 不再生成基于工具结果的新消息
console.log('[OpenAIProvider] 工具调用已执行,不生成第二条消息')
}
const processStream = async (stream: any, idx: number) => {

View File

@ -21,6 +21,7 @@ import {
convertLinksToZhipu,
extractUrlsFromMarkdown
} from '@renderer/utils/linkConverter'
import { executeToolCalls, parseToolUse } from '@renderer/utils/mcp-tools'
import { cloneDeep, findLast, isEmpty } from 'lodash'
import AiProvider from '../providers/AiProvider'
@ -113,7 +114,7 @@ export async function fetchChatCompletion({
}
} else {
query = lastMessage.content
}
} // Corrected brace placement
// 处理搜索结果
message.metadata = {
@ -123,12 +124,13 @@ export async function fetchChatCompletion({
window.keyv.set(`web-search-${lastMessage?.id}`, webSearchResponse)
} catch (error) {
// Restoring the catch block for the outer try
console.error('Web search failed:', error)
}
}
}
}
}
} // Closes 'if (lastMessage)'
} // Closes 'if (isEmpty(webSearchParams) ...)'
} // Closes 'if (WebSearchService.isWebSearchEnabled() ...)'
} // Closes searchTheWeb function
try {
let _messages: Message[] = []
@ -149,6 +151,11 @@ export async function fetchChatCompletion({
const mcpTools: MCPTool[] = []
const enabledMCPs = lastUserMessage?.enabledMCPs
// Store enabledMCPs on the assistant message object so MessageTools can access it for rerun
if (enabledMCPs && enabledMCPs.length > 0) {
message.enabledMCPs = enabledMCPs // Add this line
}
if (enabledMCPs && enabledMCPs.length > 0) {
for (const mcpServer of enabledMCPs) {
const tools = await window.api.mcp.listTools(mcpServer)
@ -312,6 +319,56 @@ export async function fetchChatCompletion({
message.status = 'success'
message = withGenerateImage(message)
// 检查消息内容中是否包含工具调用
const mcpToolResponses = message.metadata?.mcpTools || []
const availableMcpTools = mcpToolResponses.map((tr) => tr.tool)
const toolCalls = parseToolUse(message.content, availableMcpTools)
// 如果有工具调用,创建新的对话响应
if (toolCalls && toolCalls.length > 0) {
console.log('[MCP] 检测到工具调用,将创建全新的对话响应')
// 完成当前消息(工具调用消息)
message.status = 'success'
// 确保当前消息有正确的使用量统计
if (!message.usage || !message?.usage?.completion_tokens) {
message.usage = await estimateMessagesUsage({
assistant,
messages: [..._messages, message]
})
// 设置metrics.completion_tokens
if (message.metrics && message?.usage?.completion_tokens) {
message.metrics = {
...message.metrics,
completion_tokens: message.usage.completion_tokens
}
}
}
// 标记为包含工具调用的消息
// 不需要额外标记,已经有 mcpTools 字段
// 发送第一条完整消息到UI
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
onResponse(message)
// 执行工具调用
const toolResponses = []
await executeToolCalls(toolCalls, toolResponses, () => {}, 0)
// 工具调用已执行,不创建新的消息
// 重置生成状态
store.dispatch(setGenerating(false))
// 不生成第二条消息,只执行工具调用
console.log('[MCP] 工具调用已执行,不生成第二条消息')
return message
}
// 如果没有工具调用,正常处理消息
if (!message.usage || !message?.usage?.completion_tokens) {
message.usage = await estimateMessagesUsage({
assistant,
@ -340,7 +397,20 @@ export async function fetchChatCompletion({
}
}
// Emit chat completion event
// 如果不是工具调用相关消息,发送消息
if (!message.metadata?.isToolResultResponse && !message.metadata?.isToolResultQuery) {
// Emit chat completion event
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
onResponse(message)
} else {
// 如果是工具调用相关消息,只调用回调函数,不发送事件
// 因为我们已经在工具调用处理中发送了事件
onResponse(message)
}
// Always emit the final message state, including metadata, regardless of tool calls.
// The previous condition was likely for the old XML tool flow.
// For native function calls, the tool info is in the metadata of this single message.
EventEmitter.emit(EVENT_NAMES.RECEIVE_MESSAGE, message)
onResponse(message)
@ -422,17 +492,44 @@ export async function fetchGenerate({
content: string
modelId?: string
}): Promise<string> {
// 使用指定的模型或默认模型
const model = modelId
? store
.getState()
.llm.providers.flatMap((provider) => provider.models)
.find((m) => m.id === modelId)
: getDefaultModel()
// 处理JSON格式的模型ID
let parsedModelId = modelId
let providerId = undefined
if (typeof modelId === 'string' && modelId.startsWith('{')) {
try {
const parsedModel = JSON.parse(modelId)
parsedModelId = parsedModel.id
providerId = parsedModel.provider
console.log(`[fetchGenerate] Parsed model ID: ${parsedModelId}, provider: ${providerId}`)
} catch (error) {
console.error(`[fetchGenerate] Failed to parse model ID: ${modelId}`, error)
}
}
// 使用指定的模型或默认模型
let model: Model | undefined = undefined
// 如果有提供商ID先尝试从该提供商中查找模型
if (parsedModelId && providerId) {
const provider = store.getState().llm.providers.find((p) => p.id === providerId)
if (provider) {
model = provider.models.find((m) => m.id === parsedModelId)
}
}
// 如果没找到,尝试在所有模型中查找
if (!model && parsedModelId) {
model = store
.getState()
.llm.providers.flatMap((provider) => provider.models)
.find((m) => m.id === parsedModelId)
}
// 如果仍然没找到,使用默认模型
if (!model) {
console.error(`Model ${modelId} not found, using default model`)
return ''
model = getDefaultModel()
}
const provider = getProviderByModel(model)
@ -444,7 +541,13 @@ export async function fetchGenerate({
const AI = new AiProvider(provider)
try {
return await AI.generateText({ prompt, content, modelId })
// 使用模型的ID而不是原始的modelId
if (model) {
return await AI.generateText({ prompt, content, modelId: model.id })
} else {
console.error('No valid model found')
return ''
}
} catch (error: any) {
console.error('Error generating text:', error)
return ''

View File

@ -59,8 +59,13 @@ export const analyzeAndAddAssistantMemories = async (assistantId: string, messag
console.log(`[Assistant Memory Analysis] Analyzing assistant: ${assistantId}`)
console.log('[Assistant Memory Analysis] New conversation length:', newConversation.length)
// 从Redux状态中获取自定义提示词
const customAssistantPrompt = store.getState().memory?.assistantMemoryPrompt
// 构建助手记忆分析提示词
const prompt = `
const prompt =
customAssistantPrompt ||
`

View File

@ -279,8 +279,11 @@ class ContextualMemoryService {
// 构建对话内容
const conversation = recentMessages.map((msg) => `${msg.role || 'user'}: ${msg.content || ''}`).join('\n')
// 从Redux状态中获取自定义提示词
const customContextualPrompt = store.getState().memory?.contextualMemoryPrompt
// 构建提示词
const prompt = `
const prompt = customContextualPrompt || `
便

View File

@ -26,5 +26,7 @@ export const EVENT_NAMES = {
RESEND_MESSAGE: 'RESEND_MESSAGE',
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR',
QUOTE_TEXT: 'QUOTE_TEXT',
VOICE_CALL_MESSAGE: 'VOICE_CALL_MESSAGE'
VOICE_CALL_MESSAGE: 'VOICE_CALL_MESSAGE',
SET_CHAT_INPUT: 'SET_CHAT_INPUT', // Add new event for setting input value
SEND_FILE_ATTACHMENT: 'SEND_FILE_ATTACHMENT' // Add new event for sending file attachment
}

View File

@ -119,7 +119,10 @@ const analyzeNeedForHistoricalContext = async (
.map((memory) => `话题ID: ${memory.topicId}\n内容: ${memory.content}`)
.join('\n\n')
const prompt = `
// 从Redux状态中获取自定义提示词
const customHistoricalPrompt = store.getState().memory?.historicalContextPrompt
const prompt = customHistoricalPrompt || `
:

View File

@ -111,8 +111,13 @@ const analyzeConversation = async (
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
// 使用自定义提示词或默认提示词
// 从Redux状态中获取自定义提示词
const memoryState = store.getState().memory
const customLongTermPrompt = memoryState?.longTermMemoryPrompt
let basePrompt =
customPrompt ||
customLongTermPrompt ||
`
@ -423,8 +428,9 @@ export const useMemoryService = () => {
// 使用 useCallback 定义分析函数,但减少依赖项
// 增加可选的 topicId 参数,允许分析指定的话题
// 增加 isManualAnalysis 参数,标记是否是手动分析
const analyzeAndAddMemories = useCallback(
async (topicId?: string) => {
async (topicId?: string, isManualAnalysis: boolean = false) => {
// 如果没有提供话题ID则使用当前话题
// 在函数执行时获取最新状态
const currentState = store.getState() // Use imported store
@ -442,14 +448,17 @@ export const useMemoryService = () => {
}
}
// 重新检查条件
if (!memoryState.isActive || !memoryState.autoAnalyze || !memoryState.analyzeModel || memoryState.isAnalyzing) {
console.log('[Memory Analysis] Conditions not met or already analyzing at time of call:', {
isActive: memoryState.isActive,
autoAnalyze: memoryState.autoAnalyze,
analyzeModel: memoryState.analyzeModel,
isAnalyzing: memoryState.isAnalyzing
})
// 检查条件
// 手动分析时不检查 autoAnalyze 条件
const conditions = {
isActive: memoryState.isActive,
autoAnalyze: isManualAnalysis ? true : memoryState.autoAnalyze, // 手动分析时忽略自动分析设置
analyzeModel: memoryState.analyzeModel,
isAnalyzing: memoryState.isAnalyzing
}
if (!conditions.isActive || !conditions.autoAnalyze || !conditions.analyzeModel || conditions.isAnalyzing) {
console.log('[Memory Analysis] Conditions not met or already analyzing at time of call:', conditions)
return
}
@ -1231,8 +1240,13 @@ export const analyzeAndAddShortMemories = async (topicId: string) => {
// 获取当前的过滤敏感信息设置
const filterSensitiveInfo = store.getState().memory?.filterSensitiveInfo ?? true
// 从Redux状态中获取自定义提示词
const customShortTermPrompt = store.getState().memory?.shortTermMemoryPrompt
// 构建短期记忆分析提示词,包含已有记忆和新对话
let prompt = `
let prompt =
customShortTermPrompt ||
`
AI理解当前对话的完整上下文
@ -1816,6 +1830,14 @@ export const applyMemoriesToPrompt = async (systemPrompt: string, topicId?: stri
console.log('[Memory] No memories to apply')
}
// 添加工作区信息
try {
const { enhancePromptWithWorkspaceInfo } = await import('./WorkspaceAIService')
result = await enhancePromptWithWorkspaceInfo(result)
} catch (error) {
console.error('[Memory] Error adding workspace info:', error)
}
// 添加历史对话上下文
if (topicId) {
try {

View File

@ -195,6 +195,16 @@ export function getGroupedMessages(messages: Message[]): { [key: string]: (Messa
processedIds.add(message.id) // 标记此消息ID为已处理
// 如果是工具调用相关消息,不进行分组
if (message.metadata?.isToolResultQuery || message.metadata?.isToolResultResponse) {
const key = 'user' + message.id // 使用消息ID作为唯一键确保不会被分组
if (!groups[key]) {
groups[key] = []
}
groups[key].unshift({ ...message, index })
return
}
const key = message.askId ? 'assistant' + message.askId : 'user' + message.id
if (key && !groups[key]) {
groups[key] = []

View File

@ -0,0 +1,87 @@
import { selectCurrentWorkspace, selectVisibleToAIWorkspaces } from '@renderer/store/workspace'
import store from '@renderer/store'
import WorkspaceService from './WorkspaceService'
/**
*
* @returns
*/
export const getWorkspaceInfo = async (): Promise<string> => {
try {
// 获取当前活动的工作区
const activeWorkspace = selectCurrentWorkspace(store.getState())
// 获取对AI可见的工作区
const visibleWorkspaces = selectVisibleToAIWorkspaces(store.getState())
// 检查当前工作区是否对AI可见
if (!activeWorkspace || !visibleWorkspaces.some(w => w.id === activeWorkspace.id)) {
return ''
}
// 获取工作区文件夹结构(只获取根目录)
const folderStructure = await WorkspaceService.getWorkspaceFolderStructure(activeWorkspace.path, {
maxDepth: 1, // 只获取根目录下的文件和文件夹
lazyLoad: true // 使用懒加载模式
})
if (!folderStructure) {
return ''
}
// 构建文件结构信息
let workspaceInfo = `当前工作区: ${activeWorkspace.name}\n`
workspaceInfo += `工作区路径: ${activeWorkspace.path}\n\n`
workspaceInfo += `工作区文件结构:\n`
// 构建文件结构字符串(只处理根目录)
const buildStructureString = (node: any) => {
if (node.type === 'directory') {
workspaceInfo += `📁 ${node.name}/\n`
if (node.children && node.children.length > 0) {
// 按名称排序,先显示目录,再显示文件
const dirs = node.children.filter((child: any) => child.type === 'directory')
.sort((a: any, b: any) => a.name.localeCompare(b.name))
const files = node.children.filter((child: any) => child.type === 'file')
.sort((a: any, b: any) => a.name.localeCompare(b.name))
// 先列出目录
for (const dir of dirs) {
workspaceInfo += ` 📁 ${dir.name}/\n`
}
// 再列出文件
for (const file of files) {
workspaceInfo += ` 📄 ${file.name}\n`
}
}
}
}
// 开始构建结构字符串(只处理根目录)
buildStructureString(folderStructure)
return workspaceInfo
} catch (error) {
console.error('获取工作区信息失败:', error)
return ''
}
}
/**
*
* @param systemPrompt
* @returns
*/
export const enhancePromptWithWorkspaceInfo = async (systemPrompt: string): Promise<string> => {
const workspaceInfo = await getWorkspaceInfo()
if (!workspaceInfo) {
return systemPrompt
}
// 添加工作区信息到系统提示词
return `${systemPrompt}\n\n工作区信息:\n${workspaceInfo}\n\n请注意上面只显示了工作区根目录下的文件和文件夹。如果需要查看子目录或文件内容请使用相应的工具函数如 workspace_list_files 或 workspace_read_file。\n\n请在回答用户问题时考虑工作区中的文件结构和内容。`
}

View File

@ -0,0 +1,177 @@
import { Workspace } from '@renderer/store/workspace'
/**
* -
*/
class WorkspaceService {
/**
*
*/
public async selectWorkspaceFolder(): Promise<string | null> {
try {
// 尝试使用工作区专用的选择器
try {
console.log('尝试使用 workspace.selectFolder...')
const folderPath = await window.api.workspace.selectFolder()
console.log('工作区选择器结果:', folderPath)
return folderPath
} catch (workspaceError) {
console.error('工作区选择器失败,尝试使用文件选择器:', workspaceError)
// 如果工作区选择器失败,则使用文件选择器
const result = await window.api.file.selectFolder()
console.log('文件选择器结果:', result)
if (result && !result.canceled && result.filePaths && result.filePaths.length > 0) {
return result.filePaths[0]
}
}
return null
} catch (error) {
console.error('选择工作区文件夹失败:', error)
return null
}
}
/**
*
*/
public async getWorkspaceFiles(
workspacePath: string,
options: {
extensions?: string[]
excludePatterns?: string[]
maxDepth?: number
maxFiles?: number
} = {}
): Promise<
Array<{
path: string
fullPath: string
name: string
size: number
isDirectory: boolean
extension: string
modifiedTime: number
}>
> {
try {
console.log('获取工作区文件列表:', workspacePath, options)
// 使用 workspace API 获取文件列表
const result = await window.api.workspace.getFiles(workspacePath, options)
console.log('工作区文件列表结果:', result)
return result
} catch (error) {
console.error('获取工作区文件列表失败:', error)
return []
}
}
/**
*
*/
public async readWorkspaceFile(filePath: string): Promise<string> {
try {
// 使用现有的文件读取API
return await window.api.fs.read(filePath, 'utf-8')
} catch (error) {
console.error('读取工作区文件失败:', error)
return ''
}
}
/**
*
*/
public async getWorkspaceFolderStructure(
workspacePath: string,
options: {
maxDepth?: number
excludePatterns?: string[]
directoryPath?: string // 新增参数,指定要加载的目录路径
lazyLoad?: boolean // 新增参数,指定是否懒加载
} = {}
): Promise<any> {
try {
console.log('获取工作区文件夹结构:', workspacePath, options)
// 使用 workspace API 获取文件夹结构
const result = await window.api.workspace.getFolderStructure(workspacePath, options)
console.log('工作区文件夹结构结果:', result)
return result
} catch (error) {
console.error('获取工作区文件夹结构失败:', error)
// 如果失败,返回一个空的结构
return {
name: workspacePath.split(/[\\/]/).pop() || 'Workspace',
type: 'directory',
children: [],
path: ''
}
}
}
/**
*
*/
public async createWorkspace(name: string, path: string): Promise<Workspace> {
const workspace: Omit<Workspace, 'id' | 'createdAt' | 'updatedAt'> = {
name,
path
}
// 使用Redux action创建工作区
window.store.dispatch({ type: 'workspace/addWorkspace', payload: workspace })
// 获取新创建的工作区
const workspaces = window.store.getState().workspace.workspaces
return workspaces[workspaces.length - 1]
}
/**
*
*/
public setCurrentWorkspace(workspaceId: string | null): void {
window.store.dispatch({ type: 'workspace/setCurrentWorkspace', payload: workspaceId })
}
/**
*
*/
public getCurrentWorkspace(): Workspace | null {
const state = window.store.getState()
const { currentWorkspaceId, workspaces } = state.workspace
return currentWorkspaceId ? workspaces.find((w: Workspace) => w.id === currentWorkspaceId) || null : null
}
/**
*
*/
public getWorkspaces(): Workspace[] {
return window.store.getState().workspace.workspaces
}
/**
*
*/
public deleteWorkspace(workspaceId: string): void {
window.store.dispatch({ type: 'workspace/removeWorkspace', payload: workspaceId })
}
/**
*
*/
public updateWorkspace(workspaceId: string, workspace: Partial<Workspace>): void {
window.store.dispatch({
type: 'workspace/updateWorkspace',
payload: { id: workspaceId, workspace }
})
}
/**
*
*/
public initWorkspaces(): void {
window.store.dispatch({ type: 'workspace/initWorkspaces' })
}
}
export default new WorkspaceService()

View File

@ -12,6 +12,7 @@ import llm from './llm'
import mcp from './mcp'
import memory from './memory' // Removed import of memoryPersistenceMiddleware
import messagesReducer from './messages'
import workspace from './workspace'
import migrate from './migrate'
import minapps from './minapps'
import nutstore from './nutstore'
@ -37,6 +38,7 @@ const rootReducer = combineReducers({
mcp,
copilot,
memory,
workspace,
messages: messagesReducer
})

View File

@ -105,6 +105,17 @@ export const builtinMCPServers: MCPServer[] = [
description:
'自动记忆工具,功能跟上面的记忆工具差不多。这个记忆会自动应用到对话中,无需显式调用。适合记住用户偏好、项目背景等长期有用信息.可以跨对话。',
isActive: true
},
{
id: nanoid(),
name: '@cherry/workspacefile',
type: 'inMemory',
description:
'工作区文件操作工具,提供读取、写入、搜索、列出、创建和编辑工作区文件的功能。需要配置 WORKSPACE_PATH 环境变量。',
env: {
WORKSPACE_PATH: ''
},
isActive: false
}
]

View File

@ -121,6 +121,13 @@ export interface MemoryState {
lastAnalyzeTime: number | null // 上次分析时间
isAnalyzing: boolean // 是否正在分析
// 提示词相关
longTermMemoryPrompt: string | null // 长期记忆分析提示词
shortTermMemoryPrompt: string | null // 短期记忆分析提示词
assistantMemoryPrompt: string | null // 助手记忆分析提示词
contextualMemoryPrompt: string | null // 上下文记忆分析提示词
historicalContextPrompt: string | null // 历史对话上下文分析提示词
// 自适应分析相关
adaptiveAnalysisEnabled: boolean // 是否启用自适应分析
analysisFrequency: number // 分析频率(消息数)
@ -180,6 +187,13 @@ const initialState: MemoryState = {
lastAnalyzeTime: null,
isAnalyzing: false,
// 提示词相关 - 默认为null将在服务中使用默认提示词
longTermMemoryPrompt: null,
shortTermMemoryPrompt: null,
assistantMemoryPrompt: null,
contextualMemoryPrompt: null,
historicalContextPrompt: null,
// 自适应分析相关
adaptiveAnalysisEnabled: true, // 默认启用自适应分析
analysisFrequency: 5, // 默认每5条消息分析一次
@ -342,6 +356,31 @@ const memorySlice = createSlice({
state.vectorizeModel = action.payload
},
// 设置长期记忆分析提示词
setLongTermMemoryPrompt: (state, action: PayloadAction<string | null>) => {
state.longTermMemoryPrompt = action.payload
},
// 设置短期记忆分析提示词
setShortTermMemoryPrompt: (state, action: PayloadAction<string | null>) => {
state.shortTermMemoryPrompt = action.payload
},
// 设置助手记忆分析提示词
setAssistantMemoryPrompt: (state, action: PayloadAction<string | null>) => {
state.assistantMemoryPrompt = action.payload
},
// 设置上下文记忆分析提示词
setContextualMemoryPrompt: (state, action: PayloadAction<string | null>) => {
state.contextualMemoryPrompt = action.payload
},
// 设置历史对话上下文分析提示词
setHistoricalContextPrompt: (state, action: PayloadAction<string | null>) => {
state.historicalContextPrompt = action.payload
},
// 设置分析状态
setAnalyzing: (state, action: PayloadAction<boolean>) => {
state.isAnalyzing = action.payload
@ -1011,6 +1050,28 @@ const memorySlice = createSlice({
)
}
// 更新提示词状态
if (action.payload.longTermMemoryPrompt !== undefined) {
state.longTermMemoryPrompt = action.payload.longTermMemoryPrompt
console.log('[Memory Reducer] Loaded longTermMemoryPrompt')
}
if (action.payload.shortTermMemoryPrompt !== undefined) {
state.shortTermMemoryPrompt = action.payload.shortTermMemoryPrompt
console.log('[Memory Reducer] Loaded shortTermMemoryPrompt')
}
if (action.payload.assistantMemoryPrompt !== undefined) {
state.assistantMemoryPrompt = action.payload.assistantMemoryPrompt
console.log('[Memory Reducer] Loaded assistantMemoryPrompt')
}
if (action.payload.contextualMemoryPrompt !== undefined) {
state.contextualMemoryPrompt = action.payload.contextualMemoryPrompt
console.log('[Memory Reducer] Loaded contextualMemoryPrompt')
}
if (action.payload.historicalContextPrompt !== undefined) {
state.historicalContextPrompt = action.payload.historicalContextPrompt
console.log('[Memory Reducer] Loaded historicalContextPrompt')
}
console.log('Short-term memory data loaded into state')
}
})
@ -1081,6 +1142,11 @@ export const {
setHistoricalContextAnalyzeModel,
setVectorizeModel,
setAnalyzing,
setLongTermMemoryPrompt,
setShortTermMemoryPrompt,
setAssistantMemoryPrompt,
setContextualMemoryPrompt,
setHistoricalContextPrompt,
importMemories,
clearMemories,
addMemoryList,
@ -1158,87 +1224,14 @@ export const saveMemoryData = createAsyncThunk(
try {
console.log('[Memory] Saving memory data to file...', Object.keys(data))
// 如果是强制覆盖模式,直接使用传入的数据,不合并当前状态
if (forceOverwrite) {
console.log('[Memory] Force overwrite mode enabled, using provided data directly')
const result = await window.api.memory.saveData(memoryData, forceOverwrite)
console.log('[Memory] Memory data saved successfully (force overwrite)')
return result
// 直接将传入的部分数据发送给主进程,不再合并完整状态
console.log('[Memory] Sending partial memory data to main process:', memoryData)
const result = await window.api.memory.saveData(memoryData, forceOverwrite)
if (result) {
console.log('[Memory] Partial memory data saved successfully via main process')
} else {
console.error('[Memory] Main process failed to save partial memory data')
}
// 非强制覆盖模式,确保数据完整性
const state = store.getState().memory
// 保存所有设置,而不仅仅是特定字段
// 创建一个包含所有设置的对象
const completeData = {
// 基本设置
isActive: memoryData.isActive !== undefined ? memoryData.isActive : state.isActive,
shortMemoryActive:
memoryData.shortMemoryActive !== undefined ? memoryData.shortMemoryActive : state.shortMemoryActive,
autoAnalyze: memoryData.autoAnalyze !== undefined ? memoryData.autoAnalyze : state.autoAnalyze,
filterSensitiveInfo:
memoryData.filterSensitiveInfo !== undefined ? memoryData.filterSensitiveInfo : state.filterSensitiveInfo,
// 模型选择
analyzeModel: memoryData.analyzeModel || state.analyzeModel,
shortMemoryAnalyzeModel: memoryData.shortMemoryAnalyzeModel || state.shortMemoryAnalyzeModel,
assistantMemoryAnalyzeModel: memoryData.assistantMemoryAnalyzeModel || state.assistantMemoryAnalyzeModel,
historicalContextAnalyzeModel: memoryData.historicalContextAnalyzeModel || state.historicalContextAnalyzeModel,
vectorizeModel: memoryData.vectorizeModel || state.vectorizeModel,
// 记忆数据
memoryLists: memoryData.memoryLists || state.memoryLists,
shortMemories: memoryData.shortMemories || state.shortMemories,
assistantMemories: memoryData.assistantMemories || state.assistantMemories,
currentListId: memoryData.currentListId || state.currentListId,
// 自适应分析相关
adaptiveAnalysisEnabled:
memoryData.adaptiveAnalysisEnabled !== undefined
? memoryData.adaptiveAnalysisEnabled
: state.adaptiveAnalysisEnabled,
analysisFrequency:
memoryData.analysisFrequency !== undefined ? memoryData.analysisFrequency : state.analysisFrequency,
analysisDepth: memoryData.analysisDepth || state.analysisDepth,
// 用户关注点相关
interestTrackingEnabled:
memoryData.interestTrackingEnabled !== undefined
? memoryData.interestTrackingEnabled
: state.interestTrackingEnabled,
// 性能监控相关
monitoringEnabled:
memoryData.monitoringEnabled !== undefined ? memoryData.monitoringEnabled : state.monitoringEnabled,
// 智能优先级与时效性管理相关
priorityManagementEnabled:
memoryData.priorityManagementEnabled !== undefined
? memoryData.priorityManagementEnabled
: state.priorityManagementEnabled,
decayEnabled: memoryData.decayEnabled !== undefined ? memoryData.decayEnabled : state.decayEnabled,
freshnessEnabled:
memoryData.freshnessEnabled !== undefined ? memoryData.freshnessEnabled : state.freshnessEnabled,
decayRate: memoryData.decayRate !== undefined ? memoryData.decayRate : state.decayRate,
// 上下文感知记忆推荐相关
contextualRecommendationEnabled:
memoryData.contextualRecommendationEnabled !== undefined
? memoryData.contextualRecommendationEnabled
: state.contextualRecommendationEnabled,
autoRecommendMemories:
memoryData.autoRecommendMemories !== undefined
? memoryData.autoRecommendMemories
: state.autoRecommendMemories,
recommendationThreshold:
memoryData.recommendationThreshold !== undefined
? memoryData.recommendationThreshold
: state.recommendationThreshold
}
const result = await window.api.memory.saveData(completeData, forceOverwrite)
console.log('[Memory] Memory data saved successfully')
return result
} catch (error) {
console.error('[Memory] Failed to save memory data:', error)
@ -1291,6 +1284,10 @@ export const saveLongTermMemoryData = createAsyncThunk(
// 模型选择
analyzeModel: memoryData.analyzeModel || state.analyzeModel,
// 提示词相关
longTermMemoryPrompt:
memoryData.longTermMemoryPrompt !== undefined ? memoryData.longTermMemoryPrompt : state.longTermMemoryPrompt,
// 记忆数据
memoryLists: memoryData.memoryLists || state.memoryLists,
memories: memoryData.memories || state.memories,
@ -1370,6 +1367,13 @@ export const saveAllMemorySettings = createAsyncThunk('memory/saveAllSettings',
historicalContextAnalyzeModel: state.historicalContextAnalyzeModel,
vectorizeModel: state.vectorizeModel,
// 提示词相关
longTermMemoryPrompt: state.longTermMemoryPrompt,
shortTermMemoryPrompt: state.shortTermMemoryPrompt,
assistantMemoryPrompt: state.assistantMemoryPrompt,
contextualMemoryPrompt: state.contextualMemoryPrompt,
historicalContextPrompt: state.historicalContextPrompt,
// 记忆数据
assistantMemories: state.assistantMemories,

View File

@ -9,8 +9,7 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'translate',
'minapp',
'knowledge',
'files',
'projects'
'files'
]
export interface MinAppsState {

View File

@ -16,6 +16,7 @@ export type SidebarIcon =
| 'knowledge'
| 'files'
| 'projects'
| 'workspace'
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'assistants',
@ -24,7 +25,8 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'translate',
'minapp',
'knowledge',
'files'
'files',
'workspace'
]
export interface NutstoreSyncRuntime extends WebDAVSyncState {}

View File

@ -0,0 +1,191 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { RootState } from '@renderer/store'
import db from '@renderer/databases'
import { nanoid } from 'nanoid'
// 工作区类型定义
export interface Workspace {
id: string
name: string
path: string
createdAt: number
updatedAt: number
visibleToAI?: boolean // 是否对AI可见默认为true
}
// 工作区状态
interface WorkspaceState {
workspaces: Workspace[]
currentWorkspaceId: string | null
isLoading: boolean
error: string | null
}
// 初始状态
const initialState: WorkspaceState = {
workspaces: [],
currentWorkspaceId: null,
isLoading: false,
error: null
}
// 创建工作区 slice
const workspaceSlice = createSlice({
name: 'workspace',
initialState,
reducers: {
// 设置工作区列表
setWorkspaces: (state, action: PayloadAction<Workspace[]>) => {
state.workspaces = action.payload
},
// 添加工作区
addWorkspace: (state, action: PayloadAction<Omit<Workspace, 'id' | 'createdAt' | 'updatedAt' | 'visibleToAI'>>) => {
const newWorkspace: Workspace = {
id: nanoid(),
...action.payload,
createdAt: Date.now(),
updatedAt: Date.now(),
visibleToAI: true // 默认对AI可见
}
state.workspaces.push(newWorkspace)
// 保存到数据库
db.workspaces.add(newWorkspace)
},
// 更新工作区
updateWorkspace: (state, action: PayloadAction<{ id: string, workspace: Partial<Workspace> }>) => {
const { id, workspace } = action.payload
const index = state.workspaces.findIndex(w => w.id === id)
if (index !== -1) {
state.workspaces[index] = {
...state.workspaces[index],
...workspace,
updatedAt: Date.now()
}
// 更新数据库
db.workspaces.update(id, {
...workspace,
updatedAt: Date.now()
})
}
},
// 删除工作区
removeWorkspace: (state, action: PayloadAction<string>) => {
const id = action.payload
state.workspaces = state.workspaces.filter(w => w.id !== id)
// 如果删除的是当前工作区,重置当前工作区
if (state.currentWorkspaceId === id) {
state.currentWorkspaceId = state.workspaces.length > 0 ? state.workspaces[0].id : null
}
// 从数据库删除
db.workspaces.delete(id)
},
// 设置当前工作区
setCurrentWorkspace: (state, action: PayloadAction<string | null>) => {
state.currentWorkspaceId = action.payload
// 保存当前工作区ID到本地存储
if (action.payload) {
localStorage.setItem('currentWorkspaceId', action.payload)
} else {
localStorage.removeItem('currentWorkspaceId')
}
},
// 设置加载状态
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload
},
// 设置错误信息
setError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload
}
}
})
// 导出 actions
export const {
setWorkspaces,
addWorkspace,
updateWorkspace,
removeWorkspace,
setCurrentWorkspace,
setLoading,
setError
} = workspaceSlice.actions
// 选择器
export const selectWorkspaces = (state: RootState) => state.workspace.workspaces
export const selectCurrentWorkspaceId = (state: RootState) => state.workspace.currentWorkspaceId
export const selectCurrentWorkspace = (state: RootState) => {
const { currentWorkspaceId, workspaces } = state.workspace
return currentWorkspaceId ? workspaces.find(w => w.id === currentWorkspaceId) || null : null
}
export const selectIsLoading = (state: RootState) => state.workspace.isLoading
export const selectError = (state: RootState) => state.workspace.error
// 选择对AI可见的工作区
export const selectVisibleToAIWorkspaces = (state: RootState) => {
return state.workspace.workspaces.filter(w => w.visibleToAI !== false) // 如果visibleToAI为undefined或true则返回
}
// 导出 reducer
export default workspaceSlice.reducer
// 初始化工作区数据
export const initWorkspaces = () => async (dispatch: any) => {
try {
dispatch(setLoading(true))
// 从数据库加载工作区
let workspaces = await db.workspaces.toArray()
// 检查并设置默认的visibleToAI属性
let needsUpdate = false
workspaces = workspaces.map(workspace => {
if (workspace.visibleToAI === undefined) {
needsUpdate = true
// 默认只有第一个工作区对AI可见
const isFirstWorkspace = workspaces.length > 0 && workspace.id === workspaces[0].id
return {
...workspace,
visibleToAI: isFirstWorkspace
}
}
return workspace
})
// 如果有更新,则保存到数据库
if (needsUpdate) {
console.log('[Workspace] 更新工作区的visibleToAI属性')
for (const workspace of workspaces) {
await db.workspaces.update(workspace.id, { visibleToAI: workspace.visibleToAI })
}
}
dispatch(setWorkspaces(workspaces))
// 从本地存储获取当前工作区ID
const currentWorkspaceId = localStorage.getItem('currentWorkspaceId')
if (currentWorkspaceId && workspaces.some(w => w.id === currentWorkspaceId)) {
dispatch(setCurrentWorkspace(currentWorkspaceId))
} else if (workspaces.length > 0) {
dispatch(setCurrentWorkspace(workspaces[0].id))
}
dispatch(setLoading(false))
} catch (error) {
console.error('Failed to initialize workspaces:', error)
dispatch(setError('Failed to initialize workspaces'))
dispatch(setLoading(false))
}
}

View File

@ -74,6 +74,8 @@ export type Message = {
useful?: boolean
error?: Record<string, any>
enabledMCPs?: MCPServer[]
// 是否隐藏消息(用于系统消息)
isHidden?: boolean
// 引用消息
referencedMessages?: {
id: string
@ -98,6 +100,15 @@ export type Message = {
generateImage?: GenerateImageResponse
// Knowledge base results
knowledge?: KnowledgeReference[]
// 工具调用查询标记
isToolResultQuery?: boolean
// 工具调用响应标记
isToolResultResponse?: boolean
// 工具调用结果
toolResults?: {
toolId: string
response: MCPCallToolResponse
}[]
}
// 多模型消息样式
multiModelMessageStyle?: 'horizontal' | 'vertical' | 'fold' | 'grid'
@ -414,6 +425,7 @@ export interface MCPServer {
disabledTools?: string[] // List of tool names that are disabled for this server
configSample?: MCPConfigSample
headers?: Record<string, string> // Custom headers to be sent with requests to this server
searchKey?: string
}
export interface MCPToolInputSchema {
@ -431,6 +443,7 @@ export interface MCPTool {
name: string
description?: string
inputSchema: MCPToolInputSchema
toolKey: string // Add descriptive key: serverId-toolName
}
export interface MCPPromptArguments {
@ -468,6 +481,7 @@ export interface MCPConfig {
export interface MCPToolResponse {
id: string // tool call id, it should be unique
tool: MCPTool // tool info
args?: any // Actual arguments passed to the tool
status: string // 'invoking' | 'done'
response?: any
}

View File

@ -219,7 +219,9 @@ export function openAIToolsToMcpTool(
serverName: tool.serverName,
name: tool.name,
description: tool.description,
inputSchema: args
inputSchema: args,
// Add the missing toolKey property
toolKey: `${tool.serverId}-${tool.name}`
}
}
@ -296,7 +298,7 @@ export function mcpToolsToGeminiTools(mcpTools: MCPTool[] | undefined): geminiTo
? Object.fromEntries(
Object.entries(properties).map(([key, value]) => [key, ensureValidSchema(value as Record<string, any>)])
)
: { _empty: { type: SchemaType.STRING } as SimpleStringSchema }
: {} // Return empty object if no properties, instead of _empty placeholder
} as FunctionDeclarationSchema
}
functions.push(functionDeclaration)
@ -304,7 +306,8 @@ export function mcpToolsToGeminiTools(mcpTools: MCPTool[] | undefined): geminiTo
const tool: geminiTool = {
functionDeclarations: functions
}
return [tool]
// Return empty array if no functions, otherwise return the tool definition
return functions.length > 0 ? [tool] : []
}
export function geminiFunctionCallToMcpTool(
@ -374,10 +377,10 @@ export function parseToolUse(content: string, mcpTools: MCPTool[]): MCPToolRespo
/<tool_use>([\s\S]*?)<name>([\s\S]*?)<\/name>([\s\S]*?)<arguments>([\s\S]*?)<\/arguments>([\s\S]*?)<\/tool_use>/g
// 2. Roo Code格式: <工具名><参数名>参数值</参数名></工具名>
const rooCodeToolUsePattern = new RegExp(`<(${mcpTools.map((tool) => tool.id).join('|')})>([\s\S]*?)<\/\\1>`, 'g')
const rooCodeToolUsePattern = new RegExp(`<(${mcpTools.map((tool) => tool.id).join('|')})>([\\s\\S]*?)<\\/\\1>`, 'g') // Keep escapes needed for RegExp constructor
// 3. 简化格式: <tool_use>工具ID参数JSON</tool_use>
const simplifiedToolUsePattern = /<tool_use>\s*([\w\d]+)\s*([\s\S]*?)\s*<\/tool_use>/g
const simplifiedToolUsePattern = /<tool_use>\s*([\w\d]+)\s*([\s\S]*?)\s*<\/tool_use>/g // Remove unnecessary escapes: \s, \S, \/
const tools: MCPToolResponse[] = []
let idx = 0
@ -481,25 +484,18 @@ export function parseToolUse(content: string, mcpTools: MCPTool[]): MCPToolRespo
return tools
}
export async function parseAndCallTools(
content: string,
// 新增函数:执行工具调用并返回结果,但不转换为消息
export async function executeToolCalls(
tools: MCPToolResponse[],
toolResponses: MCPToolResponse[],
onChunk: CompletionsParams['onChunk'],
idx: number,
convertToMessage: (
toolCallId: string,
resp: MCPCallToolResponse,
isVisionModel: boolean
) => ChatCompletionMessageParam | MessageParam | Content,
mcpTools?: MCPTool[],
isVisionModel: boolean = false
): Promise<(ChatCompletionMessageParam | MessageParam | Content)[]> {
const toolResults: (ChatCompletionMessageParam | MessageParam | Content)[] = []
// process tool use
const tools = parseToolUse(content, mcpTools || [])
idx: number
): Promise<{ toolId: string; response: MCPCallToolResponse }[]> {
if (!tools || tools.length === 0) {
return toolResults
return []
}
// 标记所有工具为调用中
for (let i = 0; i < tools.length; i++) {
const tool = tools[i]
upsertMCPToolResponse(toolResponses, { id: `${tool.id}-${idx}-${i}`, tool: tool.tool, status: 'invoking' }, onChunk)
@ -528,10 +524,44 @@ export async function parseAndCallTools(
}
})
return convertToMessage(tool.tool.id, toolCallResponse, isVisionModel)
return {
toolId: tool.tool.id,
response: toolCallResponse
}
})
toolResults.push(...(await Promise.all(toolPromises)))
return await Promise.all(toolPromises)
}
// 修改后的函数:解析工具调用并执行,然后转换为消息
export async function parseAndCallTools(
content: string,
toolResponses: MCPToolResponse[],
onChunk: CompletionsParams['onChunk'],
idx: number,
convertToMessage: (
toolCallId: string,
resp: MCPCallToolResponse,
isVisionModel: boolean
) => ChatCompletionMessageParam | MessageParam | Content,
mcpTools?: MCPTool[],
isVisionModel: boolean = false
): Promise<(ChatCompletionMessageParam | MessageParam | Content)[]> {
const toolResults: (ChatCompletionMessageParam | MessageParam | Content)[] = []
// process tool use
const tools = parseToolUse(content, mcpTools || [])
if (!tools || tools.length === 0) {
return toolResults
}
// 执行工具调用
const toolCallResults = await executeToolCalls(tools, toolResponses, onChunk, idx)
// 转换工具调用结果为消息
for (const result of toolCallResults) {
toolResults.push(convertToMessage(result.toolId, result.response, isVisionModel))
}
return toolResults
}
@ -667,66 +697,38 @@ export function mcpToolCallResponseToAnthropicMessage(
message.content = content
}
return message
return message // This function seems unused in the new flow, keeping for potential reference
}
export function mcpToolCallResponseToGeminiMessage(
toolCallId: string,
resp: MCPCallToolResponse,
isVisionModel: boolean = false
): Content {
const message = {
role: 'user'
} as Content
/**
* Converts the response from an MCP tool call into a Gemini Part object
* suitable for sending back to the model as a function response.
* @param toolCallName The name/ID of the function that was called.
* @param resp The response object from the MCP tool call.
* @returns A Gemini Part object containing the function response.
*/
export function mcpToolCallResponseToGeminiFunctionResponsePart(toolCallName: string, resp: MCPCallToolResponse): Part {
// Serialize the response content.
// Join text parts, stringify others. Handle potential errors.
const responseContent = resp.content
.map((item) => {
if (item.type === 'text') return item.text
try {
return JSON.stringify(item) // Fallback for non-text parts
} catch (e) {
console.error('Error stringifying tool response part:', item, e)
return `[Error serializing ${item.type} part]`
}
})
.join('\n')
if (resp.isError) {
message.parts = [
{
text: JSON.stringify(resp.content)
return {
functionResponse: {
name: toolCallName,
response: {
// Pass the serialized content. Add error prefix if needed.
content: resp.isError ? `Error: ${responseContent}` : responseContent || '(empty response)'
}
]
} else {
const parts: Part[] = [
{
text: `Here is the result of tool call ${toolCallId}:`
}
]
if (isVisionModel) {
for (const item of resp.content) {
switch (item.type) {
case 'text':
parts.push({
text: item.text || 'no content'
})
break
case 'image':
if (!item.data) {
parts.push({
text: 'No image data provided'
})
} else {
parts.push({
inlineData: {
data: item.data,
mimeType: item.mimeType || 'image/png'
}
})
}
break
default:
parts.push({
text: `Unsupported type: ${item.type}`
})
break
}
}
} else {
parts.push({
text: JSON.stringify(resp.content)
})
}
message.parts = parts
}
return message
}

View File

@ -151,6 +151,27 @@ import { applyMemoriesToPrompt } from '@renderer/services/MemoryService'
import { MCPServer } from '@renderer/types'
import { getRememberedMemories } from './remember-utils'
// 添加强化工具使用的提示词
export const GEMINI_TOOL_PROMPT = `
使
1. 使使
2. 使
- 使 workspace_list_files
- 使 workspace_read_file
- 使 workspace_create_file workspace_write_file
- 使 workspace_search_files
3. workspace_list_files
4. 使
5.
使使
`
export const buildSystemPrompt = async (
userSystemPrompt: string,
tools: MCPTool[],
@ -185,16 +206,22 @@ export const buildSystemPrompt = async (
const enhancedPrompt = appMemoriesPrompt + (mcpMemoriesPrompt ? `\n\n${mcpMemoriesPrompt}` : '')
let finalPrompt: string
// When using native function calling (tools are present), the system prompt should only contain
// the user's instructions and any relevant memories. The model learns how to call tools
// from the 'tools' parameter in the API request, not from XML instructions in the prompt.
if (tools && tools.length > 0) {
console.log('[Prompt] Final prompt with tools:', { promptLength: enhancedPrompt.length })
// Break down the chained replace calls to potentially help the parser
const availableToolsString = AvailableTools(tools)
let tempPrompt = SYSTEM_PROMPT.replace('{{ USER_SYSTEM_PROMPT }}', enhancedPrompt)
tempPrompt = tempPrompt.replace('{{ TOOL_USE_EXAMPLES }}', ToolUseExamples)
finalPrompt = tempPrompt.replace('{{ AVAILABLE_TOOLS }}', availableToolsString)
console.log('[Prompt] Building prompt for native function calling:', { promptLength: enhancedPrompt.length })
// 添加强化工具使用的提示词
finalPrompt = GEMINI_TOOL_PROMPT + '\n\n' + enhancedPrompt
console.log('[Prompt] Added tool usage enhancement prompt')
} else {
console.log('[Prompt] Final prompt without tools:', { promptLength: enhancedPrompt.length })
finalPrompt = enhancedPrompt // Assign enhancedPrompt when no tools are present
console.log('[Prompt] Building prompt without tools (or for XML tool use):', {
promptLength: enhancedPrompt.length
})
// If no tools are provided (or if a model doesn't support native calls and relies on XML),
// we might still need the old SYSTEM_PROMPT logic. For now, assume no tools means no tool instructions needed.
// If XML fallback is needed later, this 'else' block might need to re-introduce SYSTEM_PROMPT.
finalPrompt = enhancedPrompt
}
// Single return point for the function
return finalPrompt

View File

@ -0,0 +1,34 @@
import { ThemeMode } from '@renderer/types'
import { MarkdownItShikiOptions, setupMarkdownIt } from '@shikijs/markdown-it'
import MarkdownIt from 'markdown-it'
import { BuiltinLanguage, BuiltinTheme, bundledLanguages, createHighlighter } from 'shiki'
const defaultOptions = {
themes: {
light: 'one-light',
dark: 'material-theme-darker'
},
defaultColor: 'light'
}
const initHighlighter = async (options: MarkdownItShikiOptions) => {
const themeNames = ('themes' in options ? Object.values(options.themes) : [options.theme]).filter(
Boolean
) as BuiltinTheme[]
return await createHighlighter({
themes: themeNames,
langs: options.langs || (Object.keys(bundledLanguages) as BuiltinLanguage[])
})
}
const highlighter = await initHighlighter(defaultOptions)
export function getShikiInstance(theme: ThemeMode) {
const options = {
...defaultOptions,
defaultColor: theme
}
return function (markdownit: MarkdownIt) {
setupMarkdownIt(markdownit, highlighter, options)
}
}

213
src/shared/IpcChannel.ts Normal file
View File

@ -0,0 +1,213 @@
export enum IpcChannel {
// App
App_Info = 'app:info',
App_Proxy = 'app:proxy',
App_Reload = 'app:reload',
App_ShowUpdateDialog = 'app:showUpdateDialog',
App_SetLanguage = 'app:setLanguage',
App_SetLaunchOnBoot = 'app:setLaunchOnBoot',
App_SetLaunchToTray = 'app:setLaunchToTray',
App_SetTray = 'app:setTray',
App_SetTrayOnClose = 'app:setTrayOnClose',
App_RestartTray = 'app:restartTray',
App_SetTheme = 'app:setTheme',
App_ClearCache = 'app:clearCache',
App_CheckForUpdate = 'app:checkForUpdate',
App_IsBinaryExist = 'app:isBinaryExist',
App_GetBinaryPath = 'app:getBinaryPath',
App_InstallUvBinary = 'app:installUvBinary',
App_InstallBunBinary = 'app:installBunBinary',
// Config
Config_Set = 'config:set',
Config_Get = 'config:get',
// Zip
Zip_Compress = 'zip:compress',
Zip_Decompress = 'zip:decompress',
// Backup
Backup_Backup = 'backup:backup',
Backup_Restore = 'backup:restore',
Backup_BackupToWebdav = 'backup:backupToWebdav',
Backup_RestoreFromWebdav = 'backup:restoreFromWebdav',
Backup_ListWebdavFiles = 'backup:listWebdavFiles',
Backup_CheckConnection = 'backup:checkConnection',
Backup_CreateDirectory = 'backup:createDirectory',
Backup_DeleteWebdavFile = 'backup:deleteWebdavFile',
// File
File_Open = 'file:open',
File_OpenPath = 'file:openPath',
File_Save = 'file:save',
File_Select = 'file:select',
File_Upload = 'file:upload',
File_Clear = 'file:clear',
File_Read = 'file:read',
File_Delete = 'file:delete',
File_Get = 'file:get',
File_SelectFolder = 'file:selectFolder',
File_Create = 'file:create',
File_Write = 'file:write',
File_SaveImage = 'file:saveImage',
File_Base64Image = 'file:base64Image',
File_Download = 'file:download',
File_Copy = 'file:copy',
File_BinaryFile = 'file:binaryFile',
// Fs
Fs_Read = 'fs:read',
// Export
Export_Word = 'export:word',
// Open
Open_Path = 'open:path',
Open_Website = 'open:website',
// Shortcuts
Shortcuts_Update = 'shortcuts:update',
// Knowledge Base
KnowledgeBase_Create = 'knowledgeBase:create',
KnowledgeBase_Reset = 'knowledgeBase:reset',
KnowledgeBase_Delete = 'knowledgeBase:delete',
KnowledgeBase_Add = 'knowledgeBase:add',
KnowledgeBase_Remove = 'knowledgeBase:remove',
KnowledgeBase_Search = 'knowledgeBase:search',
KnowledgeBase_Rerank = 'knowledgeBase:rerank',
// Windows
Windows_SetMinimumSize = 'windows:setMinimumSize',
Windows_ResetMinimumSize = 'windows:resetMinimumSize',
// Gemini
Gemini_UploadFile = 'gemini:uploadFile',
Gemini_Base64File = 'gemini:base64File',
Gemini_RetrieveFile = 'gemini:retrieveFile',
Gemini_ListFiles = 'gemini:listFiles',
Gemini_DeleteFile = 'gemini:deleteFile',
// Mini Window
MiniWindow_Show = 'miniWindow:show',
MiniWindow_Hide = 'miniWindow:hide',
MiniWindow_Close = 'miniWindow:close',
MiniWindow_Toggle = 'miniWindow:toggle',
MiniWindow_SetPin = 'miniWindow:setPin',
// AES
Aes_Encrypt = 'aes:encrypt',
Aes_Decrypt = 'aes:decrypt',
// MCP
Mcp_RemoveServer = 'mcp:removeServer',
Mcp_RestartServer = 'mcp:restartServer',
Mcp_StopServer = 'mcp:stopServer',
Mcp_ListTools = 'mcp:listTools',
Mcp_CallTool = 'mcp:callTool',
Mcp_ListPrompts = 'mcp:listPrompts',
Mcp_GetPrompt = 'mcp:getPrompt',
Mcp_ListResources = 'mcp:listResources',
Mcp_GetResource = 'mcp:getResource',
Mcp_GetInstallInfo = 'mcp:getInstallInfo',
Mcp_RerunTool = 'mcp:rerunTool',
// Copilot
Copilot_GetAuthMessage = 'copilot:getAuthMessage',
Copilot_GetCopilotToken = 'copilot:getCopilotToken',
Copilot_SaveCopilotToken = 'copilot:saveCopilotToken',
Copilot_GetToken = 'copilot:getToken',
Copilot_Logout = 'copilot:logout',
Copilot_GetUser = 'copilot:getUser',
// Obsidian
Obsidian_GetVaults = 'obsidian:getVaults',
Obsidian_GetFiles = 'obsidian:getFiles',
// Nutstore
Nutstore_GetSsoUrl = 'nutstore:getSsoUrl',
Nutstore_DecryptToken = 'nutstore:decryptToken',
Nutstore_GetDirectoryContents = 'nutstore:getDirectoryContents',
// Search Window
SearchWindow_Open = 'searchWindow:open',
SearchWindow_Close = 'searchWindow:close',
SearchWindow_OpenUrl = 'searchWindow:openUrl',
// Memory
Memory_LoadData = 'memory:loadData',
Memory_SaveData = 'memory:saveData',
Memory_DeleteShortMemoryById = 'memory:deleteShortMemoryById',
LongTermMemory_LoadData = 'longTermMemory:loadData',
LongTermMemory_SaveData = 'longTermMemory:saveData',
// ASR
ASR_Start = 'asr:start',
ASR_Stop = 'asr:stop',
ASR_GetStatus = 'asr:getStatus',
ASR_GetLanguages = 'asr:getLanguages',
ASR_SetLanguage = 'asr:setLanguage',
ASR_GetLanguage = 'asr:getLanguage',
ASR_GetModels = 'asr:getModels',
ASR_SetModel = 'asr:setModel',
ASR_GetModel = 'asr:getModel',
ASR_GetDevices = 'asr:getDevices',
ASR_SetDevice = 'asr:setDevice',
ASR_GetDevice = 'asr:getDevice',
ASR_GetServerStatus = 'asr:getServerStatus',
ASR_StartServer = 'asr:startServer',
ASR_StopServer = 'asr:stopServer',
ASR_GetServerInfo = 'asr:getServerInfo',
ASR_GetServerLogs = 'asr:getServerLogs',
ASR_ClearServerLogs = 'asr:clearServerLogs',
ASR_GetServerConfig = 'asr:getServerConfig',
ASR_SetServerConfig = 'asr:setServerConfig',
ASR_GetServerModels = 'asr:getServerModels',
ASR_DownloadServerModel = 'asr:downloadServerModel',
ASR_DeleteServerModel = 'asr:deleteServerModel',
ASR_GetServerModelDownloadStatus = 'asr:getServerModelDownloadStatus',
ASR_CancelServerModelDownload = 'asr:cancelServerModelDownload',
ASR_GetServerModelInfo = 'asr:getServerModelInfo',
ASR_GetServerModelList = 'asr:getServerModelList',
ASR_GetServerModelDownloadList = 'asr:getServerModelDownloadList',
ASR_GetServerModelDownloadInfo = 'asr:getServerModelDownloadInfo',
ASR_GetServerModelDownloadProgress = 'asr:getServerModelDownloadProgress',
ASR_GetServerModelDownloadSpeed = 'asr:getServerModelDownloadSpeed',
ASR_GetServerModelDownloadSize = 'asr:getServerModelDownloadSize',
ASR_GetServerModelDownloadTime = 'asr:getServerModelDownloadTime',
ASR_GetServerModelDownloadTimeLeft = 'asr:getServerModelDownloadTimeLeft',
ASR_GetServerModelDownloadPercentage = 'asr:getServerModelDownloadPercentage',
ASR_GetServerModelDownloadState = 'asr:getServerModelDownloadState',
ASR_GetServerModelDownloadError = 'asr:getServerModelDownloadError',
ASR_GetServerModelDownloadUrl = 'asr:getServerModelDownloadUrl',
ASR_GetServerModelDownloadPath = 'asr:getServerModelDownloadPath',
ASR_GetServerModelDownloadFileName = 'asr:getServerModelDownloadFileName',
ASR_GetServerModelDownloadFileSize = 'asr:getServerModelDownloadFileSize',
ASR_GetServerModelDownloadFileSizeFormatted = 'asr:getServerModelDownloadFileSizeFormatted',
ASR_GetServerModelDownloadFileSizeUnit = 'asr:getServerModelDownloadFileSizeUnit',
ASR_GetServerModelDownloadFileSizeValue = 'asr:getServerModelDownloadFileSizeValue',
ASR_GetServerModelDownloadFileSizeValueFormatted = 'asr:getServerModelDownloadFileSizeValueFormatted',
ASR_GetServerModelDownloadFileSizeValueUnit = 'asr:getServerModelDownloadFileSizeValueUnit',
// MsTTS
MsTTS_GetVoices = 'mstts:getVoices',
MsTTS_Synthesize = 'mstts:synthesize',
// CodeExecutor
CodeExecutor_GetSupportedLanguages = 'codeExecutor:getSupportedLanguages',
CodeExecutor_ExecuteJS = 'codeExecutor:executeJS',
CodeExecutor_ExecutePython = 'codeExecutor:executePython',
// PDF
PDF_SplitPDF = 'pdf:splitPDF',
PDF_GetPageCount = 'pdf:getPageCount',
// Theme
ThemeChange = 'theme:change',
// Workspace
Workspace_SelectFolder = 'workspace:selectFolder',
Workspace_GetFiles = 'workspace:getFiles',
Workspace_ReadFile = 'workspace:readFile',
Workspace_GetFolderStructure = 'workspace:getFolderStructure'
}

546
yarn.lock
View File

@ -470,6 +470,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.27.0
resolution: "@babel/runtime@npm:7.27.0"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/35091ea9de48bd7fd26fb177693d64f4d195eb58ab2b142b893b7f3fa0f1d7c677604d36499ae0621a3703f35ba0c6a8f6c572cc8f7dc0317213841e493cf663
languageName: node
linkType: hard
"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.9.2":
version: 7.26.10
resolution: "@babel/runtime@npm:7.26.10"
@ -479,15 +488,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.27.0
resolution: "@babel/runtime@npm:7.27.0"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/35091ea9de48bd7fd26fb177693d64f4d195eb58ab2b142b893b7f3fa0f1d7c677604d36499ae0621a3703f35ba0c6a8f6c572cc8f7dc0317213841e493cf663
languageName: node
linkType: hard
"@babel/template@npm:^7.26.9":
version: 7.26.9
resolution: "@babel/template@npm:7.26.9"
@ -3860,6 +3860,18 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/core@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/core@npm:3.2.2"
dependencies:
"@shikijs/types": "npm:3.2.2"
"@shikijs/vscode-textmate": "npm:^10.0.2"
"@types/hast": "npm:^3.0.4"
hast-util-to-html: "npm:^9.0.5"
checksum: 10c0/69afe788994653b69f1bafd4fd3c2143609b4b0c05e970c8dc8d82ec660d617850ad9eeb2b6aa5be2dd534cefa0213d577129cb9ae1070eb4890cbbf1ac0f63e
languageName: node
linkType: hard
"@shikijs/engine-javascript@npm:3.2.1":
version: 3.2.1
resolution: "@shikijs/engine-javascript@npm:3.2.1"
@ -3871,6 +3883,17 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/engine-javascript@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/engine-javascript@npm:3.2.2"
dependencies:
"@shikijs/types": "npm:3.2.2"
"@shikijs/vscode-textmate": "npm:^10.0.2"
oniguruma-to-es: "npm:^4.1.0"
checksum: 10c0/2db8f9c04cc8e40352eb69ddd4de81bda95c5318c897f43215622352c91591974522cefb3959bcfaa183fd36a18d1af9e704289ed7999273dcd763bfaa5a1827
languageName: node
linkType: hard
"@shikijs/engine-oniguruma@npm:3.2.1":
version: 3.2.1
resolution: "@shikijs/engine-oniguruma@npm:3.2.1"
@ -3881,6 +3904,16 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/engine-oniguruma@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/engine-oniguruma@npm:3.2.2"
dependencies:
"@shikijs/types": "npm:3.2.2"
"@shikijs/vscode-textmate": "npm:^10.0.2"
checksum: 10c0/b5eedfca26f7e1525fd079c1827ae9bdedafb574ce4eb535c54d484218b7428fb9ac93607f79a2adc1482483dd0366fdf07b0846403a76cd4767649adb8fa590
languageName: node
linkType: hard
"@shikijs/langs@npm:3.2.1":
version: 3.2.1
resolution: "@shikijs/langs@npm:3.2.1"
@ -3890,6 +3923,30 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/langs@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/langs@npm:3.2.2"
dependencies:
"@shikijs/types": "npm:3.2.2"
checksum: 10c0/04b5c9b92de9070624d24e20a2b3607edcbe4894a1db8056927f0d0637f080e2eed4e54925f0ded36874361db14bab9e4d9c2d06614ddd733f3f314250eabaf8
languageName: node
linkType: hard
"@shikijs/markdown-it@npm:^3.2.2":
version: 3.2.2
resolution: "@shikijs/markdown-it@npm:3.2.2"
dependencies:
markdown-it: "npm:^14.1.0"
shiki: "npm:3.2.2"
peerDependencies:
markdown-it-async: ^2.2.0
peerDependenciesMeta:
markdown-it-async:
optional: true
checksum: 10c0/37c98e45a0905ea58f605c7cd341c83f3289b6a37093862535c59f7cc178fe9bfb13413fea68d4d923341e51b2e5718fc5172147d15e07457a76aceed2ac1f95
languageName: node
linkType: hard
"@shikijs/themes@npm:3.2.1":
version: 3.2.1
resolution: "@shikijs/themes@npm:3.2.1"
@ -3899,6 +3956,15 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/themes@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/themes@npm:3.2.2"
dependencies:
"@shikijs/types": "npm:3.2.2"
checksum: 10c0/93745e76e7ed6cab1d797ec68b53a0a183d989201e5064b33a78b516e128848d2c9be194d29cf602d5017dc2a74013699c773d052aeb45593851ae35b035afaa
languageName: node
linkType: hard
"@shikijs/types@npm:3.2.1":
version: 3.2.1
resolution: "@shikijs/types@npm:3.2.1"
@ -3909,6 +3975,16 @@ __metadata:
languageName: node
linkType: hard
"@shikijs/types@npm:3.2.2":
version: 3.2.2
resolution: "@shikijs/types@npm:3.2.2"
dependencies:
"@shikijs/vscode-textmate": "npm:^10.0.2"
"@types/hast": "npm:^3.0.4"
checksum: 10c0/aec3327d0cfc89af138ce195ac070ba62d8229864c079a3f06dff5a180036fdd963282068d67bd4c89a04ae688005c2b7c214c274ad0bb265f6f7ab6907a67a6
languageName: node
linkType: hard
"@shikijs/vscode-textmate@npm:^10.0.2":
version: 10.0.2
resolution: "@shikijs/vscode-textmate@npm:10.0.2"
@ -4418,6 +4494,16 @@ __metadata:
languageName: node
linkType: hard
"@types/glob@npm:^8.1.0":
version: 8.1.0
resolution: "@types/glob@npm:8.1.0"
dependencies:
"@types/minimatch": "npm:^5.1.2"
"@types/node": "npm:*"
checksum: 10c0/ded07aa0d7a1caf3c47b85e262be82989ccd7933b4a14712b79c82fd45a239249811d9fc3a135b3e9457afa163e74a297033d7245b0dc63cd3d032f3906b053f
languageName: node
linkType: hard
"@types/hast@npm:^2.0.0":
version: 2.3.10
resolution: "@types/hast@npm:2.3.10"
@ -4548,6 +4634,13 @@ __metadata:
languageName: node
linkType: hard
"@types/minimatch@npm:^5.1.2":
version: 5.1.2
resolution: "@types/minimatch@npm:5.1.2"
checksum: 10c0/83cf1c11748891b714e129de0585af4c55dd4c2cafb1f1d5233d79246e5e1e19d1b5ad9e8db449667b3ffa2b6c80125c429dbee1054e9efb45758dbc4e118562
languageName: node
linkType: hard
"@types/ms@npm:*":
version: 2.1.0
resolution: "@types/ms@npm:2.1.0"
@ -4606,6 +4699,13 @@ __metadata:
languageName: node
linkType: hard
"@types/path-browserify@npm:^1.0.3":
version: 1.0.3
resolution: "@types/path-browserify@npm:1.0.3"
checksum: 10c0/6ed6de375c210dbc20d171ab547be6065ce9904463ca8ca63443a3b23efc7234ff5400d62be05b9be36fc2e863b77592c2e607896988ad39a45276b8a7e42d70
languageName: node
linkType: hard
"@types/plist@npm:^3.0.1":
version: 3.0.5
resolution: "@types/plist@npm:3.0.5"
@ -4616,6 +4716,13 @@ __metadata:
languageName: node
linkType: hard
"@types/prop-types@npm:*":
version: 15.7.14
resolution: "@types/prop-types@npm:15.7.14"
checksum: 10c0/1ec775160bfab90b67a782d735952158c7e702ca4502968aa82565bd8e452c2de8601c8dfe349733073c31179116cf7340710160d3836aa8a1ef76d1532893b1
languageName: node
linkType: hard
"@types/react-dom@npm:^19.0.4":
version: 19.0.4
resolution: "@types/react-dom@npm:19.0.4"
@ -4634,7 +4741,7 @@ __metadata:
languageName: node
linkType: hard
"@types/react-syntax-highlighter@npm:^15":
"@types/react-syntax-highlighter@npm:^15.5.13":
version: 15.5.13
resolution: "@types/react-syntax-highlighter@npm:15.5.13"
dependencies:
@ -4652,6 +4759,25 @@ __metadata:
languageName: node
linkType: hard
"@types/react-virtualized@npm:^9.22.2":
version: 9.22.2
resolution: "@types/react-virtualized@npm:9.22.2"
dependencies:
"@types/prop-types": "npm:*"
"@types/react": "npm:*"
checksum: 10c0/e46ae29ef3187d15c1a6693694e2612da0b6a7f259b12f86b58d150df02e5ca8bb4776cbe6059af5e0e6e48584e5fa10465e951662a3f74e47663a742f4d4d5d
languageName: node
linkType: hard
"@types/react-window@npm:^1":
version: 1.8.8
resolution: "@types/react-window@npm:1.8.8"
dependencies:
"@types/react": "npm:*"
checksum: 10c0/2170a3957752603e8b994840c5d31b72ddf94c427c0f42b0175b343cc54f50fe66161d8871e11786ec7a59906bd33861945579a3a8f745455a3744268ec1069f
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:^19.0.12":
version: 19.0.12
resolution: "@types/react@npm:19.0.12"
@ -4988,6 +5114,7 @@ __metadata:
"@mozilla/readability": "npm:^0.6.0"
"@notionhq/client": "npm:^2.2.15"
"@reduxjs/toolkit": "npm:^2.2.5"
"@shikijs/markdown-it": "npm:^3.2.2"
"@strongtz/win32-arm64-msvc": "npm:^0.4.7"
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
"@tryfabric/martian": "npm:^1.2.4"
@ -4995,16 +5122,20 @@ __metadata:
"@types/d3": "npm:^7"
"@types/diff": "npm:^7"
"@types/fs-extra": "npm:^11"
"@types/glob": "npm:^8.1.0"
"@types/lodash": "npm:^4.17.16"
"@types/markdown-it": "npm:^14"
"@types/md5": "npm:^2.3.5"
"@types/node": "npm:^18.19.9"
"@types/pako": "npm:^1.0.2"
"@types/path-browserify": "npm:^1.0.3"
"@types/react": "npm:^19.0.12"
"@types/react-dom": "npm:^19.0.4"
"@types/react-infinite-scroll-component": "npm:^5.0.0"
"@types/react-syntax-highlighter": "npm:^15"
"@types/react-syntax-highlighter": "npm:^15.5.13"
"@types/react-transition-group": "npm:^4.4.12"
"@types/react-virtualized": "npm:^9.22.2"
"@types/react-window": "npm:^1"
"@types/tinycolor2": "npm:^1"
"@vitejs/plugin-react": "npm:^4.3.4"
"@xyflow/react": "npm:^12.4.4"
@ -5045,24 +5176,29 @@ __metadata:
fast-xml-parser: "npm:^5.0.9"
fetch-socks: "npm:^1.3.2"
fs-extra: "npm:^11.2.0"
glob: "npm:^11.0.1"
got-scraping: "npm:^4.1.1"
html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7"
i18next: "npm:^23.11.5"
js-tiktoken: "npm:^1.0.19"
jsdom: "npm:^26.0.0"
less: "npm:^4.3.0"
lint-staged: "npm:^15.5.0"
lodash: "npm:^4.17.21"
lru-cache: "npm:^11.1.0"
lucide-react: "npm:^0.487.0"
markdown-it: "npm:^14.1.0"
mime: "npm:^4.0.4"
minimatch: "npm:^10.0.1"
monaco-editor: "npm:^0.52.2"
node-edge-tts: "npm:^1.2.8"
npx-scope-finder: "npm:^1.2.0"
officeparser: "npm:^4.1.1"
openai: "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch"
os-proxy-config: "npm:^1.1.1"
p-queue: "npm:^8.1.0"
path-browserify: "npm:^1.0.1"
pdf-lib: "npm:^1.17.1"
pdfjs-dist: "npm:^5.1.91"
prettier: "npm:^3.5.3"
@ -5080,6 +5216,9 @@ __metadata:
react-spinners: "npm:^0.14.1"
react-syntax-highlighter: "npm:^15.6.1"
react-transition-group: "npm:^4.4.5"
react-virtualized: "npm:^9.22.6"
react-vtree: "npm:^2.0.4"
react-window: "npm:^1.8.11"
redux: "npm:^5.0.1"
redux-persist: "npm:^6.0.0"
rehype-katex: "npm:^7.0.1"
@ -6504,6 +6643,13 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^1.0.4":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 10c0/34dead8bee24f5e96f6e7937d711978380647e936a22e76380290e35486afd8634966ce300fc4b74a32f3762c7d4c0303f442c3e259f4ce02374eb0c82834f27
languageName: node
linkType: hard
"code-point-at@npm:^1.0.0":
version: 1.1.0
resolution: "code-point-at@npm:1.1.0"
@ -6787,6 +6933,15 @@ __metadata:
languageName: node
linkType: hard
"copy-anything@npm:^2.0.1":
version: 2.0.6
resolution: "copy-anything@npm:2.0.6"
dependencies:
is-what: "npm:^3.14.1"
checksum: 10c0/2702998a8cc015f9917385b7f16b0d85f1f6e5e2fd34d99f14df584838f492f49aa0c390d973684c687e895c5c58d08b308a0400ac3e1e3d6fa1e5884a5402ad
languageName: node
linkType: hard
"copy-to-clipboard@npm:^3.3.3":
version: 3.3.3
resolution: "copy-to-clipboard@npm:3.3.3"
@ -7403,6 +7558,15 @@ __metadata:
languageName: node
linkType: hard
"decompress-response@npm:^4.2.0":
version: 4.2.1
resolution: "decompress-response@npm:4.2.1"
dependencies:
mimic-response: "npm:^2.0.0"
checksum: 10c0/5e4821be332e80e3639acee2441c41d245fc07ac3ee85a6f28893c10c079d66d9bf09e8d84bffeae5656a4625e09e9b93fb4a5705adbe6b07202eea64fae1c8d
languageName: node
linkType: hard
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@ -7799,7 +7963,7 @@ __metadata:
languageName: node
linkType: hard
"dom-helpers@npm:^5.0.1":
"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.3":
version: 5.2.1
resolution: "dom-helpers@npm:5.2.1"
dependencies:
@ -8297,6 +8461,17 @@ __metadata:
languageName: node
linkType: hard
"errno@npm:^0.1.1":
version: 0.1.8
resolution: "errno@npm:0.1.8"
dependencies:
prr: "npm:~1.0.1"
bin:
errno: cli.js
checksum: 10c0/83758951967ec57bf00b5f5b7dc797e6d65a6171e57ea57adcf1bd1a0b477fd9b5b35fae5be1ff18f4090ed156bce1db749fe7e317aac19d485a5d150f6a4936
languageName: node
linkType: hard
"error-ex@npm:^1.2.0":
version: 1.3.2
resolution: "error-ex@npm:1.3.2"
@ -9843,6 +10018,22 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^11.0.1":
version: 11.0.1
resolution: "glob@npm:11.0.1"
dependencies:
foreground-child: "npm:^3.1.0"
jackspeak: "npm:^4.0.1"
minimatch: "npm:^10.0.0"
minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^2.0.0"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/2b32588be52e9e90f914c7d8dec32f3144b81b84054b0f70e9adfebf37cd7014570489f2a79d21f7801b9a4bd4cca94f426966bfd00fb64a5b705cfe10da3a03
languageName: node
linkType: hard
"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@ -10675,7 +10866,7 @@ __metadata:
languageName: node
linkType: hard
"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2":
"iconv-lite@npm:0.6, iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
dependencies:
@ -10734,6 +10925,15 @@ __metadata:
languageName: node
linkType: hard
"image-size@npm:~0.5.0":
version: 0.5.5
resolution: "image-size@npm:0.5.5"
bin:
image-size: bin/image-size.js
checksum: 10c0/655204163af06732f483a9fe7cce9dff4a29b7b2e88f5c957a5852e8143fa750f5e54b1955a2ca83de99c5220dbd680002d0d4e09140b01433520f4d5a0b1f4c
languageName: node
linkType: hard
"immediate@npm:~3.0.5":
version: 3.0.6
resolution: "immediate@npm:3.0.6"
@ -11148,6 +11348,13 @@ __metadata:
languageName: node
linkType: hard
"is-what@npm:^3.14.1":
version: 3.14.1
resolution: "is-what@npm:3.14.1"
checksum: 10c0/4b770b85454c877b6929a84fd47c318e1f8c2ff70fd72fd625bc3fde8e0c18a6e57345b6e7aa1ee9fbd1c608d27cfe885df473036c5c2e40cd2187250804a2c7
languageName: node
linkType: hard
"is-wsl@npm:^2.2.0":
version: 2.2.0
resolution: "is-wsl@npm:2.2.0"
@ -11214,6 +11421,15 @@ __metadata:
languageName: node
linkType: hard
"jackspeak@npm:^4.0.1":
version: 4.1.0
resolution: "jackspeak@npm:4.1.0"
dependencies:
"@isaacs/cliui": "npm:^8.0.2"
checksum: 10c0/08a6a24a366c90b83aef3ad6ec41dcaaa65428ffab8d80bc7172add0fbb8b134a34f415ad288b2a6fbd406526e9a62abdb40ed4f399fbe00cb45c44056d4dce0
languageName: node
linkType: hard
"jake@npm:^10.8.5":
version: 10.9.2
resolution: "jake@npm:10.9.2"
@ -11694,6 +11910,41 @@ __metadata:
languageName: node
linkType: hard
"less@npm:^4.3.0":
version: 4.3.0
resolution: "less@npm:4.3.0"
dependencies:
copy-anything: "npm:^2.0.1"
errno: "npm:^0.1.1"
graceful-fs: "npm:^4.1.2"
image-size: "npm:~0.5.0"
make-dir: "npm:^2.1.0"
mime: "npm:^1.4.1"
needle: "npm:^3.1.0"
parse-node-version: "npm:^1.0.1"
source-map: "npm:~0.6.0"
tslib: "npm:^2.3.0"
dependenciesMeta:
errno:
optional: true
graceful-fs:
optional: true
image-size:
optional: true
make-dir:
optional: true
mime:
optional: true
needle:
optional: true
source-map:
optional: true
bin:
lessc: bin/lessc
checksum: 10c0/69a9260d4613387fd1f2da3b0904005423272c59f304b27475c145a62f6890be5d38729fd78ef2a65cba3d1d9b02760f309074cad0be4764252934ad04efb2ae
languageName: node
linkType: hard
"leven@npm:2.1.0":
version: 2.1.0
resolution: "leven@npm:2.1.0"
@ -12002,7 +12253,7 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^11.1.0":
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0":
version: 11.1.0
resolution: "lru-cache@npm:11.1.0"
checksum: 10c0/85c312f7113f65fae6a62de7985348649937eb34fb3d212811acbf6704dc322a421788aca253b62838f1f07049a84cc513d88f494e373d3756514ad263670a64
@ -12043,6 +12294,13 @@ __metadata:
languageName: node
linkType: hard
"mac-system-proxy@npm:^1.0.0":
version: 1.0.4
resolution: "mac-system-proxy@npm:1.0.4"
checksum: 10c0/5511658640b938f7ef99a42cd551b0143efd5a432caffbe4cb219ab7eaeeb79eb8786e830c73e2652d813d7af81d6aeb5bbb34e957e38754ff51f6a2e5797bad
languageName: node
linkType: hard
"magic-string@npm:^0.30.10":
version: 0.30.17
resolution: "magic-string@npm:0.30.17"
@ -12061,6 +12319,16 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^2.1.0":
version: 2.1.0
resolution: "make-dir@npm:2.1.0"
dependencies:
pify: "npm:^4.0.1"
semver: "npm:^5.6.0"
checksum: 10c0/ada869944d866229819735bee5548944caef560d7a8536ecbc6536edca28c72add47cc4f6fc39c54fb25d06b58da1f8994cf7d9df7dadea047064749efc085d8
languageName: node
linkType: hard
"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.2.1":
version: 10.2.1
resolution: "make-fetch-happen@npm:10.2.1"
@ -12534,6 +12802,13 @@ __metadata:
languageName: node
linkType: hard
"memoize-one@npm:>=3.1.1 <6":
version: 5.2.1
resolution: "memoize-one@npm:5.2.1"
checksum: 10c0/fd22dbe9a978a2b4f30d6a491fc02fb90792432ad0dab840dc96c1734d2bd7c9cdeb6a26130ec60507eb43230559523615873168bcbe8fafab221c30b11d54c1
languageName: node
linkType: hard
"memoize-one@npm:^6.0.0":
version: 6.0.0
resolution: "memoize-one@npm:6.0.0"
@ -13136,7 +13411,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:^1.3.4":
"mime@npm:^1.3.4, mime@npm:^1.4.1":
version: 1.6.0
resolution: "mime@npm:1.6.0"
bin:
@ -13198,6 +13473,13 @@ __metadata:
languageName: node
linkType: hard
"mimic-response@npm:^2.0.0":
version: 2.1.0
resolution: "mimic-response@npm:2.1.0"
checksum: 10c0/717475c840f20deca87a16cb2f7561f9115f5de225ea2377739e09890c81aec72f43c81fd4984650c4044e66be5a846fa7a517ac7908f01009e1e624e19864d5
languageName: node
linkType: hard
"mimic-response@npm:^3.1.0":
version: 3.1.0
resolution: "mimic-response@npm:3.1.0"
@ -13235,7 +13517,7 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^10.0.0":
"minimatch@npm:^10.0.0, minimatch@npm:^10.0.1":
version: 10.0.1
resolution: "minimatch@npm:10.0.1"
dependencies:
@ -13505,6 +13787,13 @@ __metadata:
languageName: node
linkType: hard
"napi-build-utils@npm:^1.0.1":
version: 1.0.2
resolution: "napi-build-utils@npm:1.0.2"
checksum: 10c0/37fd2cd0ff2ad20073ce78d83fd718a740d568b225924e753ae51cb69d68f330c80544d487e5e5bd18e28702ed2ca469c2424ad948becd1862c1b0209542b2e9
languageName: node
linkType: hard
"napi-build-utils@npm:^2.0.0":
version: 2.0.0
resolution: "napi-build-utils@npm:2.0.0"
@ -13532,6 +13821,18 @@ __metadata:
languageName: node
linkType: hard
"needle@npm:^3.1.0":
version: 3.3.1
resolution: "needle@npm:3.3.1"
dependencies:
iconv-lite: "npm:^0.6.3"
sax: "npm:^1.2.4"
bin:
needle: bin/needle
checksum: 10c0/233b9315d47b735867d03e7a018fb665ee6cacf3a83b991b19538019cf42b538a3e85ca745c840b4c5e9a0ffdca76472f941363bf7c166214ae8cbc650fd4d39
languageName: node
linkType: hard
"negotiator@npm:^0.6.3":
version: 0.6.4
resolution: "negotiator@npm:0.6.4"
@ -13560,6 +13861,15 @@ __metadata:
languageName: node
linkType: hard
"node-abi@npm:^2.7.0":
version: 2.30.1
resolution: "node-abi@npm:2.30.1"
dependencies:
semver: "npm:^5.4.1"
checksum: 10c0/baddd9799ae3f9ad085695cd6545438a76f5a8deb47976daf06b13ee90e9dab5463de145b703aca38a5afb627c038e85d5f8a4ba2f31ec678a68366cb6daf76f
languageName: node
linkType: hard
"node-abi@npm:^3.3.0, node-abi@npm:^3.45.0":
version: 3.74.0
resolution: "node-abi@npm:3.74.0"
@ -13578,6 +13888,15 @@ __metadata:
languageName: node
linkType: hard
"node-addon-api@npm:^3.1.0":
version: 3.2.1
resolution: "node-addon-api@npm:3.2.1"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/41f21c9d12318875a2c429befd06070ce367065a3ef02952cfd4ea17ef69fa14012732f510b82b226e99c254da8d671847ea018cad785f839a5366e02dd56302
languageName: node
linkType: hard
"node-addon-api@npm:^7.0.0":
version: 7.1.1
resolution: "node-addon-api@npm:7.1.1"
@ -13696,6 +14015,13 @@ __metadata:
languageName: node
linkType: hard
"noop-logger@npm:^0.1.1":
version: 0.1.1
resolution: "noop-logger@npm:0.1.1"
checksum: 10c0/7319a3f1dcfaca9d066e1786ad13c2209891226e29dc4a4ce6dbaafb0cc6efeeb8dc78414fcf630d7f4f3964a5a69cda8b815a165d034f2de9142632b8c8ac20
languageName: node
linkType: hard
"nopt@npm:^4.0.1":
version: 4.0.3
resolution: "nopt@npm:4.0.3"
@ -13781,7 +14107,7 @@ __metadata:
languageName: node
linkType: hard
"npmlog@npm:^4.0.2":
"npmlog@npm:^4.0.1, npmlog@npm:^4.0.2":
version: 4.1.2
resolution: "npmlog@npm:4.1.2"
dependencies:
@ -14113,6 +14439,16 @@ __metadata:
languageName: node
linkType: hard
"os-proxy-config@npm:^1.1.1":
version: 1.1.1
resolution: "os-proxy-config@npm:1.1.1"
dependencies:
mac-system-proxy: "npm:^1.0.0"
windows-system-proxy: "npm:^1.0.0"
checksum: 10c0/87f493e73e3daa91c908d7a9d7271e768ca3f3267adc77a2423bc71af536ec3ee78d16b8e34e647bae1225893c0e0ccf30be8d0b3e7a6d2bf58876c5a701813d
languageName: node
linkType: hard
"os-tmpdir@npm:^1.0.0":
version: 1.0.2
resolution: "os-tmpdir@npm:1.0.2"
@ -14421,6 +14757,13 @@ __metadata:
languageName: node
linkType: hard
"parse-node-version@npm:^1.0.1":
version: 1.0.1
resolution: "parse-node-version@npm:1.0.1"
checksum: 10c0/999cd3d7da1425c2e182dce82b226c6dc842562d3ed79ec47f5c719c32a7f6c1a5352495b894fc25df164be7f2ede4224758255da9902ddef81f2b77ba46bb2c
languageName: node
linkType: hard
"parse5@npm:^7.0.0, parse5@npm:^7.2.1":
version: 7.2.1
resolution: "parse5@npm:7.2.1"
@ -14447,6 +14790,13 @@ __metadata:
languageName: node
linkType: hard
"path-browserify@npm:^1.0.1":
version: 1.0.1
resolution: "path-browserify@npm:1.0.1"
checksum: 10c0/8b8c3fd5c66bd340272180590ae4ff139769e9ab79522e2eb82e3d571a89b8117c04147f65ad066dccfb42fcad902e5b7d794b3d35e0fd840491a8ddbedf8c66
languageName: node
linkType: hard
"path-exists@npm:^2.0.0":
version: 2.1.0
resolution: "path-exists@npm:2.1.0"
@ -14515,6 +14865,16 @@ __metadata:
languageName: node
linkType: hard
"path-scurry@npm:^2.0.0":
version: 2.0.0
resolution: "path-scurry@npm:2.0.0"
dependencies:
lru-cache: "npm:^11.0.0"
minipass: "npm:^7.1.2"
checksum: 10c0/3da4adedaa8e7ef8d6dc4f35a0ff8f05a9b4d8365f2b28047752b62d4c1ad73eec21e37b1579ef2d075920157856a3b52ae8309c480a6f1a8bbe06ff8e52b33c
languageName: node
linkType: hard
"path-to-regexp@npm:^8.0.0":
version: 8.2.0
resolution: "path-to-regexp@npm:8.2.0"
@ -14718,6 +15078,13 @@ __metadata:
languageName: node
linkType: hard
"pify@npm:^4.0.1":
version: 4.0.1
resolution: "pify@npm:4.0.1"
checksum: 10c0/6f9d404b0d47a965437403c9b90eca8bb2536407f03de165940e62e72c8c8b75adda5516c6b9b23675a5877cc0bcac6bdfb0ef0e39414cd2476d5495da40e7cf
languageName: node
linkType: hard
"pinkie-promise@npm:^2.0.0":
version: 2.0.1
resolution: "pinkie-promise@npm:2.0.1"
@ -14822,6 +15189,31 @@ __metadata:
languageName: node
linkType: hard
"prebuild-install@npm:^5.3.5":
version: 5.3.6
resolution: "prebuild-install@npm:5.3.6"
dependencies:
detect-libc: "npm:^1.0.3"
expand-template: "npm:^2.0.3"
github-from-package: "npm:0.0.0"
minimist: "npm:^1.2.3"
mkdirp-classic: "npm:^0.5.3"
napi-build-utils: "npm:^1.0.1"
node-abi: "npm:^2.7.0"
noop-logger: "npm:^0.1.1"
npmlog: "npm:^4.0.1"
pump: "npm:^3.0.0"
rc: "npm:^1.2.7"
simple-get: "npm:^3.0.3"
tar-fs: "npm:^2.0.0"
tunnel-agent: "npm:^0.6.0"
which-pm-runs: "npm:^1.0.0"
bin:
prebuild-install: bin.js
checksum: 10c0/e24b7ea6c12fffdccad3fa40bb999277216081cbf96b768b34417aec773e9df2242df22c4529d47263713a1f02db16f5a89e909b24020c47232fe98b7efb7037
languageName: node
linkType: hard
"prebuild-install@npm:^7.1.1":
version: 7.1.3
resolution: "prebuild-install@npm:7.1.3"
@ -14942,7 +15334,7 @@ __metadata:
languageName: node
linkType: hard
"prop-types@npm:^15.6.2":
"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@ -15009,6 +15401,13 @@ __metadata:
languageName: node
linkType: hard
"prr@npm:~1.0.1":
version: 1.0.1
resolution: "prr@npm:1.0.1"
checksum: 10c0/5b9272c602e4f4472a215e58daff88f802923b84bc39c8860376bb1c0e42aaf18c25d69ad974bd06ec6db6f544b783edecd5502cd3d184748d99080d68e4be5f
languageName: node
linkType: hard
"psl@npm:^1.1.28":
version: 1.15.0
resolution: "psl@npm:1.15.0"
@ -15766,6 +16165,13 @@ __metadata:
languageName: node
linkType: hard
"react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"
checksum: 10c0/1d0df3c85af79df720524780f00c064d53a9dd1899d785eddb7264b378026979acbddb58a4b7e06e7d0d12aa1494fd5754562ee55d32907b15601068dae82c27
languageName: node
linkType: hard
"react-markdown@npm:^9.0.1":
version: 9.1.0
resolution: "react-markdown@npm:9.1.0"
@ -15911,6 +16317,50 @@ __metadata:
languageName: node
linkType: hard
"react-virtualized@npm:^9.22.6":
version: 9.22.6
resolution: "react-virtualized@npm:9.22.6"
dependencies:
"@babel/runtime": "npm:^7.7.2"
clsx: "npm:^1.0.4"
dom-helpers: "npm:^5.1.3"
loose-envify: "npm:^1.4.0"
prop-types: "npm:^15.7.2"
react-lifecycles-compat: "npm:^3.0.4"
peerDependencies:
react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/0c4fbe86e0c121adcdb7a3f322601eee4661afe65e31ef767c6d876016b1e7043fdad7998b4fa0252eaf73ffb6c14effcf0f729d154cd15304a8b15ad42b7b06
languageName: node
linkType: hard
"react-vtree@npm:^2.0.4":
version: 2.0.4
resolution: "react-vtree@npm:2.0.4"
dependencies:
"@babel/runtime": "npm:^7.11.0"
peerDependencies:
"@types/react-window": ^1.8.2
react: ^16.13.1
react-dom: ^16.13.1
react-window: ^1.8.5
checksum: 10c0/4dd7ef3d4d7fed701d02bbdf7acbad7b7a0fa6e88e7e4d01b37a6db2b40547b5154ef46638d5ed9c99d87812724a64af49c7dffa60eebcdba9cbcf5db6a23869
languageName: node
linkType: hard
"react-window@npm:^1.8.11":
version: 1.8.11
resolution: "react-window@npm:1.8.11"
dependencies:
"@babel/runtime": "npm:^7.0.0"
memoize-one: "npm:>=3.1.1 <6"
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/5ae8da1bc5c47d8f0a428b28a600256e2db511975573e52cb65a9b27ed1a0e5b9f7b3bee5a54fb0da93956d782c24010be434be451072f46ba5a89159d2b3944
languageName: node
linkType: hard
"react@npm:^19.0.0":
version: 19.0.0
resolution: "react@npm:19.0.0"
@ -16089,6 +16539,17 @@ __metadata:
languageName: node
linkType: hard
"registry-js@npm:^1.15.1":
version: 1.16.0
resolution: "registry-js@npm:1.16.0"
dependencies:
node-addon-api: "npm:^3.1.0"
node-gyp: "npm:latest"
prebuild-install: "npm:^5.3.5"
checksum: 10c0/654e9e780648da40099f198f1ea1ddcdecb65c5162103c86b27c021a5e3ee66a6141f0209eb7054b5832bc7f34502d6f28f9ce419fabf434452604d207f36be4
languageName: node
linkType: hard
"rehype-katex@npm:^7.0.1":
version: 7.0.1
resolution: "rehype-katex@npm:7.0.1"
@ -16747,7 +17208,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.5.0":
"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.3.0, semver@npm:^5.4.1, semver@npm:^5.5.0, semver@npm:^5.6.0":
version: 5.7.2
resolution: "semver@npm:5.7.2"
bin:
@ -16871,6 +17332,22 @@ __metadata:
languageName: node
linkType: hard
"shiki@npm:3.2.2":
version: 3.2.2
resolution: "shiki@npm:3.2.2"
dependencies:
"@shikijs/core": "npm:3.2.2"
"@shikijs/engine-javascript": "npm:3.2.2"
"@shikijs/engine-oniguruma": "npm:3.2.2"
"@shikijs/langs": "npm:3.2.2"
"@shikijs/themes": "npm:3.2.2"
"@shikijs/types": "npm:3.2.2"
"@shikijs/vscode-textmate": "npm:^10.0.2"
"@types/hast": "npm:^3.0.4"
checksum: 10c0/0183f889029ff1d14f79aa34e36f1e5a67b667661422f8a7de8936164099827588df7b2b4ed6835ad2eb3efb11ea882b4cb8022550503108c958a796df01f35c
languageName: node
linkType: hard
"shiki@npm:^3.2.1":
version: 3.2.1
resolution: "shiki@npm:3.2.1"
@ -16956,6 +17433,17 @@ __metadata:
languageName: node
linkType: hard
"simple-get@npm:^3.0.3":
version: 3.1.1
resolution: "simple-get@npm:3.1.1"
dependencies:
decompress-response: "npm:^4.2.0"
once: "npm:^1.3.1"
simple-concat: "npm:^1.0.0"
checksum: 10c0/438c78844ea1b1e7268d13ee0b3a39c7d644183367aec916aed3b676b45d3037a61d9f975c200a49b42eb851f29f03745118af1e13c01e60a7b4044f2fd60be7
languageName: node
linkType: hard
"simple-get@npm:^4.0.0":
version: 4.0.1
resolution: "simple-get@npm:4.0.1"
@ -17089,7 +17577,7 @@ __metadata:
languageName: node
linkType: hard
"source-map@npm:^0.6.0, source-map@npm:~0.6.1":
"source-map@npm:^0.6.0, source-map@npm:~0.6.0, source-map@npm:~0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011
@ -17964,7 +18452,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2":
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@ -18759,6 +19247,13 @@ __metadata:
languageName: node
linkType: hard
"which-pm-runs@npm:^1.0.0":
version: 1.1.0
resolution: "which-pm-runs@npm:1.1.0"
checksum: 10c0/b8f2f230aa49babe21cb93f169f5da13937f940b8cc7a47d2078d9d200950c0dba5ac5659bc01bdbe401e6db3adec6a97b6115215a4ca8e87fd714aebd0cabc6
languageName: node
linkType: hard
"which@npm:^1.2.10":
version: 1.3.1
resolution: "which@npm:1.3.1"
@ -18797,6 +19292,15 @@ __metadata:
languageName: node
linkType: hard
"windows-system-proxy@npm:^1.0.0":
version: 1.0.0
resolution: "windows-system-proxy@npm:1.0.0"
dependencies:
registry-js: "npm:^1.15.1"
checksum: 10c0/f3712938ce0786c359f171c40cc40df37448e06feeaaddd056312dcc93caff02c1eac9df6f0fac162ec99ac1f23dffb70bf19fe08fac097fed3c32b7ae95f6aa
languageName: node
linkType: hard
"word-wrap@npm:^1.2.5":
version: 1.2.5
resolution: "word-wrap@npm:1.2.5"

1
新建文本文档.txt Normal file
View File

@ -0,0 +1 @@
我爱世界