From 4dde843ef4784c1ff496c5d8466d854dffa25e7d Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Tue, 22 Apr 2025 11:44:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 15 +- packages/shared/IpcChannel.ts | 6 +- project_documentation/00_索引.md | 39 ++ project_documentation/01_核心架构与配置.md | 112 ++++ project_documentation/02_AI助手与对话功能.md | 89 +++ project_documentation/03_知识库管理.md | 63 ++ project_documentation/04_工作区功能.md | 45 ++ project_documentation/05_翻译功能.md | 49 ++ project_documentation/06_绘画功能.md | 49 ++ project_documentation/07_文件管理.md | 58 ++ project_documentation/08_用户界面与组件.md | 73 +++ project_documentation/09_数据库与存储.md | 75 +++ project_documentation/10_工具与实用功能.md | 81 +++ src/main/index.ts | 11 +- src/main/ipc.ts | 23 +- src/main/mcpServers/factory.ts | 33 +- src/main/mcpServers/workspacefile.ts | 523 +++++++++++++++++ src/main/services/MCPService.ts | 99 +++- src/main/services/MemoryFileService.ts | 26 +- src/main/services/ProxyManager.ts | 16 +- src/main/services/WorkspaceService.ts | 250 ++++++++ src/main/services/registerMCPHandlers.ts | 34 ++ src/main/utils/logger.ts | 32 + src/preload/index.d.ts | 10 +- src/preload/index.ts | 58 +- src/renderer/src/App.tsx | 4 + .../components/ChatWorkspacePanel/index.tsx | 152 +++++ .../src/components/MemoryProvider.tsx | 15 +- .../MinApp/MinappPopupContainer.tsx | 4 +- .../components/Popups/ShortMemoryPopup.tsx | 19 +- .../SimpleVirtualizedExplorer.tsx | 316 ++++++++++ .../components/WorkspaceExplorer/index.css | 92 +++ .../components/WorkspaceExplorer/index.tsx | 370 ++++++++++++ .../components/WorkspaceFileViewer/index.tsx | 397 +++++++++++++ .../src/components/WorkspaceInitializer.tsx | 18 + .../components/WorkspaceSelector/index.tsx | 248 ++++++++ src/renderer/src/components/app/Sidebar.tsx | 9 +- src/renderer/src/databases/index.ts | 12 + src/renderer/src/hooks/useAppInit.ts | 2 +- .../src/hooks/useNavBackgroundColor.ts | 8 - src/renderer/src/hooks/useTheme.ts | 9 + src/renderer/src/i18n/locales/en-us.json | 73 +++ src/renderer/src/i18n/locales/ja-jp.json | 1 + src/renderer/src/i18n/locales/ru-ru.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 83 ++- src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/locales/en/workspace.json | 31 + src/renderer/src/locales/zh-CN/workspace.json | 35 ++ src/renderer/src/pages/home/Chat.tsx | 44 +- .../src/pages/home/Inputbar/Inputbar.tsx | 60 +- .../src/pages/home/Markdown/Markdown.tsx | 173 +----- .../home/Messages/MessageAttachments.tsx | 117 ++-- .../pages/home/Messages/MessageContent.tsx | 3 + .../src/pages/home/Messages/MessageTools.tsx | 417 +++++++++---- .../src/pages/home/Messages/Messages.tsx | 19 + .../home/Messages/ToolResponseContent.tsx | 228 ++++++-- .../settings/MCPSettings/McpDescription.tsx | 50 ++ .../settings/MCPSettings/McpSettings.tsx | 95 ++- .../pages/settings/MCPSettings/McpTool.tsx | 3 +- .../pages/settings/MCPSettings/NpxSearch.tsx | 1 + .../CollapsibleShortMemoryManager.tsx | 18 +- .../MemorySettings/PromptSettings.tsx | 391 +++++++++++++ .../MemorySettings/ShortMemoryManager.tsx | 19 +- .../pages/settings/MemorySettings/index.tsx | 19 +- src/renderer/src/pages/workspace/index.tsx | 107 ++++ .../providers/AiProvider/GeminiProvider.ts | 233 ++++++-- .../providers/AiProvider/OpenAIProvider.ts | 35 +- src/renderer/src/services/ApiService.ts | 133 ++++- .../src/services/AssistantMemoryService.ts | 7 +- .../src/services/ContextualMemoryService.ts | 5 +- src/renderer/src/services/EventService.ts | 4 +- .../src/services/HistoricalContextService.ts | 5 +- src/renderer/src/services/MemoryService.ts | 42 +- src/renderer/src/services/MessagesService.ts | 10 + .../src/services/WorkspaceAIService.ts | 87 +++ src/renderer/src/services/WorkspaceService.ts | 177 ++++++ src/renderer/src/store/index.ts | 2 + src/renderer/src/store/mcp.ts | 11 + src/renderer/src/store/memory.ts | 164 +++--- src/renderer/src/store/minapps.ts | 3 +- src/renderer/src/store/settings.ts | 4 +- src/renderer/src/store/workspace.ts | 191 ++++++ src/renderer/src/types/index.ts | 14 + src/renderer/src/utils/mcp-tools.ts | 158 ++--- src/renderer/src/utils/prompt.ts | 43 +- src/renderer/src/utils/shiki.ts | 34 ++ src/shared/IpcChannel.ts | 213 +++++++ yarn.lock | 546 +++++++++++++++++- 新建文本文档.txt | 1 + 89 files changed, 6814 insertions(+), 841 deletions(-) create mode 100644 project_documentation/00_索引.md create mode 100644 project_documentation/01_核心架构与配置.md create mode 100644 project_documentation/02_AI助手与对话功能.md create mode 100644 project_documentation/03_知识库管理.md create mode 100644 project_documentation/04_工作区功能.md create mode 100644 project_documentation/05_翻译功能.md create mode 100644 project_documentation/06_绘画功能.md create mode 100644 project_documentation/07_文件管理.md create mode 100644 project_documentation/08_用户界面与组件.md create mode 100644 project_documentation/09_数据库与存储.md create mode 100644 project_documentation/10_工具与实用功能.md create mode 100644 src/main/mcpServers/workspacefile.ts create mode 100644 src/main/services/WorkspaceService.ts create mode 100644 src/main/services/registerMCPHandlers.ts create mode 100644 src/main/utils/logger.ts create mode 100644 src/renderer/src/components/ChatWorkspacePanel/index.tsx create mode 100644 src/renderer/src/components/WorkspaceExplorer/SimpleVirtualizedExplorer.tsx create mode 100644 src/renderer/src/components/WorkspaceExplorer/index.css create mode 100644 src/renderer/src/components/WorkspaceExplorer/index.tsx create mode 100644 src/renderer/src/components/WorkspaceFileViewer/index.tsx create mode 100644 src/renderer/src/components/WorkspaceInitializer.tsx create mode 100644 src/renderer/src/components/WorkspaceSelector/index.tsx create mode 100644 src/renderer/src/hooks/useTheme.ts create mode 100644 src/renderer/src/locales/en/workspace.json create mode 100644 src/renderer/src/locales/zh-CN/workspace.json create mode 100644 src/renderer/src/pages/settings/MCPSettings/McpDescription.tsx create mode 100644 src/renderer/src/pages/settings/MemorySettings/PromptSettings.tsx create mode 100644 src/renderer/src/pages/workspace/index.tsx create mode 100644 src/renderer/src/services/WorkspaceAIService.ts create mode 100644 src/renderer/src/services/WorkspaceService.ts create mode 100644 src/renderer/src/store/workspace.ts create mode 100644 src/renderer/src/utils/shiki.ts create mode 100644 src/shared/IpcChannel.ts create mode 100644 新建文本文档.txt diff --git a/package.json b/package.json index 87ded6c80a..479a955aad 100644 --- a/package.json +++ b/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", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index a45a5c15be..8f179ca781 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -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' } diff --git a/project_documentation/00_索引.md b/project_documentation/00_索引.md new file mode 100644 index 0000000000..f1aecff714 --- /dev/null +++ b/project_documentation/00_索引.md @@ -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解决方案 diff --git a/project_documentation/01_核心架构与配置.md b/project_documentation/01_核心架构与配置.md new file mode 100644 index 0000000000..e989fc0498 --- /dev/null +++ b/project_documentation/01_核心架构与配置.md @@ -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文档加载器 diff --git a/project_documentation/02_AI助手与对话功能.md b/project_documentation/02_AI助手与对话功能.md new file mode 100644 index 0000000000..c5ff085e14 --- /dev/null +++ b/project_documentation/02_AI助手与对话功能.md @@ -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交互体验,包括文本对话、语音交互、记忆功能和结构化思考等高级特性。 \ No newline at end of file diff --git a/project_documentation/03_知识库管理.md b/project_documentation/03_知识库管理.md new file mode 100644 index 0000000000..857d781c0f --- /dev/null +++ b/project_documentation/03_知识库管理.md @@ -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与知识库的深度集成。 \ No newline at end of file diff --git a/project_documentation/04_工作区功能.md b/project_documentation/04_工作区功能.md new file mode 100644 index 0000000000..ce1837a070 --- /dev/null +++ b/project_documentation/04_工作区功能.md @@ -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助手的帮助下高效处理工作区中的文件和代码。 \ No newline at end of file diff --git a/project_documentation/05_翻译功能.md b/project_documentation/05_翻译功能.md new file mode 100644 index 0000000000..df1046dd43 --- /dev/null +++ b/project_documentation/05_翻译功能.md @@ -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模型深度集成,实现高质量的翻译结果。 \ No newline at end of file diff --git a/project_documentation/06_绘画功能.md b/project_documentation/06_绘画功能.md new file mode 100644 index 0000000000..b179a2b00a --- /dev/null +++ b/project_documentation/06_绘画功能.md @@ -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图像生成能力,支持多种模型和参数设置,实现高质量的图像创作。 \ No newline at end of file diff --git a/project_documentation/07_文件管理.md b/project_documentation/07_文件管理.md new file mode 100644 index 0000000000..90021a208c --- /dev/null +++ b/project_documentation/07_文件管理.md @@ -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的文件管理功能,提供了全面的文件上传、存储、查看和导出能力,支持多种文件格式和云存储集成,为用户提供便捷的文件管理体验 \ No newline at end of file diff --git a/project_documentation/08_用户界面与组件.md b/project_documentation/08_用户界面与组件.md new file mode 100644 index 0000000000..6e517623a2 --- /dev/null +++ b/project_documentation/08_用户界面与组件.md @@ -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组件和页面,实现了美观、易用的用户界面,支持多种主题和样式定制。 \ No newline at end of file diff --git a/project_documentation/09_数据库与存储.md b/project_documentation/09_数据库与存储.md new file mode 100644 index 0000000000..9aa647c176 --- /dev/null +++ b/project_documentation/09_数据库与存储.md @@ -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的数据库与存储系统,提供了可靠的数据持久化、状态管理和同步功能,确保用户数据的安全存储和高效访问。 \ No newline at end of file diff --git a/project_documentation/10_工具与实用功能.md b/project_documentation/10_工具与实用功能.md new file mode 100644 index 0000000000..08f1f0bde4 --- /dev/null +++ b/project_documentation/10_工具与实用功能.md @@ -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的工具与实用功能,提供了丰富的辅助工具和实用功能,增强了应用的使用体验和功能性。 \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index bf6c96559b..a1b8365041 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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) diff --git a/src/main/ipc.ts b/src/main/ipc.ts index ec7d7b5cd4..0881589c33 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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 + ) => 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) } diff --git a/src/main/mcpServers/factory.ts b/src/main/mcpServers/factory.ts index d35770d48d..560e24731f 100644 --- a/src/main/mcpServers/factory.ts +++ b/src/main/mcpServers/factory.ts @@ -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 = {}): Server { +export async function createInMemoryMCPServer( + name: string, + args: string[] = [], + envs: Record = {} +): Promise { 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}`) } diff --git a/src/main/mcpServers/workspacefile.ts b/src/main/mcpServers/workspacefile.ts new file mode 100644 index 0000000000..3088422831 --- /dev/null +++ b/src/main/mcpServers/workspacefile.ts @@ -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 { + // 增加日志输出,便于调试 + 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 + +// 工具实现 + +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 { + 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 { + 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)}` + ) + } + }) + } +} diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index dbc3425718..14aecc7017 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -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 + ): Promise => { + // 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 => { return new Promise((resolve, reject) => { let command: string diff --git a/src/main/services/MemoryFileService.ts b/src/main/services/MemoryFileService.ts index 51cdc4a2cb..e71a73d6d3 100644 --- a/src/main/services/MemoryFileService.ts +++ b/src/main/services/MemoryFileService.ts @@ -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)) diff --git a/src/main/services/ProxyManager.ts b/src/main/services/ProxyManager.ts index 64df333337..a349600163 100644 --- a/src/main/services/ProxyManager.ts +++ b/src/main/services/ProxyManager.ts @@ -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 { 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 diff --git a/src/main/services/WorkspaceService.ts b/src/main/services/WorkspaceService.ts new file mode 100644 index 0000000000..f482bf556c --- /dev/null +++ b/src/main/services/WorkspaceService.ts @@ -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 { + 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 => { + 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 + } + } +} diff --git a/src/main/services/registerMCPHandlers.ts b/src/main/services/registerMCPHandlers.ts new file mode 100644 index 0000000000..2218871636 --- /dev/null +++ b/src/main/services/registerMCPHandlers.ts @@ -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); +} diff --git a/src/main/utils/logger.ts b/src/main/utils/logger.ts new file mode 100644 index 0000000000..1cb05e5b7a --- /dev/null +++ b/src/main/utils/logger.ts @@ -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) + } +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index eee4505844..23c085fae8 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -56,7 +56,7 @@ declare global { read: (fileId: string) => Promise clear: () => Promise get: (filePath: string) => Promise - selectFolder: () => Promise + selectFolder: () => Promise<{ canceled: boolean; filePaths: string[] }> create: (fileName: string) => Promise write: (filePath: string, data: Uint8Array | string) => Promise 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 + read: (path: string, encoding?: BufferEncoding) => Promise } export: { toWord: (markdown: string, fileName: string) => Promise @@ -209,6 +209,12 @@ declare global { loadLongTermData: () => Promise saveLongTermData: (data: any, forceOverwrite?: boolean) => Promise } + workspace: { + selectFolder: () => Promise + getFiles: (workspacePath: string, options: any) => Promise + readFile: (filePath: string) => Promise + getFolderStructure: (workspacePath: string, options: any) => Promise + } asrServer: { startServer: () => Promise<{ success: boolean; pid?: number; port?: number; error?: string }> stopServer: (pid: number) => Promise<{ success: boolean; error?: string }> diff --git a/src/preload/index.ts b/src/preload/index.ts index ec9bda3c8d..442bce5306 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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 + ) => 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 // 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 + 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) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index b178e5ec6e..539b3e8a38 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 { + @@ -47,6 +50,7 @@ function App(): React.ReactElement { } /> } /> } /> + } /> } /> diff --git a/src/renderer/src/components/ChatWorkspacePanel/index.tsx b/src/renderer/src/components/ChatWorkspacePanel/index.tsx new file mode 100644 index 0000000000..4984030377 --- /dev/null +++ b/src/renderer/src/components/ChatWorkspacePanel/index.tsx @@ -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 = ({ + 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 ( + { + onClose() + setSelectedFile(null) + }} + open={visible} + styles={{ + header: { marginTop: '40px' }, + body: { padding: 0, height: 'calc(100% - 95px)', overflow: 'hidden' } + }} + closable={false} + destroyOnClose> + {selectedFile ? ( + <> + + } onClick={handleCloseViewer} /> + {t('workspace.fileViewer')} + + + + ) : ( + + + + + + + + + + )} + + ) +} + +export default ChatWorkspacePanel diff --git a/src/renderer/src/components/MemoryProvider.tsx b/src/renderer/src/components/MemoryProvider.tsx index 5787cbe1f4..54b3e6103d 100644 --- a/src/renderer/src/components/MemoryProvider.tsx +++ b/src/renderer/src/components/MemoryProvider.tsx @@ -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 = ({ 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(null) diff --git a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx index 3e68943d70..2f3607474b 100644 --- a/src/renderer/src/components/MinApp/MinappPopupContainer.tsx +++ b/src/renderer/src/components/MinApp/MinappPopupContainer.tsx @@ -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 ( - + diff --git a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx index 504648c4ae..619db9ae1d 100644 --- a/src/renderer/src/components/Popups/ShortMemoryPopup.tsx +++ b/src/renderer/src/components/Popups/ShortMemoryPopup.tsx @@ -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 = ({ 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}`) diff --git a/src/renderer/src/components/WorkspaceExplorer/SimpleVirtualizedExplorer.tsx b/src/renderer/src/components/WorkspaceExplorer/SimpleVirtualizedExplorer.tsx new file mode 100644 index 0000000000..fedc868ee6 --- /dev/null +++ b/src/renderer/src/components/WorkspaceExplorer/SimpleVirtualizedExplorer.tsx @@ -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 = ({ onFileSelect }) => { + const { t } = useTranslation(); + const [files, setFiles] = useState([]); + 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 ( +
handleFileClick(file)}> + {file.isDirectory ? ( + + + + + {file.name} + + ) : ( + + + + + {file.name} + + )} +
+ ); + }; + + if (!currentWorkspace) { + return ( + + + + ); + } + + return ( + + + {t('workspace.explorer')} + loadFiles(currentPath)}> + + + + + + {breadcrumbs.map((item, index) => ( + + {index > 0 && /} + handleBreadcrumbClick(item.path)}> + {item.name} + + + ))} + + + + {loading ? ( + + + + ) : files.length > 0 ? ( + + {({ width, height }) => ( + + )} + + ) : ( + + + + )} + + + ); +}; + +export default SimpleVirtualizedExplorer; diff --git a/src/renderer/src/components/WorkspaceExplorer/index.css b/src/renderer/src/components/WorkspaceExplorer/index.css new file mode 100644 index 0000000000..fe704827ce --- /dev/null +++ b/src/renderer/src/components/WorkspaceExplorer/index.css @@ -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); +} diff --git a/src/renderer/src/components/WorkspaceExplorer/index.tsx b/src/renderer/src/components/WorkspaceExplorer/index.tsx new file mode 100644 index 0000000000..eb0f628b75 --- /dev/null +++ b/src/renderer/src/components/WorkspaceExplorer/index.tsx @@ -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([]) + const [expandedKeys, setExpandedKeys] = useState([]) + 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) => { + const { value } = e.target + setSearchValue(value) + + if (!value) { + setExpandedKeys([]) + setAutoExpandParent(false) + return + } + + // 查找匹配的节点并展开其父节点 + const expandedKeysSet = new Set() + + 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 ( + + + + + {nodeData.title} + + ) + } + + return ( + + + {t('workspace.explorer')} + + + + + + + + + + } + onChange={handleSearch} + value={searchValue} + allowClear + /> + + + + {loading ? ( + + + {t('common.loading')} + + + ) : currentWorkspace ? ( + useVirtualized ? ( + // 使用虚拟化树视图 + + ) : treeData.length > 0 ? ( + // 使用传统树视图 + + ) : ( + + + + ) + ) : ( + + + + )} + + + ) +} + +export default WorkspaceExplorer diff --git a/src/renderer/src/components/WorkspaceFileViewer/index.tsx b/src/renderer/src/components/WorkspaceFileViewer/index.tsx new file mode 100644 index 0000000000..980fc8a3e1 --- /dev/null +++ b/src/renderer/src/components/WorkspaceFileViewer/index.tsx @@ -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` + 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` + 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)` + 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` + 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 = ({ + filePath, + content, + onClose, + onSendToChat, + onSendFileToChat, + onContentChange +}) => { + const { t } = useTranslation() + const { theme: appTheme } = useTheme() + const { token } = antdTheme.useToken() + + const extensionMap = useMemo>( + () => ({ + 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('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(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 ( + + + + {path.basename(filePath)} + + + } + unCheckedChildren={} + checked={useInternalLightTheme} + onChange={toggleSyntaxTheme} + title={t('common.toggleTheme')} + /> + + + + ) : ( + + )} + + + + + {onSendToChat && ( + + )} + {onSendFileToChat && ( + + )} + + + + ) +} + +export default WorkspaceFileViewer diff --git a/src/renderer/src/components/WorkspaceInitializer.tsx b/src/renderer/src/components/WorkspaceInitializer.tsx new file mode 100644 index 0000000000..2361e9d4a1 --- /dev/null +++ b/src/renderer/src/components/WorkspaceInitializer.tsx @@ -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 diff --git a/src/renderer/src/components/WorkspaceSelector/index.tsx b/src/renderer/src/components/WorkspaceSelector/index.tsx new file mode 100644 index 0000000000..a9e43d5410 --- /dev/null +++ b/src/renderer/src/components/WorkspaceSelector/index.tsx @@ -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(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: ( + + {workspace.name} + {workspace.path} + + + + + + + + + + ) +} + +export default WorkspaceSelector diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 198e49a43b..77b743e2c7 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -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: , minapp: , knowledge: , - files: + files: , + workspace: } 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) => { diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index c942e678a0..db12dc97ee 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -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 translate_history: EntityTable quick_phrases: EntityTable + workspaces: EntityTable } 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 diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 6e818c83e7..0fd264bff8 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -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)) }) diff --git a/src/renderer/src/hooks/useNavBackgroundColor.ts b/src/renderer/src/hooks/useNavBackgroundColor.ts index 035944990c..6c2e94ead3 100644 --- a/src/renderer/src/hooks/useNavBackgroundColor.ts +++ b/src/renderer/src/hooks/useNavBackgroundColor.ts @@ -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' } diff --git a/src/renderer/src/hooks/useTheme.ts b/src/renderer/src/hooks/useTheme.ts new file mode 100644 index 0000000000..3c2bca0158 --- /dev/null +++ b/src/renderer/src/hooks/useTheme.ts @@ -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 } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8db3605c54..cb9f36514f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index ba542f9951..84e79a7a0f 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1121,6 +1121,7 @@ "installHelp": "インストールヘルプを取得", "tabs": { "general": "一般", + "description": "説明", "tools": "ツール", "prompts": "プロンプト", "resources": "リソース" diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b8baa0e932..3e96f17559 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1124,6 +1124,7 @@ "installHelp": "Получить помощь по установке", "tabs": { "general": "Общие", + "description": "Описание", "tools": "Инструменты", "prompts": "Подсказки", "resources": "Ресурсы" diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c1fe346f11..d4d429dbd1 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "包管理源", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 6097e39ae1..09a9202439 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1121,6 +1121,7 @@ "installHelp": "獲取安裝幫助", "tabs": { "general": "通用", + "description": "描述", "tools": "工具", "prompts": "提示", "resources": "資源" diff --git a/src/renderer/src/locales/en/workspace.json b/src/renderer/src/locales/en/workspace.json new file mode 100644 index 0000000000..82aeecbd1f --- /dev/null +++ b/src/renderer/src/locales/en/workspace.json @@ -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" +} diff --git a/src/renderer/src/locales/zh-CN/workspace.json b/src/renderer/src/locales/zh-CN/workspace.json new file mode 100644 index 0000000000..1238284066 --- /dev/null +++ b/src/renderer/src/locales/zh-CN/workspace.json @@ -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隐藏" +} diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx index 861ef85a11..f570592a89 100644 --- a/src/renderer/src/pages/home/Chat.tsx +++ b/src/renderer/src/pages/home/Chat.tsx @@ -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) => { 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) => { const inputbarComponent = useMemo( () => ( - + ), - [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) => { ) }, [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 (
@@ -68,6 +100,12 @@ const Chat: FC = (props) => { {inputbarComponent}
{tabsComponent} +
) } diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index db46bd2d2a..8db70cfd2b 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -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 = ({ assistant: _assistant, setActiveTopic, topic }) => { +const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic, onToggleWorkspacePanel }) => { + // Destructure the new prop const [text, setText] = useState(_text) // 用于存储语音识别的中间结果,不直接显示在输入框中 const [, setAsrCurrentText] = useState('') @@ -1137,6 +1139,51 @@ const Inputbar: FC = ({ 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 = ({ assistant: _assistant, setActiveTopic, topic }) = )}