mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
更新
This commit is contained in:
parent
607cded6c9
commit
4dde843ef4
15
package.json
15
package.json
@ -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",
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
39
project_documentation/00_索引.md
Normal file
39
project_documentation/00_索引.md
Normal 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解决方案
|
||||
112
project_documentation/01_核心架构与配置.md
Normal file
112
project_documentation/01_核心架构与配置.md
Normal 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文档加载器
|
||||
89
project_documentation/02_AI助手与对话功能.md
Normal file
89
project_documentation/02_AI助手与对话功能.md
Normal 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交互体验,包括文本对话、语音交互、记忆功能和结构化思考等高级特性。
|
||||
63
project_documentation/03_知识库管理.md
Normal file
63
project_documentation/03_知识库管理.md
Normal 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与知识库的深度集成。
|
||||
45
project_documentation/04_工作区功能.md
Normal file
45
project_documentation/04_工作区功能.md
Normal 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助手的帮助下高效处理工作区中的文件和代码。
|
||||
49
project_documentation/05_翻译功能.md
Normal file
49
project_documentation/05_翻译功能.md
Normal 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模型深度集成,实现高质量的翻译结果。
|
||||
49
project_documentation/06_绘画功能.md
Normal file
49
project_documentation/06_绘画功能.md
Normal 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图像生成能力,支持多种模型和参数设置,实现高质量的图像创作。
|
||||
58
project_documentation/07_文件管理.md
Normal file
58
project_documentation/07_文件管理.md
Normal 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的文件管理功能,提供了全面的文件上传、存储、查看和导出能力,支持多种文件格式和云存储集成,为用户提供便捷的文件管理体验
|
||||
73
project_documentation/08_用户界面与组件.md
Normal file
73
project_documentation/08_用户界面与组件.md
Normal 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组件和页面,实现了美观、易用的用户界面,支持多种主题和样式定制。
|
||||
75
project_documentation/09_数据库与存储.md
Normal file
75
project_documentation/09_数据库与存储.md
Normal 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的数据库与存储系统,提供了可靠的数据持久化、状态管理和同步功能,确保用户数据的安全存储和高效访问。
|
||||
81
project_documentation/10_工具与实用功能.md
Normal file
81
project_documentation/10_工具与实用功能.md
Normal 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的工具与实用功能,提供了丰富的辅助工具和实用功能,增强了应用的使用体验和功能性。
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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}`)
|
||||
}
|
||||
|
||||
523
src/main/mcpServers/workspacefile.ts
Normal file
523
src/main/mcpServers/workspacefile.ts
Normal 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)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
250
src/main/services/WorkspaceService.ts
Normal file
250
src/main/services/WorkspaceService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/main/services/registerMCPHandlers.ts
Normal file
34
src/main/services/registerMCPHandlers.ts
Normal 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
32
src/main/utils/logger.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
10
src/preload/index.d.ts
vendored
10
src/preload/index.d.ts
vendored
@ -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 }>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
152
src/renderer/src/components/ChatWorkspacePanel/index.tsx
Normal file
152
src/renderer/src/components/ChatWorkspacePanel/index.tsx
Normal 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
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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;
|
||||
92
src/renderer/src/components/WorkspaceExplorer/index.css
Normal file
92
src/renderer/src/components/WorkspaceExplorer/index.css
Normal 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);
|
||||
}
|
||||
370
src/renderer/src/components/WorkspaceExplorer/index.tsx
Normal file
370
src/renderer/src/components/WorkspaceExplorer/index.tsx
Normal 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
|
||||
397
src/renderer/src/components/WorkspaceFileViewer/index.tsx
Normal file
397
src/renderer/src/components/WorkspaceFileViewer/index.tsx
Normal 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
|
||||
18
src/renderer/src/components/WorkspaceInitializer.tsx
Normal file
18
src/renderer/src/components/WorkspaceInitializer.tsx
Normal 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
|
||||
248
src/renderer/src/components/WorkspaceSelector/index.tsx
Normal file
248
src/renderer/src/components/WorkspaceSelector/index.tsx
Normal 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
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
})
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
9
src/renderer/src/hooks/useTheme.ts
Normal file
9
src/renderer/src/hooks/useTheme.ts
Normal 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 }
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -1121,6 +1121,7 @@
|
||||
"installHelp": "インストールヘルプを取得",
|
||||
"tabs": {
|
||||
"general": "一般",
|
||||
"description": "説明",
|
||||
"tools": "ツール",
|
||||
"prompts": "プロンプト",
|
||||
"resources": "リソース"
|
||||
|
||||
@ -1124,6 +1124,7 @@
|
||||
"installHelp": "Получить помощь по установке",
|
||||
"tabs": {
|
||||
"general": "Общие",
|
||||
"description": "Описание",
|
||||
"tools": "Инструменты",
|
||||
"prompts": "Подсказки",
|
||||
"resources": "Ресурсы"
|
||||
|
||||
@ -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": "包管理源",
|
||||
|
||||
@ -1121,6 +1121,7 @@
|
||||
"installHelp": "獲取安裝幫助",
|
||||
"tabs": {
|
||||
"general": "通用",
|
||||
"description": "描述",
|
||||
"tools": "工具",
|
||||
"prompts": "提示",
|
||||
"resources": "資源"
|
||||
|
||||
31
src/renderer/src/locales/en/workspace.json
Normal file
31
src/renderer/src/locales/en/workspace.json
Normal 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"
|
||||
}
|
||||
35
src/renderer/src/locales/zh-CN/workspace.json
Normal file
35
src/renderer/src/locales/zh-CN/workspace.json
Normal 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隐藏"
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 包装组件,避免不必要的重渲染
|
||||
|
||||
@ -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
|
||||
@ -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(
|
||||
{
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -211,6 +211,7 @@ const NpxSearch: FC<{
|
||||
env: record.configSample?.env,
|
||||
isActive: false,
|
||||
type: record.type,
|
||||
searchKey: record.fullName,
|
||||
configSample: record.configSample
|
||||
}
|
||||
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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. 记录对话中提到的文件、路径、变量名等具体技术细节
|
||||
|
||||
与长期记忆不同,短期记忆应该非常详细地关注当前对话的具体细节和上下文。每条短期记忆应该是对对话片段的精准总结,确保不遗漏任何重要信息。
|
||||
|
||||
请注意,对于长对话(超过5万字),您应该生成至少15-20条详细的记忆条目,确保完整捕捉对话的所有重要方面。对于超长对话(超过8万字),应生成至少20-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
|
||||
@ -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}`)
|
||||
|
||||
@ -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: (
|
||||
|
||||
107
src/renderer/src/pages/workspace/index.tsx
Normal file
107
src/renderer/src/pages/workspace/index.tsx
Normal 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
|
||||
@ -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?
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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 ''
|
||||
|
||||
@ -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 ||
|
||||
`
|
||||
请分析以下对话内容,提取对助手需要长期记住的重要信息。这些信息将作为助手的记忆,帮助助手在未来的对话中更好地理解用户和提供个性化服务。
|
||||
|
||||
请注意以下几点:
|
||||
|
||||
@ -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 || `
|
||||
请分析以下对话内容,提取出关键信息和主题,以便我可以找到相关的记忆。
|
||||
|
||||
请提供:
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 || `
|
||||
你是一个专门分析对话上下文的助手,你的任务是判断当前对话是否需要引用历史对话来提供更完整、更连贯的回答。
|
||||
|
||||
最近的对话内容:
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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] = []
|
||||
|
||||
87
src/renderer/src/services/WorkspaceAIService.ts
Normal file
87
src/renderer/src/services/WorkspaceAIService.ts
Normal 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请在回答用户问题时,考虑工作区中的文件结构和内容。`
|
||||
}
|
||||
177
src/renderer/src/services/WorkspaceService.ts
Normal file
177
src/renderer/src/services/WorkspaceService.ts
Normal 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()
|
||||
@ -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
|
||||
})
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -9,8 +9,7 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
|
||||
'translate',
|
||||
'minapp',
|
||||
'knowledge',
|
||||
'files',
|
||||
'projects'
|
||||
'files'
|
||||
]
|
||||
|
||||
export interface MinAppsState {
|
||||
|
||||
@ -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 {}
|
||||
|
||||
191
src/renderer/src/store/workspace.ts
Normal file
191
src/renderer/src/store/workspace.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
34
src/renderer/src/utils/shiki.ts
Normal file
34
src/renderer/src/utils/shiki.ts
Normal 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
213
src/shared/IpcChannel.ts
Normal 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
546
yarn.lock
@ -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
1
新建文本文档.txt
Normal file
@ -0,0 +1 @@
|
||||
我爱世界
|
||||
Loading…
Reference in New Issue
Block a user