From 53643e81f069521c2b2d956f80dc83b10e34b88e Mon Sep 17 00:00:00 2001 From: 1600822305 <1600822305@qq.com> Date: Sun, 20 Apr 2025 01:46:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asr-server/embedded.js | 27 +- asr-server/test.js | 106 ++--- check-duplicate-messages.js | 82 ++-- check_json.js | 66 +-- create_empty_agents.js | 16 +- electron-builder.yml | 2 + package.json | 9 +- packages/shared/IpcChannel.ts | 1 + scripts/artifact-build-completed.js | 48 +++ src/main/index.ts | 4 + src/main/reranker/JinaReranker.ts | 4 +- src/main/reranker/SiliconFlowReranker.ts | 4 +- src/main/reranker/VoyageReranker.ts | 4 +- src/main/services/ASRServerService.ts | 368 ++++++++-------- src/main/services/AxiosProxy.ts | 27 ++ src/main/services/CodeExecutorService.ts | 7 +- src/main/services/CopilotService.ts | 12 +- src/main/services/MCPService.ts | 260 ++++++----- src/main/services/WindowService.ts | 1 + src/main/services/mcp/oauth/callback.ts | 76 ++++ src/main/services/mcp/oauth/provider.ts | 78 ++++ src/main/services/mcp/oauth/storage.ts | 120 ++++++ src/main/services/mcp/oauth/types.ts | 61 +++ src/preload/index.d.ts | 5 +- src/preload/index.ts | 3 +- src/renderer/src/assets/images/apps/zai.png | 1 + src/renderer/src/assets/styles/index.scss | 1 + .../src/components/AssistantMemoryPopup.tsx | 4 +- .../CodeExecutorButton/ExecutionResult.tsx | 6 +- .../CodeMirrorEditor/ChineseSearchPanel.ts | 2 +- .../src/components/CodeMirrorEditor/index.tsx | 93 ++-- .../src/components/DeepClaudeProvider.tsx | 36 +- .../src/components/PDFSettingsInitializer.tsx | 16 +- .../Popups/AssistantMemoryPopup.tsx | 12 +- .../components/Popups/SelectModelPopup.tsx | 43 +- .../src/components/QuickPanel/view.tsx | 15 +- src/renderer/src/components/WebdavModals.tsx | 3 +- src/renderer/src/components/app/Navbar.tsx | 4 +- src/renderer/src/config/models.ts | 48 ++- src/renderer/src/hooks/useSettings.ts | 3 +- src/renderer/src/hooks/useSidebarIcon.ts | 2 +- src/renderer/src/hooks/useTopic.ts | 6 +- src/renderer/src/i18n/locales/en-us.json | 4 +- src/renderer/src/i18n/locales/ja-jp.json | 4 +- src/renderer/src/i18n/locales/ru-ru.json | 4 +- src/renderer/src/i18n/locales/zh-cn.json | 16 +- src/renderer/src/i18n/locales/zh-tw.json | 4 +- .../pages/agents/components/AddAgentPopup.tsx | 13 +- src/renderer/src/pages/agents/index.ts | 5 +- .../pages/home/Inputbar/AttachmentButton.tsx | 76 ++-- .../home/Inputbar/MentionModelsButton.tsx | 2 + .../src/pages/home/Markdown/Markdown.tsx | 17 +- .../src/pages/home/Messages/Message.tsx | 342 ++++++++------- .../pages/home/Messages/MessageContent.tsx | 237 ++++++----- .../home/Messages/MessageErrorBoundary.tsx | 39 +- .../pages/home/Messages/MessageMenubar.tsx | 2 +- .../src/pages/home/Messages/MessageStream.tsx | 8 +- src/renderer/src/pages/home/Navbar.tsx | 3 - .../settings/DeepClaudeSettings/index.tsx | 130 +++--- .../settings/MCPSettings/EditMcpJsonPopup.tsx | 4 + .../MCPSettings/ImportMcpServerPopup.tsx | 199 +++++++++ .../MCPSettings/McpSettingsNavbar.tsx | 12 +- .../pages/settings/MCPSettings/NpxSearch.tsx | 16 +- .../src/pages/settings/MCPSettings/index.tsx | 4 + .../MemorySettings/AssistantMemoryManager.tsx | 13 +- .../pages/settings/MemorySettings/index.tsx | 7 +- .../ModelCombinationSettings/index.tsx | 402 ++++++++---------- .../ProviderSettings/GeminiKeyManager.tsx | 80 +++- .../ProviderSettings/ProviderSetting.tsx | 68 ++- .../pages/settings/ProviderSettings/index.tsx | 9 +- .../WebSearchSettings/AddSubscribePopup.tsx | 4 +- .../WebSearchProviderSetting.tsx | 51 ++- src/renderer/src/pages/settings/styles.ts | 2 +- .../AiProvider/DeepClaudeProvider.ts | 236 +++++----- .../providers/AiProvider/GeminiProvider.ts | 40 +- .../providers/AiProvider/OpenAIProvider.ts | 16 +- .../providers/AiProvider/ProviderFactory.ts | 34 +- src/renderer/src/services/ASRService.ts | 1 + src/renderer/src/services/ApiService.ts | 2 +- .../src/services/AssistantMemoryService.ts | 58 +-- src/renderer/src/services/AssistantService.ts | 16 +- src/renderer/src/services/BackupService.ts | 6 +- .../services/MemoryDeduplicationService.ts | 28 +- src/renderer/src/services/MemoryService.ts | 19 +- .../src/services/ModelMessageService.ts | 49 +++ .../__tests__/ModelMessageService.test.ts | 124 ++++++ src/renderer/src/store/memory.ts | 14 +- src/renderer/src/store/messages.ts | 29 +- src/renderer/src/store/settings.ts | 10 +- src/renderer/src/types/index.ts | 19 +- src/renderer/src/types/model.ts | 222 ++++++++++ .../src/utils/createDeepClaudeProvider.ts | 72 ++-- src/renderer/src/utils/index.ts | 26 ++ src/renderer/src/utils/markdown.ts | 65 +++ src/renderer/src/utils/thinkingLibrary.ts | 75 ++-- .../src/windows/mini/home/HomeWindow.tsx | 2 +- yarn.lock | 41 +- 97 files changed, 3113 insertions(+), 1554 deletions(-) create mode 100644 scripts/artifact-build-completed.js create mode 100644 src/main/services/AxiosProxy.ts create mode 100644 src/main/services/mcp/oauth/callback.ts create mode 100644 src/main/services/mcp/oauth/provider.ts create mode 100644 src/main/services/mcp/oauth/storage.ts create mode 100644 src/main/services/mcp/oauth/types.ts create mode 100644 src/renderer/src/assets/images/apps/zai.png create mode 100644 src/renderer/src/pages/settings/MCPSettings/ImportMcpServerPopup.tsx create mode 100644 src/renderer/src/services/ModelMessageService.ts create mode 100644 src/renderer/src/services/__tests__/ModelMessageService.test.ts create mode 100644 src/renderer/src/types/model.ts diff --git a/asr-server/embedded.js b/asr-server/embedded.js index f1a5cdcc91..b7d0af0093 100644 --- a/asr-server/embedded.js +++ b/asr-server/embedded.js @@ -7,7 +7,7 @@ const http = require('http') const path = require('path') const fs = require('fs') -const net = require('net') +// const net = require('net') const crypto = require('crypto') // 输出环境信息 @@ -114,23 +114,24 @@ const clients = { } // 处理WebSocket连接 -server.on('upgrade', (request, socket, head) => { +server.on('upgrade', (request, socket) => { try { console.log('[WebSocket] Connection upgrade request received') // 解析WebSocket密钥 const key = request.headers['sec-websocket-key'] - const acceptKey = crypto.createHash('sha1') + const acceptKey = crypto + .createHash('sha1') .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary') .digest('base64') // 发送WebSocket握手响应 socket.write( 'HTTP/1.1 101 Switching Protocols\r\n' + - 'Upgrade: websocket\r\n' + - 'Connection: Upgrade\r\n' + - `Sec-WebSocket-Accept: ${acceptKey}\r\n` + - '\r\n' + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${acceptKey}\r\n` + + '\r\n' ) console.log('[WebSocket] Handshake successful') @@ -157,10 +158,8 @@ function handleWebSocketConnection(socket) { // 检查是否有完整的帧 const firstByte = buffer[0] const secondByte = buffer[1] - const isFinalFrame = Boolean((firstByte >>> 7) & 0x1) - const [opCode, maskFlag, payloadLength] = [ - firstByte & 0xF, (secondByte >>> 7) & 0x1, secondByte & 0x7F - ] + // const isFinalFrame = Boolean((firstByte >>> 7) & 0x1) + const [opCode, maskFlag, payloadLength] = [firstByte & 0xf, (secondByte >>> 7) & 0x1, secondByte & 0x7f] // 处理不同的负载长度 let payloadStartIndex = 2 @@ -265,7 +264,7 @@ function sendWebSocketFrame(socket, data, opCode = 0x1) { // 发送Pong响应 function sendPong(socket) { - const pongFrame = Buffer.from([0x8A, 0x00]) + const pongFrame = Buffer.from([0x8a, 0x00]) socket.write(pongFrame) } @@ -351,11 +350,11 @@ async function findAvailablePort(startPort) { port++ } - throw new Error(`Could not find an available port between ${startPort} and ${maxPort-1}`) + throw new Error(`Could not find an available port between ${startPort} and ${maxPort - 1}`) } // 尝试启动服务器 -(async () => { +;(async () => { try { // 默认端口 const defaultPort = 34515 diff --git a/asr-server/test.js b/asr-server/test.js index 85558cc946..cd7a23c77c 100644 --- a/asr-server/test.js +++ b/asr-server/test.js @@ -3,67 +3,73 @@ * 用于测试ASR服务器是否正常工作 */ -const WebSocket = require('ws'); -const http = require('http'); +const WebSocket = require('ws') +const http = require('http') // 测试HTTP服务器 -console.log('测试HTTP服务器...'); -http.get('http://localhost:34515', (res) => { - console.log(`HTTP状态码: ${res.statusCode}`); - - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - console.log('HTTP响应接收完成'); - console.log(`响应长度: ${data.length} 字节`); - console.log('HTTP测试完成'); - - // 测试WebSocket - testWebSocket(); - }); -}).on('error', (err) => { - console.error('HTTP测试失败:', err.message); -}); +console.log('测试HTTP服务器...') +http + .get('http://localhost:34515', (res) => { + console.log(`HTTP状态码: ${res.statusCode}`) + + let data = '' + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + console.log('HTTP响应接收完成') + console.log(`响应长度: ${data.length} 字节`) + console.log('HTTP测试完成') + + // 测试WebSocket + testWebSocket() + }) + }) + .on('error', (err) => { + console.error('HTTP测试失败:', err.message) + }) // 测试WebSocket function testWebSocket() { - console.log('\n测试WebSocket...'); - const ws = new WebSocket('ws://localhost:34515'); - + console.log('\n测试WebSocket...') + const ws = new WebSocket('ws://localhost:34515') + ws.on('open', () => { - console.log('WebSocket连接已打开'); - + console.log('WebSocket连接已打开') + // 发送身份识别消息 - ws.send(JSON.stringify({ - type: 'identify', - role: 'electron' - })); - + ws.send( + JSON.stringify({ + type: 'identify', + role: 'electron' + }) + ) + // 发送测试消息 setTimeout(() => { - console.log('发送测试消息...'); - ws.send(JSON.stringify({ - type: 'test', - message: '这是一条测试消息' - })); - }, 1000); - + console.log('发送测试消息...') + ws.send( + JSON.stringify({ + type: 'test', + message: '这是一条测试消息' + }) + ) + }, 1000) + // 关闭连接 setTimeout(() => { - console.log('关闭WebSocket连接...'); - ws.close(); - console.log('测试完成'); - }, 2000); - }); - + console.log('关闭WebSocket连接...') + ws.close() + console.log('测试完成') + }, 2000) + }) + ws.on('message', (data) => { - console.log(`收到WebSocket消息: ${data}`); - }); - + console.log(`收到WebSocket消息: ${data}`) + }) + ws.on('error', (error) => { - console.error('WebSocket测试失败:', error.message); - }); + console.error('WebSocket测试失败:', error.message) + }) } diff --git a/check-duplicate-messages.js b/check-duplicate-messages.js index 243cf4caed..0a7e0d586a 100644 --- a/check-duplicate-messages.js +++ b/check-duplicate-messages.js @@ -1,77 +1,77 @@ // 检查重复消息的脚本 -const { app } = require('electron'); -const path = require('path'); -const fs = require('fs'); +const { app } = require('electron') +const path = require('path') +const fs = require('fs') // 获取数据库文件路径 -const userDataPath = app.getPath('userData'); -const dbFilePath = path.join(userDataPath, 'CherryStudio.db'); +const userDataPath = app.getPath('userData') +const dbFilePath = path.join(userDataPath, 'CherryStudio.db') -console.log('数据库文件路径:', dbFilePath); +console.log('数据库文件路径:', dbFilePath) // 检查文件是否存在 if (fs.existsSync(dbFilePath)) { - console.log('数据库文件存在'); - + console.log('数据库文件存在') + // 读取数据库内容 - const dbContent = fs.readFileSync(dbFilePath, 'utf8'); - + const dbContent = fs.readFileSync(dbFilePath, 'utf8') + // 解析数据库内容 try { - const data = JSON.parse(dbContent); - + const data = JSON.parse(dbContent) + // 检查topics表中的消息 if (data.topics) { - console.log('找到topics表,共有', data.topics.length, '个主题'); - + console.log('找到topics表,共有', data.topics.length, '个主题') + // 遍历每个主题 - data.topics.forEach(topic => { - console.log(`检查主题: ${topic.id}`); - + data.topics.forEach((topic) => { + console.log(`检查主题: ${topic.id}`) + if (topic.messages && Array.isArray(topic.messages)) { - console.log(` 主题消息数量: ${topic.messages.length}`); - + console.log(` 主题消息数量: ${topic.messages.length}`) + // 检查重复消息 - const messageIds = new Set(); - const duplicates = []; - - topic.messages.forEach(message => { + const messageIds = new Set() + const duplicates = [] + + topic.messages.forEach((message) => { if (messageIds.has(message.id)) { - duplicates.push(message.id); + duplicates.push(message.id) } else { - messageIds.add(message.id); + messageIds.add(message.id) } - }); - + }) + if (duplicates.length > 0) { - console.log(` 发现${duplicates.length}条重复消息ID:`, duplicates); + console.log(` 发现${duplicates.length}条重复消息ID:`, duplicates) } else { - console.log(' 未发现重复消息ID'); + console.log(' 未发现重复消息ID') } - + // 检查重复的askId (对于助手消息) - const askIds = {}; - topic.messages.forEach(message => { + const askIds = {} + topic.messages.forEach((message) => { if (message.role === 'assistant' && message.askId) { if (!askIds[message.askId]) { - askIds[message.askId] = []; + askIds[message.askId] = [] } - askIds[message.askId].push(message.id); + askIds[message.askId].push(message.id) } - }); - + }) + // 输出每个askId对应的助手消息数量 Object.entries(askIds).forEach(([askId, messageIds]) => { if (messageIds.length > 1) { - console.log(` askId ${askId} 有 ${messageIds.length} 条助手消息`); + console.log(` askId ${askId} 有 ${messageIds.length} 条助手消息`) } - }); + }) } - }); + }) } } catch (error) { - console.error('解析数据库内容失败:', error); + console.error('解析数据库内容失败:', error) } } else { - console.log('数据库文件不存在'); + console.log('数据库文件不存在') } diff --git a/check_json.js b/check_json.js index bc0a2592c9..ff20d61be7 100644 --- a/check_json.js +++ b/check_json.js @@ -1,64 +1,64 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require('fs') +const path = require('path') // 读取agents.json文件 -const filePath = path.join('resources', 'data', 'agents.json'); +const filePath = path.join('resources', 'data', 'agents.json') fs.readFile(filePath, (err, data) => { if (err) { - console.error('读取文件失败:', err); - return; + console.error('读取文件失败:', err) + return } // 输出文件的前20个字节的十六进制表示 - console.log('文件前20个字节:'); + console.log('文件前20个字节:') for (let i = 0; i < Math.min(20, data.length); i++) { - console.log(`字节 ${i}: 0x${data[i].toString(16)} (${String.fromCharCode(data[i])})`); + console.log(`字节 ${i}: 0x${data[i].toString(16)} (${String.fromCharCode(data[i])})`) } // 尝试不同的方式解析JSON - console.log('\n尝试不同的方式解析JSON:'); - + console.log('\n尝试不同的方式解析JSON:') + // 1. 直接解析 try { - const json1 = JSON.parse(data); - console.log('方法1成功: 直接解析'); + JSON.parse(data) + console.log('方法1成功: 直接解析') } catch (e) { - console.error('方法1失败:', e.message); + console.error('方法1失败:', e.message) } - + // 2. 转换为字符串后解析 try { - const json2 = JSON.parse(data.toString()); - console.log('方法2成功: 转换为字符串后解析'); + JSON.parse(data.toString()) + console.log('方法2成功: 转换为字符串后解析') } catch (e) { - console.error('方法2失败:', e.message); + console.error('方法2失败:', e.message) } - + // 3. 移除BOM后解析 try { - const str = data.toString(); - const noBomStr = str.charCodeAt(0) === 0xFEFF ? str.slice(1) : str; - const json3 = JSON.parse(noBomStr); - console.log('方法3成功: 移除BOM后解析'); + const str = data.toString() + const noBomStr = str.charCodeAt(0) === 0xfeff ? str.slice(1) : str + JSON.parse(noBomStr) + console.log('方法3成功: 移除BOM后解析') } catch (e) { - console.error('方法3失败:', e.message); + console.error('方法3失败:', e.message) } - + // 4. 移除前3个字符后解析 try { - const str = data.toString().slice(3); - const json4 = JSON.parse(str); - console.log('方法4成功: 移除前3个字符后解析'); + const str = data.toString().slice(3) + JSON.parse(str) + console.log('方法4成功: 移除前3个字符后解析') } catch (e) { - console.error('方法4失败:', e.message); + console.error('方法4失败:', e.message) } - + // 5. 移除所有非ASCII字符后解析 try { - const str = data.toString().replace(/[^\x20-\x7E]/g, ''); - const json5 = JSON.parse(str); - console.log('方法5成功: 移除所有非ASCII字符后解析'); + const str = data.toString().replace(/[^\x20-\x7E]/g, '') + JSON.parse(str) + console.log('方法5成功: 移除所有非ASCII字符后解析') } catch (e) { - console.error('方法5失败:', e.message); + console.error('方法5失败:', e.message) } -}); +}) diff --git a/create_empty_agents.js b/create_empty_agents.js index e30985ae57..1770917fcc 100644 --- a/create_empty_agents.js +++ b/create_empty_agents.js @@ -1,14 +1,14 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require('fs') +const path = require('path') // 创建一个空的agents.json文件 -const emptyAgents = []; -const filePath = path.join('resources', 'data', 'agents.json'); +const emptyAgents = [] +const filePath = path.join('resources', 'data', 'agents.json') // 备份原始文件 -fs.copyFileSync(filePath, filePath + '.bak'); -console.log('已备份原始文件到 ' + filePath + '.bak'); +fs.copyFileSync(filePath, filePath + '.bak') +console.log('已备份原始文件到 ' + filePath + '.bak') // 写入新文件 -fs.writeFileSync(filePath, JSON.stringify(emptyAgents, null, 2), 'utf8'); -console.log('已创建新的agents.json文件,内容为空数组'); +fs.writeFileSync(filePath, JSON.stringify(emptyAgents, null, 2), 'utf8') +console.log('已创建新的agents.json文件,内容为空数组') diff --git a/electron-builder.yml b/electron-builder.yml index b903f5ab88..b261ceea1b 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -65,6 +65,7 @@ nsis: allowToChangeInstallationDirectory: true oneClick: false include: build/nsis-installer.nsh + buildUniversalInstaller: false portable: artifactName: ${productName}-${version}-${arch}-portable.${ext} mac: @@ -101,6 +102,7 @@ electronDownload: mirror: https://npmmirror.com/mirrors/electron/ afterPack: scripts/after-pack.js afterSign: scripts/notarize.js +artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | 全新图标风格 diff --git a/package.json b/package.json index 78b29d06f7..3b31abfcde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "1.2.4", + "version": "1.2.5-bate", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", @@ -23,7 +23,7 @@ "build": "npm run typecheck && electron-vite build", "build:check": "yarn test && yarn typecheck && yarn check:i18n", "build:unpack": "dotenv npm run build && electron-builder --dir", - "build:win": "dotenv npm run build && electron-builder --win && node scripts/after-build.js", + "build:win": "dotenv npm run build && electron-builder --win", "build:win:x64": "dotenv npm run build && electron-builder --win --x64", "build:win:arm64": "dotenv npm run build && electron-builder --win --arm64", "build:mac": "dotenv electron-vite build && electron-builder --mac", @@ -119,7 +119,6 @@ "fs-extra": "^11.2.0", "got-scraping": "^4.1.1", "js-tiktoken": "^1.0.19", - "js-yaml": "^4.1.0", "jsdom": "^26.0.0", "markdown-it": "^14.1.0", "monaco-editor": "^0.52.2", @@ -154,7 +153,7 @@ "@google/genai": "^0.8.0", "@hello-pangea/dnd": "^16.6.0", "@kangfenmao/keyv-storage": "^0.1.0", - "@modelcontextprotocol/sdk": "^1.9.0", + "@modelcontextprotocol/sdk": "^1.10.1", "@notionhq/client": "^2.2.15", "@reduxjs/toolkit": "^2.2.5", "@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch", @@ -163,7 +162,6 @@ "@types/d3": "^7", "@types/diff": "^7", "@types/fs-extra": "^11", - "@types/js-yaml": "^4", "@types/lodash": "^4.17.16", "@types/markdown-it": "^14", "@types/md5": "^2.3.5", @@ -223,6 +221,7 @@ "rehype-katex": "^7.0.1", "rehype-mathjax": "^7.0.0", "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", "remark-cjk-friendly": "^1.1.0", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 4355b93acf..a45a5c15be 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -139,6 +139,7 @@ export enum IpcChannel { // system System_GetDeviceType = 'system:getDeviceType', + System_GetHostname = 'system:getHostname', // events SelectionAction = 'selection-action', diff --git a/scripts/artifact-build-completed.js b/scripts/artifact-build-completed.js new file mode 100644 index 0000000000..f25a63bbc6 --- /dev/null +++ b/scripts/artifact-build-completed.js @@ -0,0 +1,48 @@ +/** + * This script is executed after each artifact is built. + * It removes spaces from filenames to ensure compatibility with various systems. + */ + +const fs = require('fs') +const path = require('path') + +/** + * Removes spaces from a filename and replaces them with hyphens + * @param {string} artifactPath - Path to the artifact file + */ +function removeSpacesFromFilename(artifactPath) { + const dir = path.dirname(artifactPath) + const filename = path.basename(artifactPath) + // Replace spaces with hyphens in the filename + const newFilename = filename.replace(/\s+/g, '-') + // If the filename has changed, rename the file + if (newFilename !== filename) { + const newPath = path.join(dir, newFilename) + console.log(`Renaming: ${filename} -> ${newFilename}`) + fs.renameSync(artifactPath, newPath) + return newPath + } + return artifactPath +} + +/** + * Main function that runs when an artifact is built + * @param {object} params - Parameters from electron-builder + */ +module.exports = async function (params) { + const { artifactPath } = params + if (!artifactPath) { + console.log('No artifact path provided') + return + } + console.log(`Processing artifact: ${artifactPath}`) + try { + const newPath = removeSpacesFromFilename(artifactPath) + // Return the new path so electron-builder knows where the artifact is + return { artifactPath: newPath } + } catch (error) { + console.error('Error processing artifact:', error) + // Return the original path if there was an error + return { artifactPath } + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 59acda25d2..bf6c96559b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -63,6 +63,10 @@ if (!app.requestSingleInstanceLock()) { ipcMain.handle(IpcChannel.System_GetDeviceType, () => { return process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'windows' : 'linux' }) + + ipcMain.handle(IpcChannel.System_GetHostname, () => { + return require('os').hostname() + }) }) registerProtocolClient(app) diff --git a/src/main/reranker/JinaReranker.ts b/src/main/reranker/JinaReranker.ts index 207ddcb992..88350a5e61 100644 --- a/src/main/reranker/JinaReranker.ts +++ b/src/main/reranker/JinaReranker.ts @@ -1,6 +1,6 @@ import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' +import AxiosProxy from '@main/services/AxiosProxy' import { KnowledgeBaseParams } from '@types' -import axios from 'axios' import BaseReranker from './BaseReranker' @@ -20,7 +20,7 @@ export default class JinaReranker extends BaseReranker { } try { - const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) + const { data } = await AxiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() }) const rerankResults = data.results return this.getRerankResult(searchResults, rerankResults) diff --git a/src/main/reranker/SiliconFlowReranker.ts b/src/main/reranker/SiliconFlowReranker.ts index 0a27cf7e2a..78a213561a 100644 --- a/src/main/reranker/SiliconFlowReranker.ts +++ b/src/main/reranker/SiliconFlowReranker.ts @@ -1,6 +1,6 @@ import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' +import axiosProxy from '@main/services/AxiosProxy' import { KnowledgeBaseParams } from '@types' -import axios from 'axios' import BaseReranker from './BaseReranker' @@ -22,7 +22,7 @@ export default class SiliconFlowReranker extends BaseReranker { } try { - const { data } = await axios.post(url, requestBody, { headers: this.defaultHeaders() }) + const { data } = await axiosProxy.axios.post(url, requestBody, { headers: this.defaultHeaders() }) const rerankResults = data.results return this.getRerankResult(searchResults, rerankResults) diff --git a/src/main/reranker/VoyageReranker.ts b/src/main/reranker/VoyageReranker.ts index a2c0f5f8af..44c800b6d5 100644 --- a/src/main/reranker/VoyageReranker.ts +++ b/src/main/reranker/VoyageReranker.ts @@ -1,6 +1,6 @@ import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces' +import axiosProxy from '@main/services/AxiosProxy' import { KnowledgeBaseParams } from '@types' -import axios from 'axios' import BaseReranker from './BaseReranker' @@ -22,7 +22,7 @@ export default class VoyageReranker extends BaseReranker { } try { - const { data } = await axios.post(url, requestBody, { + const { data } = await axiosProxy.axios.post(url, requestBody, { headers: { ...this.defaultHeaders() } diff --git a/src/main/services/ASRServerService.ts b/src/main/services/ASRServerService.ts index 90d28219cb..2c1f500305 100644 --- a/src/main/services/ASRServerService.ts +++ b/src/main/services/ASRServerService.ts @@ -1,12 +1,13 @@ -import http from 'node:http' -import net from 'node:net' import crypto from 'node:crypto' import fs from 'node:fs' +import http from 'node:http' +import net from 'node:net' import path from 'node:path' import { IpcChannel } from '@shared/IpcChannel' import { app, ipcMain } from 'electron' import log from 'electron-log' + import { getResourcePath } from '../utils' /** @@ -14,19 +15,19 @@ import { getResourcePath } from '../utils' */ export class ASRServerService { // HTML内容 - private INDEX_HTML_CONTENT: string = ''; + private INDEX_HTML_CONTENT: string = '' // 服务器相关属性 - private httpServer: http.Server | null = null; - private wsClients: { browser: any | null; electron: any | null } = { browser: null, electron: null }; - private serverPort: number = 34515; // 默认端口 - private isServerRunning: boolean = false; + private httpServer: http.Server | null = null + private wsClients: { browser: any | null; electron: any | null } = { browser: null, electron: null } + private serverPort: number = 34515 // 默认端口 + private isServerRunning: boolean = false /** * 构造函数 */ constructor() { - this.loadIndexHtml(); + this.loadIndexHtml() } /** @@ -35,24 +36,24 @@ export class ASRServerService { private loadIndexHtml(): void { try { // 在开发环境和生产环境中使用不同的路径 - let htmlPath = ''; + let htmlPath = '' if (app.isPackaged) { // 生产环境 - const resourcePath = getResourcePath(); - htmlPath = path.join(resourcePath, 'app', 'asr-server', 'index.html'); + const resourcePath = getResourcePath() + htmlPath = path.join(resourcePath, 'app', 'asr-server', 'index.html') } else { // 开发环境 - htmlPath = path.join(app.getAppPath(), 'asr-server', 'index.html'); + htmlPath = path.join(app.getAppPath(), 'asr-server', 'index.html') } - log.info(`加载index.html文件: ${htmlPath}`); + log.info(`加载index.html文件: ${htmlPath}`) if (fs.existsSync(htmlPath)) { - this.INDEX_HTML_CONTENT = fs.readFileSync(htmlPath, 'utf8'); - log.info(`成功加载index.html文件`); + this.INDEX_HTML_CONTENT = fs.readFileSync(htmlPath, 'utf8') + log.info(`成功加载index.html文件`) } else { - log.error(`index.html文件不存在: ${htmlPath}`); + log.error(`index.html文件不存在: ${htmlPath}`) // 使用默认的HTML内容 this.INDEX_HTML_CONTENT = ` @@ -63,10 +64,10 @@ export class ASRServerService {

Error: index.html file not found

Please make sure the ASR server files are properly installed.

-`; +` } } catch (error) { - log.error(`加载index.html文件时出错:`, error); + log.error(`加载index.html文件时出错:`, error) // 使用默认的HTML内容 this.INDEX_HTML_CONTENT = ` @@ -77,7 +78,7 @@ export class ASRServerService {

Error loading index.html

An error occurred while loading the ASR server files.

-`; +` } } @@ -86,10 +87,10 @@ export class ASRServerService { */ public registerIpcHandlers(): void { // 启动ASR服务器 - ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this)); + ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this)) // 停止ASR服务器 - ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this)); + ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this)) } /** @@ -99,22 +100,22 @@ export class ASRServerService { */ private isPortAvailable(port: number): Promise { return new Promise((resolve) => { - const testServer = net.createServer(); + const testServer = net.createServer() testServer.once('error', (err: any) => { if (err.code === 'EADDRINUSE') { - log.info(`端口 ${port} 已被占用,尝试其他端口...`); - resolve(false); + log.info(`端口 ${port} 已被占用,尝试其他端口...`) + resolve(false) } else { - log.error(`检查端口 ${port} 时出错:`, err); - resolve(false); + log.error(`检查端口 ${port} 时出错:`, err) + resolve(false) } - }); + }) testServer.once('listening', () => { - testServer.close(); - resolve(true); - }); - testServer.listen(port); - }); + testServer.close() + resolve(true) + }) + testServer.listen(port) + }) } /** @@ -123,17 +124,17 @@ export class ASRServerService { * @returns 可用的端口 */ private async findAvailablePort(startPort: number): Promise { - let port = startPort; - const maxPort = startPort + 10; // 尝试最多10个端口 + let port = startPort + const maxPort = startPort + 10 // 尝试最多10个端口 while (port < maxPort) { if (await this.isPortAvailable(port)) { - return port; + return port } - port++; + port++ } - throw new Error(`在 ${startPort} 和 ${maxPort-1} 之间找不到可用的端口`); + throw new Error(`在 ${startPort} 和 ${maxPort - 1} 之间找不到可用的端口`) } /** @@ -144,18 +145,17 @@ export class ASRServerService { private handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): void { // 只处理根路径请求,返回index.html if (req.url === '/' || req.url === '/index.html') { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(this.INDEX_HTML_CONTENT); - log.info(`返回index.html到客户端`); + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(this.INDEX_HTML_CONTENT) + log.info(`返回index.html到客户端`) } else { // 其他路径返回404 - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found'); - log.info(`请求的路径不存在: ${req.url}`); + res.writeHead(404, { 'Content-Type': 'text/plain' }) + res.end('Not Found') + log.info(`请求的路径不存在: ${req.url}`) } } - /** * 启动ASR服务器 * @returns Promise<{success: boolean, pid?: number, port?: number, error?: string}> @@ -164,63 +164,61 @@ export class ASRServerService { try { // 如果服务器已经运行,直接返回成功 if (this.isServerRunning && this.httpServer) { - return { success: true, port: this.serverPort }; + return { success: true, port: this.serverPort } } // 尝试找到可用的端口 try { - this.serverPort = await this.findAvailablePort(this.serverPort); + this.serverPort = await this.findAvailablePort(this.serverPort) } catch (error) { - log.error('找不到可用的端口:', error); - return { success: false, error: '找不到可用的端口' }; + log.error('找不到可用的端口:', error) + return { success: false, error: '找不到可用的端口' } } - log.info(`使用端口: ${this.serverPort}`); + log.info(`使用端口: ${this.serverPort}`) // 创建HTTP服务器 - this.httpServer = http.createServer(this.handleHttpRequest.bind(this)); + this.httpServer = http.createServer(this.handleHttpRequest.bind(this)) // 启动HTTP服务器 try { await new Promise((resolve, reject) => { if (!this.httpServer) { - reject(new Error('HTTP服务器创建失败')); - return; + reject(new Error('HTTP服务器创建失败')) + return } this.httpServer.on('error', (err) => { - log.error(`HTTP服务器错误:`, err); - reject(err); - }); + log.error(`HTTP服务器错误:`, err) + reject(err) + }) this.httpServer.listen(this.serverPort, () => { - log.info(`HTTP服务器已启动,监听端口: ${this.serverPort}`); - resolve(); - }); - }); + log.info(`HTTP服务器已启动,监听端口: ${this.serverPort}`) + resolve() + }) + }) // 设置WebSocket处理 - this.setupWebSocketServer(); + this.setupWebSocketServer() // 标记服务器已启动 - this.isServerRunning = true; + this.isServerRunning = true - log.info(`ASR服务器启动成功,端口: ${this.serverPort}`); - return { success: true, port: this.serverPort }; + log.info(`ASR服务器启动成功,端口: ${this.serverPort}`) + return { success: true, port: this.serverPort } } catch (error) { - log.error('启动HTTP服务器失败:', error); + log.error('启动HTTP服务器失败:', error) // 关闭HTTP服务器 if (this.httpServer) { - this.httpServer.close(); - this.httpServer = null; + this.httpServer.close() + this.httpServer = null } - return { success: false, error: `启动HTTP服务器失败: ${(error as Error).message}` }; + return { success: false, error: `启动HTTP服务器失败: ${(error as Error).message}` } } - - } catch (error) { - log.error('启动ASR服务器失败:', error); - return { success: false, error: (error as Error).message }; + log.error('启动ASR服务器失败:', error) + return { success: false, error: (error as Error).message } } } @@ -229,40 +227,40 @@ export class ASRServerService { */ private setupWebSocketServer(): void { if (!this.httpServer) { - log.error('HTTP服务器不存在,无法设置WebSocket'); - return; + log.error('HTTP服务器不存在,无法设置WebSocket') + return } // 处理WebSocket连接升级 - this.httpServer.on('upgrade', (request, socket, _head) => { + this.httpServer.on('upgrade', (request, socket) => { try { - log.info('[WebSocket] 收到连接升级请求'); + log.info('[WebSocket] 收到连接升级请求') // 解析WebSocket密钥 - const key = request.headers['sec-websocket-key'] as string; + const key = request.headers['sec-websocket-key'] as string const acceptKey = crypto .createHash('sha1') .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary') - .digest('base64'); + .digest('base64') // 发送WebSocket握手响应 socket.write( 'HTTP/1.1 101 Switching Protocols\r\n' + - 'Upgrade: websocket\r\n' + - 'Connection: Upgrade\r\n' + - `Sec-WebSocket-Accept: ${acceptKey}\r\n` + - '\r\n' - ); + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${acceptKey}\r\n` + + '\r\n' + ) - log.info('[WebSocket] 握手成功'); + log.info('[WebSocket] 握手成功') // 处理WebSocket数据 - this.handleWebSocketConnection(socket); + this.handleWebSocketConnection(socket) } catch (error) { - log.error('[WebSocket] 处理升级错误:', error); - socket.destroy(); + log.error('[WebSocket] 处理升级错误:', error) + socket.destroy() } - }); + }) } /** @@ -270,106 +268,107 @@ export class ASRServerService { * @param socket 套接字 */ private handleWebSocketConnection(socket: any): void { - let buffer = Buffer.alloc(0); - let role: 'browser' | 'electron' | null = null; + let buffer = Buffer.alloc(0) + const role: 'browser' | 'electron' | null = null socket.on('data', (data: Buffer) => { try { - buffer = Buffer.concat([buffer, data]); + buffer = Buffer.concat([buffer, data]) // 处理数据帧 while (buffer.length > 2) { // 检查是否有完整的帧 - const firstByte = buffer[0]; - const secondByte = buffer[1]; + const firstByte = buffer[0] + const secondByte = buffer[1] // const isFinalFrame = Boolean((firstByte >>> 7) & 0x1); // 暂时不使用 - const [opCode, maskFlag, payloadLength] = [ - firstByte & 0xF, (secondByte >>> 7) & 0x1, secondByte & 0x7F - ]; + const [opCode, maskFlag, payloadLength] = [firstByte & 0xf, (secondByte >>> 7) & 0x1, secondByte & 0x7f] // 处理不同的负载长度 - let payloadStartIndex = 2; - let payloadLen = payloadLength; + let payloadStartIndex = 2 + let payloadLen = payloadLength if (payloadLength === 126) { - payloadLen = buffer.readUInt16BE(2); - payloadStartIndex = 4; + payloadLen = buffer.readUInt16BE(2) + payloadStartIndex = 4 } else if (payloadLength === 127) { // 处理大于16位的长度 - payloadLen = Number(buffer.readBigUInt64BE(2)); - payloadStartIndex = 10; + payloadLen = Number(buffer.readBigUInt64BE(2)) + payloadStartIndex = 10 } // 处理掩码 - let maskingKey: Buffer | undefined; + let maskingKey: Buffer | undefined if (maskFlag) { - maskingKey = buffer.slice(payloadStartIndex, payloadStartIndex + 4); - payloadStartIndex += 4; + maskingKey = buffer.slice(payloadStartIndex, payloadStartIndex + 4) + payloadStartIndex += 4 } // 检查是否有足够的数据 - const frameEnd = payloadStartIndex + payloadLen; + const frameEnd = payloadStartIndex + payloadLen if (buffer.length < frameEnd) { // 需要更多数据 - break; + break } // 提取负载 - let payload = buffer.slice(payloadStartIndex, frameEnd); + const payload = buffer.slice(payloadStartIndex, frameEnd) // 如果有掩码,解码负载 if (maskFlag && maskingKey) { for (let i = 0; i < payload.length; i++) { - payload[i] = payload[i] ^ maskingKey[i % 4]; + payload[i] = payload[i] ^ maskingKey[i % 4] } } // 处理不同的操作码 if (opCode === 0x8) { // 关闭帧 - log.info('[WebSocket] 收到关闭帧'); - socket.end(); - return; + log.info('[WebSocket] 收到关闭帧') + socket.end() + return } else if (opCode === 0x9) { // Ping - this.sendPong(socket); + this.sendPong(socket) } else if (opCode === 0x1 || opCode === 0x2) { // 文本或二进制数据 - const message = opCode === 0x1 ? payload.toString('utf8') : payload; - this.handleMessage(socket, message, role); + const message = opCode === 0x1 ? payload.toString('utf8') : payload + this.handleMessage(socket, message, role) } // 移除已处理的帧 - buffer = buffer.slice(frameEnd); + buffer = buffer.slice(frameEnd) } } catch (error) { - log.error('[WebSocket] 处理数据错误:', error); + log.error('[WebSocket] 处理数据错误:', error) } - }); + }) socket.on('close', () => { - const socketRole = (socket as any)._role || role; - log.info(`[WebSocket] 连接关闭${socketRole ? ` (${socketRole})` : ''}`); + const socketRole = (socket as any)._role || role + log.info(`[WebSocket] 连接关闭${socketRole ? ` (${socketRole})` : ''}`) if (socketRole === 'browser') { - this.wsClients.browser = null; + this.wsClients.browser = null // 如果浏览器断开连接,通知Electron客户端 if (this.wsClients.electron) { - this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({ - type: 'status', - message: 'Browser disconnected' - })); - log.info('[WebSocket] 已向Electron发送Browser disconnected状态'); + this.sendWebSocketFrame( + this.wsClients.electron, + JSON.stringify({ + type: 'status', + message: 'Browser disconnected' + }) + ) + log.info('[WebSocket] 已向Electron发送Browser disconnected状态') } } else if (socketRole === 'electron') { - this.wsClients.electron = null; + this.wsClients.electron = null } - }); + }) socket.on('error', (error: Error) => { - log.error(`[WebSocket] 套接字错误${role ? ` (${role})` : ''}:`, error); - }); + log.error(`[WebSocket] 套接字错误${role ? ` (${role})` : ''}:`, error) + }) } /** @@ -380,27 +379,27 @@ export class ASRServerService { */ private sendWebSocketFrame(socket: any, data: string | object, opCode = 0x1): void { try { - const payload = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data)); - const payloadLength = payload.length; + const payload = Buffer.from(typeof data === 'string' ? data : JSON.stringify(data)) + const payloadLength = payload.length - let header: Buffer; + let header: Buffer if (payloadLength < 126) { - header = Buffer.from([0x80 | opCode, payloadLength]); + header = Buffer.from([0x80 | opCode, payloadLength]) } else if (payloadLength < 65536) { - header = Buffer.alloc(4); - header[0] = 0x80 | opCode; - header[1] = 126; - header.writeUInt16BE(payloadLength, 2); + header = Buffer.alloc(4) + header[0] = 0x80 | opCode + header[1] = 126 + header.writeUInt16BE(payloadLength, 2) } else { - header = Buffer.alloc(10); - header[0] = 0x80 | opCode; - header[1] = 127; - header.writeBigUInt64BE(BigInt(payloadLength), 2); + header = Buffer.alloc(10) + header[0] = 0x80 | opCode + header[1] = 127 + header.writeBigUInt64BE(BigInt(payloadLength), 2) } - socket.write(Buffer.concat([header, payload])); + socket.write(Buffer.concat([header, payload])) } catch (error) { - log.error('[WebSocket] 发送数据错误:', error); + log.error('[WebSocket] 发送数据错误:', error) } } @@ -409,8 +408,8 @@ export class ASRServerService { * @param socket 套接字 */ private sendPong(socket: any): void { - const pongFrame = Buffer.from([0x8A, 0x00]); - socket.write(pongFrame); + const pongFrame = Buffer.from([0x8a, 0x00]) + socket.write(pongFrame) } /** @@ -422,65 +421,71 @@ export class ASRServerService { private handleMessage(socket: any, message: string | Buffer, currentRole: string | null): void { try { if (typeof message === 'string') { - const data = JSON.parse(message); + const data = JSON.parse(message) // 处理身份识别 if (data.type === 'identify') { - const role = data.role; + const role = data.role if (role === 'browser' || role === 'electron') { - log.info(`[WebSocket] 客户端识别为: ${role}`); + log.info(`[WebSocket] 客户端识别为: ${role}`) // 存储客户端连接 - this.wsClients[role] = socket; + this.wsClients[role] = socket // 设置当前连接的角色 - (socket as any)._role = role; + ;(socket as any)._role = role // 如果是浏览器连接,通知Electron客户端 if (role === 'browser' && this.wsClients.electron) { // 发送browser_ready消息 - this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({ - type: 'status', - message: 'browser_ready' - })); - log.info('[WebSocket] 已向Electron发送browser_ready状态'); + this.sendWebSocketFrame( + this.wsClients.electron, + JSON.stringify({ + type: 'status', + message: 'browser_ready' + }) + ) + log.info('[WebSocket] 已向Electron发送browser_ready状态') // 发送Browser connected消息 - this.sendWebSocketFrame(this.wsClients.electron, JSON.stringify({ - type: 'status', - message: 'Browser connected' - })); - log.info('[WebSocket] 已向Electron发送Browser connected状态'); + this.sendWebSocketFrame( + this.wsClients.electron, + JSON.stringify({ + type: 'status', + message: 'Browser connected' + }) + ) + log.info('[WebSocket] 已向Electron发送Browser connected状态') } - return; + return } } // 获取当前连接的角色 - const role = currentRole || (socket as any)._role; + const role = currentRole || (socket as any)._role // 转发消息 if (role === 'browser') { // 浏览器发送的消息转发给Electron if (this.wsClients.electron) { - log.info(`[WebSocket] Browser -> Electron: ${JSON.stringify(data)}`); - this.sendWebSocketFrame(this.wsClients.electron, message); + log.info(`[WebSocket] Browser -> Electron: ${JSON.stringify(data)}`) + this.sendWebSocketFrame(this.wsClients.electron, message) } else { - log.info('[WebSocket] 无法转发消息: Electron客户端未连接'); + log.info('[WebSocket] 无法转发消息: Electron客户端未连接') } } else if (role === 'electron') { // Electron发送的消息转发给浏览器 if (this.wsClients.browser) { - log.info(`[WebSocket] Electron -> Browser: ${JSON.stringify(data)}`); - this.sendWebSocketFrame(this.wsClients.browser, message); + log.info(`[WebSocket] Electron -> Browser: ${JSON.stringify(data)}`) + this.sendWebSocketFrame(this.wsClients.browser, message) } else { - log.info('[WebSocket] 无法转发消息: 浏览器客户端未连接'); + log.info('[WebSocket] 无法转发消息: 浏览器客户端未连接') } } else { - log.info(`[WebSocket] 收到来自未知角色的消息: ${message}`); + log.info(`[WebSocket] 收到来自未知角色的消息: ${message}`) } } } catch (error) { - log.error('[WebSocket] 处理消息错误:', error, message); + log.error('[WebSocket] 处理消息错误:', error, message) } } @@ -490,31 +495,28 @@ export class ASRServerService { * @param pid 进程ID * @returns Promise<{success: boolean, error?: string}> */ - private async stopServer( - _event: Electron.IpcMainInvokeEvent, - _pid?: number - ): Promise<{ success: boolean; error?: string }> { + private async stopServer(): Promise<{ success: boolean; error?: string }> { try { // 关闭HTTP服务器 if (this.httpServer) { - this.httpServer.close(); - this.httpServer = null; + this.httpServer.close() + this.httpServer = null } // 重置客户端连接 - this.wsClients = { browser: null, electron: null }; + this.wsClients = { browser: null, electron: null } // 重置服务器状态 - this.isServerRunning = false; + this.isServerRunning = false - log.info('ASR服务器已停止'); - return { success: true }; + log.info('ASR服务器已停止') + return { success: true } } catch (error) { - log.error('停止ASR服务器失败:', error); - return { success: false, error: (error as Error).message }; + log.error('停止ASR服务器失败:', error) + return { success: false, error: (error as Error).message } } } } // 创建并导出单例 -export const asrServerService = new ASRServerService(); +export const asrServerService = new ASRServerService() diff --git a/src/main/services/AxiosProxy.ts b/src/main/services/AxiosProxy.ts new file mode 100644 index 0000000000..bdac92bd7c --- /dev/null +++ b/src/main/services/AxiosProxy.ts @@ -0,0 +1,27 @@ +import { AxiosInstance, default as axios_ } from 'axios' + +import { proxyManager } from './ProxyManager' + +class AxiosProxy { + private cacheAxios: AxiosInstance | undefined + private proxyURL: string | undefined + + get axios(): AxiosInstance { + const currentProxyURL = proxyManager.getProxyUrl() + if (this.proxyURL !== currentProxyURL) { + this.proxyURL = currentProxyURL + const agent = proxyManager.getProxyAgent() + this.cacheAxios = axios_.create({ + proxy: false, + ...(agent && { httpAgent: agent, httpsAgent: agent }) + }) + } + + if (this.cacheAxios === undefined) { + this.cacheAxios = axios_.create({ proxy: false }) + } + return this.cacheAxios + } +} + +export default new AxiosProxy() diff --git a/src/main/services/CodeExecutorService.ts b/src/main/services/CodeExecutorService.ts index 6224dba43d..3c68c87c7a 100644 --- a/src/main/services/CodeExecutorService.ts +++ b/src/main/services/CodeExecutorService.ts @@ -1,13 +1,12 @@ import { spawn } from 'child_process' +// 如果将来需要使用这些工具函数,可以取消注释 +// import { getBinaryPath, isBinaryExists } from '@main/utils/process' +import log from 'electron-log' import fs from 'fs' import os from 'os' import path from 'path' import { v4 as uuidv4 } from 'uuid' -// 如果将来需要使用这些工具函数,可以取消注释 -// import { getBinaryPath, isBinaryExists } from '@main/utils/process' -import log from 'electron-log' - // 支持的语言类型 export enum CodeLanguage { JavaScript = 'javascript', diff --git a/src/main/services/CopilotService.ts b/src/main/services/CopilotService.ts index e19bb59f56..bc3f2c4afd 100644 --- a/src/main/services/CopilotService.ts +++ b/src/main/services/CopilotService.ts @@ -1,8 +1,10 @@ -import axios, { AxiosRequestConfig } from 'axios' +import { AxiosRequestConfig } from 'axios' import { app, safeStorage } from 'electron' import fs from 'fs/promises' import path from 'path' +import aoxisProxy from './AxiosProxy' + // 配置常量,集中管理 const CONFIG = { GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98', @@ -93,7 +95,7 @@ class CopilotService { } } - const response = await axios.get(CONFIG.API_URLS.GITHUB_USER, config) + const response = await aoxisProxy.axios.get(CONFIG.API_URLS.GITHUB_USER, config) return { login: response.data.login, avatar: response.data.avatar_url @@ -114,7 +116,7 @@ class CopilotService { try { this.updateHeaders(headers) - const response = await axios.post( + const response = await aoxisProxy.axios.post( CONFIG.API_URLS.GITHUB_DEVICE_CODE, { client_id: CONFIG.GITHUB_CLIENT_ID, @@ -146,7 +148,7 @@ class CopilotService { await this.delay(currentDelay) try { - const response = await axios.post( + const response = await aoxisProxy.axios.post( CONFIG.API_URLS.GITHUB_ACCESS_TOKEN, { client_id: CONFIG.GITHUB_CLIENT_ID, @@ -208,7 +210,7 @@ class CopilotService { } } - const response = await axios.get(CONFIG.API_URLS.COPILOT_TOKEN, config) + const response = await aoxisProxy.axios.get(CONFIG.API_URLS.COPILOT_TOKEN, config) return response.data } catch (error) { diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index e43ce55654..dbc3425718 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -1,3 +1,4 @@ +import crypto from 'node:crypto' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' @@ -22,9 +23,12 @@ import { } from '@types' import { app } from 'electron' import Logger from 'electron-log' +import { EventEmitter } from 'events' import { memoize } from 'lodash' import { CacheService } from './CacheService' +import { CallBackServer } from './mcp/oauth/callback' +import { McpOAuthClientProvider } from './mcp/oauth/provider' import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient' // Generic type for caching wrapped functions @@ -117,103 +121,172 @@ class McpService { const args = [...(server.args || [])] - let transport: StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport + const authProvider = new McpOAuthClientProvider({ + serverUrlHash: crypto + .createHash('md5') + .update(server.baseUrl || '') + .digest('hex') + }) try { - // Create appropriate transport based on configuration - if (server.type === 'inMemory') { - 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 || {}) - try { - await inMemoryServer.connect(serverTransport) - Logger.info(`[MCP] In-memory server started: ${server.name}`) - } catch (error: Error | any) { - Logger.error(`[MCP] Error starting in-memory server: ${error}`) - throw new Error(`Failed to start in-memory server: ${error.message}`) - } - // set the client transport to the client - transport = clientTransport - } else if (server.baseUrl) { - if (server.type === 'streamableHttp') { - const options: StreamableHTTPClientTransportOptions = { - requestInit: { - headers: server.headers || {} + const initTransport = async (): Promise< + StdioClientTransport | SSEClientTransport | InMemoryTransport | StreamableHTTPClientTransport + > => { + // Create appropriate transport based on configuration + if (server.type === 'inMemory') { + 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 || {}) + try { + await inMemoryServer.connect(serverTransport) + Logger.info(`[MCP] In-memory server started: ${server.name}`) + } catch (error: Error | any) { + Logger.error(`[MCP] Error starting in-memory server: ${error}`) + throw new Error(`Failed to start in-memory server: ${error.message}`) + } + // return the client transport + return clientTransport + } else if (server.baseUrl) { + if (server.type === 'streamableHttp') { + const options: StreamableHTTPClientTransportOptions = { + requestInit: { + headers: server.headers || {} + }, + authProvider + } + return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options) + } else if (server.type === 'sse') { + const options: SSEClientTransportOptions = { + requestInit: { + headers: server.headers || {} + }, + authProvider + } + return new SSEClientTransport(new URL(server.baseUrl!), options) + } else { + throw new Error('Invalid server type') + } + } else if (server.command) { + let cmd = server.command + + if (server.command === 'npx') { + cmd = await getBinaryPath('bun') + Logger.info(`[MCP] Using command: ${cmd}`) + + // add -x to args if args exist + if (args && args.length > 0) { + if (!args.includes('-y')) { + !args.includes('-y') && args.unshift('-y') + } + if (!args.includes('x')) { + args.unshift('x') + } + } + if (server.registryUrl) { + server.env = { + ...server.env, + NPM_CONFIG_REGISTRY: server.registryUrl + } + + // if the server name is mcp-auto-install, use the mcp-registry.json file in the bin directory + if (server.name.includes('mcp-auto-install')) { + const binPath = await getBinaryPath() + makeSureDirExists(binPath) + server.env.MCP_REGISTRY_PATH = path.join(binPath, '..', 'config', 'mcp-registry.json') + } + } + } else if (server.command === 'uvx' || server.command === 'uv') { + cmd = await getBinaryPath(server.command) + if (server.registryUrl) { + server.env = { + ...server.env, + UV_DEFAULT_INDEX: server.registryUrl, + PIP_INDEX_URL: server.registryUrl + } } } - transport = new StreamableHTTPClientTransport(new URL(server.baseUrl!), options) - } else if (server.type === 'sse') { - const options: SSEClientTransportOptions = { - requestInit: { - headers: server.headers || {} - } - } - transport = new SSEClientTransport(new URL(server.baseUrl!), options) + + Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) + // Logger.info(`[MCP] Environment variables for server:`, server.env) + + const stdioTransport = new StdioClientTransport({ + command: cmd, + args, + env: { + ...getDefaultEnvironment(), + PATH: await this.getEnhancedPath(process.env.PATH || ''), + ...server.env + }, + stderr: 'pipe' + }) + stdioTransport.stderr?.on('data', (data: Buffer) => + Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString()) + ) + return stdioTransport } else { - throw new Error('Invalid server type') + throw new Error('Either baseUrl or command must be provided') } - } else if (server.command) { - let cmd = server.command - - if (server.command === 'npx' || server.command === 'bun' || server.command === 'bunx') { - cmd = await getBinaryPath('bun') - Logger.info(`[MCP] Using command: ${cmd}`) - - // add -x to args if args exist - if (args && args.length > 0) { - if (!args.includes('-y')) { - !args.includes('-y') && args.unshift('-y') - } - if (!args.includes('x')) { - args.unshift('x') - } - } - if (server.registryUrl) { - server.env = { - ...server.env, - NPM_CONFIG_REGISTRY: server.registryUrl - } - - // if the server name is mcp-auto-install, use the mcp-registry.json file in the bin directory - if (server.name.includes('mcp-auto-install')) { - const binPath = await getBinaryPath() - makeSureDirExists(binPath) - server.env.MCP_REGISTRY_PATH = path.join(binPath, '..', 'config', 'mcp-registry.json') - } - } - } else if (server.command === 'uvx' || server.command === 'uv') { - cmd = await getBinaryPath(server.command) - if (server.registryUrl) { - server.env = { - ...server.env, - UV_DEFAULT_INDEX: server.registryUrl, - PIP_INDEX_URL: server.registryUrl - } - } - } - - Logger.info(`[MCP] Starting server with command: ${cmd} ${args ? args.join(' ') : ''}`) - // Logger.info(`[MCP] Environment variables for server:`, server.env) - - transport = new StdioClientTransport({ - command: cmd, - args, - env: { - ...getDefaultEnvironment(), - PATH: await this.getEnhancedPath(process.env.PATH || ''), - ...server.env - }, - stderr: 'pipe' - }) - transport.stderr?.on('data', (data) => - Logger.info(`[MCP] Stdio stderr for server: ${server.name} `, data.toString()) - ) - } else { - throw new Error('Either baseUrl or command must be provided') + // This line is unreachable } - await client.connect(transport) + const handleAuth = async (client: Client, transport: SSEClientTransport | StreamableHTTPClientTransport) => { + Logger.info(`[MCP] Starting OAuth flow for server: ${server.name}`) + // Create an event emitter for the OAuth callback + const events = new EventEmitter() + + // Create a callback server + const callbackServer = new CallBackServer({ + port: authProvider.config.callbackPort, + path: authProvider.config.callbackPath || '/oauth/callback', + events + }) + + // Set a timeout to close the callback server + const timeoutId = setTimeout(() => { + Logger.warn(`[MCP] OAuth flow timed out for server: ${server.name}`) + callbackServer.close() + }, 300000) // 5 minutes timeout + + try { + // Wait for the authorization code + const authCode = await callbackServer.waitForAuthCode() + Logger.info(`[MCP] Received auth code: ${authCode}`) + + // Complete the OAuth flow + await transport.finishAuth(authCode) + + await client.connect(transport) + Logger.info(`[MCP] OAuth flow completed for server: ${server.name}`) + + const newTransport = await initTransport() + // Try to connect again + await client.connect(newTransport) + + Logger.info(`[MCP] Successfully authenticated with server: ${server.name}`) + } catch (oauthError) { + Logger.error(`[MCP] OAuth authentication failed for server ${server.name}:`, oauthError) + throw new Error( + `OAuth authentication failed: ${oauthError instanceof Error ? oauthError.message : String(oauthError)}` + ) + } finally { + // Clear the timeout and close the callback server + clearTimeout(timeoutId) + callbackServer.close() + } + } + + const transport = await initTransport() + try { + await client.connect(transport) + } catch (error: Error | any) { + if (error instanceof Error && (error.name === 'UnauthorizedError' || error.message.includes('Unauthorized'))) { + Logger.info(`[MCP] Authentication required for server: ${server.name}`) + await handleAuth(client, transport as SSEClientTransport | StreamableHTTPClientTransport) + } else { + throw error + } + } // Store the new client in the cache this.clients.set(serverKey, client) @@ -514,15 +587,12 @@ class McpService { // 根据不同的 shell 构建不同的命令 if (userShell.includes('zsh')) { - shell = '/bin/zsh' command = 'source /etc/zshenv 2>/dev/null || true; source ~/.zshenv 2>/dev/null || true; source /etc/zprofile 2>/dev/null || true; source ~/.zprofile 2>/dev/null || true; source /etc/zshrc 2>/dev/null || true; source ~/.zshrc 2>/dev/null || true; source /etc/zlogin 2>/dev/null || true; source ~/.zlogin 2>/dev/null || true; echo $PATH' } else if (userShell.includes('bash')) { - shell = '/bin/bash' command = 'source /etc/profile 2>/dev/null || true; source ~/.bash_profile 2>/dev/null || true; source ~/.bash_login 2>/dev/null || true; source ~/.profile 2>/dev/null || true; source ~/.bashrc 2>/dev/null || true; echo $PATH' } else if (userShell.includes('fish')) { - shell = '/bin/fish' command = 'source /etc/fish/config.fish 2>/dev/null || true; source ~/.config/fish/config.fish 2>/dev/null || true; source ~/.config/fish/config.local.fish 2>/dev/null || true; echo $PATH' } else { @@ -540,15 +610,15 @@ class McpService { }) let path = '' - child.stdout.on('data', (data) => { + child.stdout.on('data', (data: Buffer) => { path += data.toString() }) - child.stderr.on('data', (data) => { + child.stderr.on('data', (data: Buffer) => { console.error('Error getting PATH:', data.toString()) }) - child.on('close', (code) => { + child.on('close', (code: number) => { if (code === 0) { const trimmedPath = path.trim() resolve(trimmedPath) diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 7c28709c59..2f1e40d960 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -405,6 +405,7 @@ export class WindowService { this.miniWindow?.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) //make miniWindow always on top of fullscreen apps with level set //[mac] level higher than 'floating' will cover the pinyin input method + //[win] level 'floating' will cover the pinyin input method this.miniWindow.setAlwaysOnTop(true, 'floating') this.miniWindow.on('ready-to-show', () => { diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts new file mode 100644 index 0000000000..6884c53043 --- /dev/null +++ b/src/main/services/mcp/oauth/callback.ts @@ -0,0 +1,76 @@ +import Logger from 'electron-log' +import EventEmitter from 'events' +import http from 'http' +import { URL } from 'url' + +import { OAuthCallbackServerOptions } from './types' + +export class CallBackServer { + private server: Promise + private events: EventEmitter + + constructor(options: OAuthCallbackServerOptions) { + const { port, path, events } = options + this.events = events + this.server = this.initialize(port, path) + } + + initialize(port: number, path: string): Promise { + const server = http.createServer((req, res) => { + // Only handle requests to the callback path + if (req.url?.startsWith(path)) { + try { + // Parse the URL to extract the authorization code + const url = new URL(req.url, `http://localhost:${port}`) + const code = url.searchParams.get('code') + if (code) { + // Emit the code event + this.events.emit('auth-code-received', code) + } + } catch (error) { + Logger.error('Error processing OAuth callback:', error) + res.writeHead(500, { 'Content-Type': 'text/plain' }) + res.end('Internal Server Error') + } + } else { + // Not a callback request + res.writeHead(404, { 'Content-Type': 'text/plain' }) + res.end('Not Found') + } + }) + + // Handle server errors + server.on('error', (error) => { + Logger.error('OAuth callback server error:', error) + }) + + const runningServer = new Promise((resolve, reject) => { + server.listen(port, () => { + Logger.info(`OAuth callback server listening on port ${port}`) + resolve(server) + }) + + server.on('error', (error) => { + reject(error) + }) + }) + return runningServer + } + + get getServer(): Promise { + return this.server + } + + async close() { + const server = await this.server + server.close() + } + + async waitForAuthCode(): Promise { + return new Promise((resolve) => { + this.events.once('auth-code-received', (code) => { + resolve(code) + }) + }) + } +} diff --git a/src/main/services/mcp/oauth/provider.ts b/src/main/services/mcp/oauth/provider.ts new file mode 100644 index 0000000000..e56fada6af --- /dev/null +++ b/src/main/services/mcp/oauth/provider.ts @@ -0,0 +1,78 @@ +import path from 'node:path' + +import { getConfigDir } from '@main/utils/file' +import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth' +import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth' +import Logger from 'electron-log' +import open from 'open' + +import { JsonFileStorage } from './storage' +import { OAuthProviderOptions } from './types' + +export class McpOAuthClientProvider implements OAuthClientProvider { + private storage: JsonFileStorage + public readonly config: Required + + constructor(options: OAuthProviderOptions) { + const configDir = path.join(getConfigDir(), 'mcp', 'oauth') + this.config = { + serverUrlHash: options.serverUrlHash, + callbackPort: options.callbackPort || 12346, + callbackPath: options.callbackPath || '/oauth/callback', + configDir: options.configDir || configDir, + clientName: options.clientName || 'Cherry Studio', + clientUri: options.clientUri || 'https://github.com/CherryHQ/cherry-studio' + } + this.storage = new JsonFileStorage(this.config.serverUrlHash, this.config.configDir) + } + + get redirectUrl(): string { + return `http://localhost:${this.config.callbackPort}${this.config.callbackPath}` + } + + get clientMetadata() { + return { + redirect_uris: [this.redirectUrl], + token_endpoint_auth_method: 'none', + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + client_name: this.config.clientName, + client_uri: this.config.clientUri + } + } + + async clientInformation(): Promise { + return this.storage.getClientInformation() + } + + async saveClientInformation(info: OAuthClientInformationFull): Promise { + await this.storage.saveClientInformation(info) + } + + async tokens(): Promise { + return this.storage.getTokens() + } + + async saveTokens(tokens: OAuthTokens): Promise { + await this.storage.saveTokens(tokens) + } + + async redirectToAuthorization(authorizationUrl: URL): Promise { + try { + // Open the browser to the authorization URL + await open(authorizationUrl.toString()) + Logger.info('Browser opened automatically.') + } catch (error) { + Logger.error('Could not open browser automatically.') + throw error // Let caller handle the error + } + } + + async saveCodeVerifier(codeVerifier: string): Promise { + await this.storage.saveCodeVerifier(codeVerifier) + } + + async codeVerifier(): Promise { + return this.storage.getCodeVerifier() + } +} diff --git a/src/main/services/mcp/oauth/storage.ts b/src/main/services/mcp/oauth/storage.ts new file mode 100644 index 0000000000..349fcf8bf1 --- /dev/null +++ b/src/main/services/mcp/oauth/storage.ts @@ -0,0 +1,120 @@ +import { + OAuthClientInformation, + OAuthClientInformationFull, + OAuthTokens +} from '@modelcontextprotocol/sdk/shared/auth.js' +import Logger from 'electron-log' +import fs from 'fs/promises' +import path from 'path' + +import { IOAuthStorage, OAuthStorageData, OAuthStorageSchema } from './types' + +export class JsonFileStorage implements IOAuthStorage { + private readonly filePath: string + private cache: OAuthStorageData | null = null + + constructor( + readonly serverUrlHash: string, + configDir: string + ) { + this.filePath = path.join(configDir, `${serverUrlHash}_oauth.json`) + } + + private async readStorage(): Promise { + if (this.cache) { + return this.cache + } + + try { + const data = await fs.readFile(this.filePath, 'utf-8') + const parsed = JSON.parse(data) + const validated = OAuthStorageSchema.parse(parsed) + this.cache = validated + return validated + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + // File doesn't exist, return initial state + const initial: OAuthStorageData = { lastUpdated: Date.now() } + await this.writeStorage(initial) + return initial + } + Logger.error('Error reading OAuth storage:', error) + throw new Error(`Failed to read OAuth storage: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private async writeStorage(data: OAuthStorageData): Promise { + try { + // Ensure directory exists + await fs.mkdir(path.dirname(this.filePath), { recursive: true }) + + // Update timestamp + data.lastUpdated = Date.now() + + // Write file atomically + const tempPath = `${this.filePath}.tmp` + await fs.writeFile(tempPath, JSON.stringify(data, null, 2)) + await fs.rename(tempPath, this.filePath) + + // Update cache + this.cache = data + } catch (error) { + Logger.error('Error writing OAuth storage:', error) + throw new Error(`Failed to write OAuth storage: ${error instanceof Error ? error.message : String(error)}`) + } + } + + async getClientInformation(): Promise { + const data = await this.readStorage() + return data.clientInfo + } + + async saveClientInformation(info: OAuthClientInformationFull): Promise { + const data = await this.readStorage() + await this.writeStorage({ + ...data, + clientInfo: info + }) + } + + async getTokens(): Promise { + const data = await this.readStorage() + return data.tokens + } + + async saveTokens(tokens: OAuthTokens): Promise { + const data = await this.readStorage() + await this.writeStorage({ + ...data, + tokens + }) + } + + async getCodeVerifier(): Promise { + const data = await this.readStorage() + if (!data.codeVerifier) { + throw new Error('No code verifier saved for session') + } + return data.codeVerifier + } + + async saveCodeVerifier(codeVerifier: string): Promise { + const data = await this.readStorage() + await this.writeStorage({ + ...data, + codeVerifier + }) + } + + async clear(): Promise { + try { + await fs.unlink(this.filePath) + this.cache = null + } catch (error) { + if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { + Logger.error('Error clearing OAuth storage:', error) + throw new Error(`Failed to clear OAuth storage: ${error instanceof Error ? error.message : String(error)}`) + } + } + } +} diff --git a/src/main/services/mcp/oauth/types.ts b/src/main/services/mcp/oauth/types.ts new file mode 100644 index 0000000000..de631c1629 --- /dev/null +++ b/src/main/services/mcp/oauth/types.ts @@ -0,0 +1,61 @@ +import { + OAuthClientInformation, + OAuthClientInformationFull, + OAuthTokens +} from '@modelcontextprotocol/sdk/shared/auth.js' +import EventEmitter from 'events' +import { z } from 'zod' + +export interface OAuthStorageData { + clientInfo?: OAuthClientInformation + tokens?: OAuthTokens + codeVerifier?: string + lastUpdated: number +} + +export const OAuthStorageSchema = z.object({ + clientInfo: z.any().optional(), + tokens: z.any().optional(), + codeVerifier: z.string().optional(), + lastUpdated: z.number() +}) + +export interface IOAuthStorage { + getClientInformation(): Promise + saveClientInformation(info: OAuthClientInformationFull): Promise + getTokens(): Promise + saveTokens(tokens: OAuthTokens): Promise + getCodeVerifier(): Promise + saveCodeVerifier(codeVerifier: string): Promise + clear(): Promise +} + +/** + * OAuth callback server setup options + */ +export interface OAuthCallbackServerOptions { + /** Port for the callback server */ + port: number + /** Path for the callback endpoint */ + path: string + /** Event emitter to signal when auth code is received */ + events: EventEmitter +} + +/** + * Options for creating an OAuth client provider + */ +export interface OAuthProviderOptions { + /** Server URL to connect to */ + serverUrlHash: string + /** Port for the OAuth callback server */ + callbackPort?: number + /** Path for the OAuth callback endpoint */ + callbackPath?: string + /** Directory to store OAuth credentials */ + configDir?: string + /** Client name to use for OAuth registration */ + clientName?: string + /** Client URI to use for OAuth registration */ + clientUri?: string +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 67a01ac50b..eee4505844 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -33,6 +33,7 @@ declare global { clearCache: () => Promise<{ success: boolean; error?: string }> system: { getDeviceType: () => Promise<'mac' | 'windows' | 'linux'> + getHostname: () => Promise } zip: { compress: (text: string) => Promise @@ -207,11 +208,11 @@ declare global { deleteShortMemoryById: (id: string) => Promise loadLongTermData: () => Promise saveLongTermData: (data: any, forceOverwrite?: boolean) => Promise - }, + } asrServer: { startServer: () => Promise<{ success: boolean; pid?: number; port?: number; error?: string }> stopServer: (pid: number) => Promise<{ success: boolean; error?: string }> - }, + } pdf: { splitPDF: (file: FileType, pageRange: string) => Promise } diff --git a/src/preload/index.ts b/src/preload/index.ts index 9a5ccc845e..ec9bda3c8d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -22,7 +22,8 @@ const api = { openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), system: { - getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType) + getDeviceType: () => ipcRenderer.invoke(IpcChannel.System_GetDeviceType), + getHostname: () => ipcRenderer.invoke(IpcChannel.System_GetHostname) }, zip: { compress: (text: string) => ipcRenderer.invoke(IpcChannel.Zip_Compress, text), diff --git a/src/renderer/src/assets/images/apps/zai.png b/src/renderer/src/assets/images/apps/zai.png new file mode 100644 index 0000000000..3e1db9650f --- /dev/null +++ b/src/renderer/src/assets/images/apps/zai.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF8WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDI1LTA0LTE5VDIyOjE1OjI3KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNS0wNC0xOVQyMjoxNjoxMCswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNS0wNC0xOVQyMjoxNjoxMCswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2YzRlZjRmZC1hMzA0LTRkNDQtOWM0Yy1mZjk3MzA5ZDRkMzAiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo3ZDlmNzZiYS1jMzA1LTExZWQtOGM1Ny1mMTQzNzA3ZWM4ZjAiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo2YzRlZjRmZC1hMzA0LTRkNDQtOWM0Yy1mZjk3MzA5ZDRkMzAiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjZjNGVmNGZkLWEzMDQtNGQ0NC05YzRjLWZmOTczMDlkNGQzMCIgc3RFdnQ6d2hlbj0iMjAyNS0wNC0xOVQyMjoxNToyNyswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKFdpbmRvd3MpIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgEk4MkAAAiASURBVHic7Z1bbBRVGMd/Z7rtLqUUWmgLpRQQEMFbQUVRo4KXRB+M8YFo9MFEI2h8MvHyYIwxPnl5wMQYb9EYb4kmxqgxxvsF8IIo3kBuQqGl0Jbdnd3j4+zU2e3Mzs7Mzpk53f1/SdPOzM6e8/2/851zvnNmzoBlDh7Zt2jw1hNXDw4NnDo4NHAqjnOWYvlMYFngZeAJYMD9+3bgIuBT4FXgUOgRWgYHh+4bvPWEO4eGTjt1cGjgVOAU4GTgZGAJsBRYBiwHVgKrgVXAGmAtsA5YD2wANgKbgM3AFuB4YCtwArANOBHYDpwE7ABOBXYCG4BdwGnA6cAZwJnAWcDZwDnAucB5wPnABcCFwEXAxcAlwKXAZcDlwBXAlcBVwNXANcC1wHXA9cANwI3ATcDNwC3ArcBtwO3AHcCdwF3A3cA9wL3AfcD9wAPAg8BDwMPAI8CjwGPADuBx4AngSeAp4GngGeA54HngBeAl4GXgFeBV4DXgdeAN4E3gLeAd4F3gPeB94APgQ+Aj4GPgE+BT4DPgc+AL4EvgK+Br4BvgW+A74Hvgh8Gfj/wI/AT8DPwC/Ar8BvwO/AH8CfwF/A38A/wL/Af8D/wHHAYOA0eAo8AxYAQYBcaAcWACmASmgGlgBpgF5oB5oAJUgSqggCZQA+pAA2gCLaANdIAu0AP6QAkYAMrAIDAEjABjwDgwAUwCU8A0MAPMAnPAPFABqkAVaAJ1oAE0gRbQBjpAF+gBfaAEDIAyMAiMAKPAGDAOTACTwBQwDcwAs8AcMA9UgCpQBZpAHWgATaAFtIEO0AV6QB8oAQNgGBgERoBRYAwYByaASWAKmAZmgFlgDpgHKkAVqAJNoA40gCbQAtpAB+gCPaAPlIABMAwMAiPAKDAGjAMTwCQwBUwDM8AsMAfMAxWgClSBJlAHGkATaAFtoAN0gR7QB0rAABgGBoERYBQYA8aBCWASmAKmgRlgFpgD5oEKUAWqQBOoAw2gCbSANtABukAP6AMlYAAMA4PACDAKjAHjwAQwCUwB08AMMAvMAfNABagCVaAJ1IEG0ARaQBvoAF2gB/SBEjAAhoFBYAQYBcaAcWACmASmgGlgBpgF5oB5oAJUgSrQBOpAA2gCLaANdIAu0AP6QAkYAMPAIDAC/A9QXvgfAE4AAAABJRU5ErkJggg== diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss index 84aadd1d66..9721f36126 100644 --- a/src/renderer/src/assets/styles/index.scss +++ b/src/renderer/src/assets/styles/index.scss @@ -260,6 +260,7 @@ body, .markdown, .anticon, .iconfont, + .lucide, .message-tokens { color: var(--chat-text-user) !important; } diff --git a/src/renderer/src/components/AssistantMemoryPopup.tsx b/src/renderer/src/components/AssistantMemoryPopup.tsx index 4f4ccb72a0..64938eed62 100644 --- a/src/renderer/src/components/AssistantMemoryPopup.tsx +++ b/src/renderer/src/components/AssistantMemoryPopup.tsx @@ -1,11 +1,11 @@ +import { DeleteOutlined } from '@ant-design/icons' import { addAssistantMemoryItem } from '@renderer/services/MemoryService' import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { deleteAssistantMemory } from '@renderer/store/memory' import { Button, Empty, Input, List, Modal, Tooltip, Typography } from 'antd' -import { DeleteOutlined } from '@ant-design/icons' import { useCallback, useState } from 'react' -import { Provider } from 'react-redux' import { useTranslation } from 'react-i18next' +import { Provider } from 'react-redux' import styled from 'styled-components' const { Text } = Typography diff --git a/src/renderer/src/components/CodeExecutorButton/ExecutionResult.tsx b/src/renderer/src/components/CodeExecutorButton/ExecutionResult.tsx index 6d23919f01..aea246cb53 100644 --- a/src/renderer/src/components/CodeExecutorButton/ExecutionResult.tsx +++ b/src/renderer/src/components/CodeExecutorButton/ExecutionResult.tsx @@ -1,7 +1,7 @@ -import React from 'react' -import styled from 'styled-components' -import { useTranslation } from 'react-i18next' import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons' +import React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' export interface ExecutionResultProps { success: boolean diff --git a/src/renderer/src/components/CodeMirrorEditor/ChineseSearchPanel.ts b/src/renderer/src/components/CodeMirrorEditor/ChineseSearchPanel.ts index 3ec04ed34e..5e243410a0 100644 --- a/src/renderer/src/components/CodeMirrorEditor/ChineseSearchPanel.ts +++ b/src/renderer/src/components/CodeMirrorEditor/ChineseSearchPanel.ts @@ -1,5 +1,5 @@ +import { openSearchPanel, search } from '@codemirror/search' import { Extension } from '@codemirror/state' -import { search, openSearchPanel } from '@codemirror/search' import { EditorView } from '@codemirror/view' // 创建中文搜索面板 diff --git a/src/renderer/src/components/CodeMirrorEditor/index.tsx b/src/renderer/src/components/CodeMirrorEditor/index.tsx index 4a2a65d540..ed5d1f1b43 100644 --- a/src/renderer/src/components/CodeMirrorEditor/index.tsx +++ b/src/renderer/src/components/CodeMirrorEditor/index.tsx @@ -1,34 +1,33 @@ -import { EditorState } from '@codemirror/state' -import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view' -import { defaultKeymap, history, historyKeymap, undo, redo, indentWithTab } from '@codemirror/commands' -import { syntaxHighlighting, HighlightStyle } from '@codemirror/language' -import { tags } from '@lezer/highlight' -import { javascript } from '@codemirror/lang-javascript' -import { python } from '@codemirror/lang-python' -import { html } from '@codemirror/lang-html' -import { css } from '@codemirror/lang-css' -import { json } from '@codemirror/lang-json' -import { markdown } from '@codemirror/lang-markdown' -import { cpp } from '@codemirror/lang-cpp' -import { java } from '@codemirror/lang-java' -import { php } from '@codemirror/lang-php' -import { rust } from '@codemirror/lang-rust' -import { sql } from '@codemirror/lang-sql' -import { xml } from '@codemirror/lang-xml' -import { vue } from '@codemirror/lang-vue' -import { oneDark } from '@codemirror/theme-one-dark' -import { autocompletion } from '@codemirror/autocomplete' -import { searchKeymap } from '@codemirror/search' -import { createChineseSearchPanel, openChineseSearchPanel } from './ChineseSearchPanel' -import { useTheme } from '@renderer/context/ThemeProvider' -import { ThemeMode } from '@renderer/types' -import { useEffect, useRef, useMemo, forwardRef, useImperativeHandle } from 'react' -import styled from 'styled-components' - import './styles.css' import './ChineseSearchPanel.css' +import { autocompletion } from '@codemirror/autocomplete' +import { defaultKeymap, history, historyKeymap, indentWithTab, redo, undo } from '@codemirror/commands' +import { cpp } from '@codemirror/lang-cpp' +import { css } from '@codemirror/lang-css' +import { html } from '@codemirror/lang-html' +import { java } from '@codemirror/lang-java' +import { javascript } from '@codemirror/lang-javascript' +import { json } from '@codemirror/lang-json' +import { markdown } from '@codemirror/lang-markdown' +import { php } from '@codemirror/lang-php' +import { python } from '@codemirror/lang-python' +import { rust } from '@codemirror/lang-rust' +import { sql } from '@codemirror/lang-sql' +import { vue } from '@codemirror/lang-vue' +import { xml } from '@codemirror/lang-xml' +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' +import { searchKeymap } from '@codemirror/search' +import { EditorState } from '@codemirror/state' +import { oneDark } from '@codemirror/theme-one-dark' +import { EditorView, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view' +import { tags } from '@lezer/highlight' +import { useTheme } from '@renderer/context/ThemeProvider' +import { ThemeMode } from '@renderer/types' +import { useEffect, useImperativeHandle, useMemo, useRef } from 'react' +import styled from 'styled-components' +import { createChineseSearchPanel, openChineseSearchPanel } from './ChineseSearchPanel' // 自定义语法高亮样式 const lightThemeHighlightStyle = HighlightStyle.define([ @@ -54,7 +53,7 @@ const lightThemeHighlightStyle = HighlightStyle.define([ { tag: tags.heading, color: '#800000', fontWeight: 'bold' }, { tag: tags.link, color: '#0000ff', textDecoration: 'underline' }, { tag: tags.emphasis, fontStyle: 'italic' }, - { tag: tags.strong, fontWeight: 'bold' }, + { tag: tags.strong, fontWeight: 'bold' } ]) // 暗色主题语法高亮样式 @@ -81,7 +80,7 @@ const darkThemeHighlightStyle = HighlightStyle.define([ { tag: tags.heading, color: '#569cd6', fontWeight: 'bold' }, { tag: tags.link, color: '#569cd6', textDecoration: 'underline' }, { tag: tags.emphasis, fontStyle: 'italic' }, - { tag: tags.strong, fontWeight: 'bold' }, + { tag: tags.strong, fontWeight: 'bold' } ]) export interface CodeMirrorEditorRef { @@ -149,18 +148,16 @@ const getLanguageExtension = (language: string) => { } } -const CodeMirrorEditor = forwardRef(( - { - code, - language, - onChange, - readOnly = false, - showLineNumbers = true, - fontSize = 14, - height = 'auto' - }, - ref -) => { +const CodeMirrorEditor = ({ + ref, + code, + language, + onChange, + readOnly = false, + showLineNumbers = true, + fontSize = 14, + height = 'auto' +}: CodeMirrorEditorProps & { ref?: React.RefObject }) => { const editorRef = useRef(null) const editorViewRef = useRef(null) const { theme } = useTheme() @@ -223,13 +220,11 @@ const CodeMirrorEditor = forwardRef( const languageExtension = getLanguageExtension(language) // 监听编辑器所有更新 - const updateListener = EditorView.updateListener.of(update => { + const updateListener = EditorView.updateListener.of((update) => { // 当文档变化时更新内部状态 if (update.docChanged) { // 检查是否是撤销/重做操作 - const isUndoRedo = update.transactions.some(tr => - tr.isUserEvent('undo') || tr.isUserEvent('redo') - ) + const isUndoRedo = update.transactions.some((tr) => tr.isUserEvent('undo') || tr.isUserEvent('redo')) // 记录所有文档变化,但只在撤销/重做时触发 onChange if (isUndoRedo && onChange) { @@ -247,9 +242,9 @@ const CodeMirrorEditor = forwardRef( ...historyKeymap, ...searchKeymap, indentWithTab, - { key: "Mod-z", run: undo }, - { key: "Mod-y", run: redo }, - { key: "Mod-Shift-z", run: redo } + { key: 'Mod-z', run: undo }, + { key: 'Mod-y', run: redo }, + { key: 'Mod-Shift-z', run: redo } ]), syntaxHighlighting(highlightStyle), languageExtension, @@ -298,7 +293,7 @@ const CodeMirrorEditor = forwardRef( }, [code, language, onChange, readOnly, showLineNumbers, theme, fontSize, height]) return -}); +} const EditorContainer = styled.div` width: 100%; diff --git a/src/renderer/src/components/DeepClaudeProvider.tsx b/src/renderer/src/components/DeepClaudeProvider.tsx index 0ad38dbff8..9d21bbee10 100644 --- a/src/renderer/src/components/DeepClaudeProvider.tsx +++ b/src/renderer/src/components/DeepClaudeProvider.tsx @@ -1,8 +1,11 @@ -import { useEffect } from 'react' import store, { useAppDispatch, useAppSelector } from '@renderer/store' import { addProvider, removeProvider } from '@renderer/store/llm' import { Provider } from '@renderer/types' -import { createAllDeepClaudeProviders, checkModelCombinationsInLocalStorage } from '@renderer/utils/createDeepClaudeProvider' +import { + checkModelCombinationsInLocalStorage, + createAllDeepClaudeProviders +} from '@renderer/utils/createDeepClaudeProvider' +import { useEffect } from 'react' /** * DeepClaudeProvider组件 @@ -10,7 +13,7 @@ import { createAllDeepClaudeProviders, checkModelCombinationsInLocalStorage } fr */ const DeepClaudeProvider = () => { const dispatch = useAppDispatch() - const providers = useAppSelector(state => state.llm.providers) + const providers = useAppSelector((state) => state.llm.providers) // 监听localStorage中的modelCombinations变化 useEffect(() => { @@ -41,9 +44,9 @@ const DeepClaudeProvider = () => { checkModelCombinationsInLocalStorage() // 移除所有现有的DeepClaude提供商 - const existingDeepClaudeProviders = providers.filter(p => p.type === 'deepclaude') + const existingDeepClaudeProviders = providers.filter((p) => p.type === 'deepclaude') console.log('[DeepClaudeProvider] 移除现有DeepClaude提供商数量:', existingDeepClaudeProviders.length) - existingDeepClaudeProviders.forEach(provider => { + existingDeepClaudeProviders.forEach((provider) => { dispatch(removeProvider(provider)) }) @@ -52,21 +55,30 @@ const DeepClaudeProvider = () => { console.log('[DeepClaudeProvider] 创建的DeepClaude提供商数量:', deepClaudeProviders.length) // 列出所有提供商,便于调试 - console.log('[DeepClaudeProvider] 当前所有提供商:', - providers.map(p => ({ id: p.id, name: p.name, type: p.type }))) + console.log( + '[DeepClaudeProvider] 当前所有提供商:', + providers.map((p) => ({ id: p.id, name: p.name, type: p.type })) + ) // 添加DeepClaude提供商 - deepClaudeProviders.forEach(provider => { - console.log('[DeepClaudeProvider] 添加DeepClaude提供商:', provider.id, provider.name, provider.type, - provider.models.length > 0 ? `包含${provider.models.length}个模型` : '无模型') + deepClaudeProviders.forEach((provider) => { + console.log( + '[DeepClaudeProvider] 添加DeepClaude提供商:', + provider.id, + provider.name, + provider.type, + provider.models.length > 0 ? `包含${provider.models.length}个模型` : '无模型' + ) dispatch(addProvider(provider)) }) // 再次列出所有提供商,确认添加成功 setTimeout(() => { const currentProviders = store.getState().llm.providers - console.log('[DeepClaudeProvider] 添加后的所有提供商:', - currentProviders.map((p: Provider) => ({ id: p.id, name: p.name, type: p.type }))) + console.log( + '[DeepClaudeProvider] 添加后的所有提供商:', + currentProviders.map((p: Provider) => ({ id: p.id, name: p.name, type: p.type })) + ) console.log('[DeepClaudeProvider] DeepClaude提供商加载完成') }, 100) } diff --git a/src/renderer/src/components/PDFSettingsInitializer.tsx b/src/renderer/src/components/PDFSettingsInitializer.tsx index 6d542a229b..10e1659767 100644 --- a/src/renderer/src/components/PDFSettingsInitializer.tsx +++ b/src/renderer/src/components/PDFSettingsInitializer.tsx @@ -1,8 +1,8 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' +import { useSettings } from '@renderer/hooks/useSettings' import store from '@renderer/store' import { setPdfSettings } from '@renderer/store/settings' -import { useSettings } from '@renderer/hooks/useSettings' +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' /** * 用于在应用启动时初始化PDF设置 @@ -41,10 +41,12 @@ const PDFSettingsInitializer = () => { // 如果设置仍然不正确,再次强制设置 if (!state.settings.pdfSettings?.enablePdfSplitting) { console.log('[PDFSettingsInitializer] Settings still incorrect, forcing again') - dispatch(setPdfSettings({ - ...state.settings.pdfSettings, - enablePdfSplitting: true - })) + dispatch( + setPdfSettings({ + ...state.settings.pdfSettings, + enablePdfSplitting: true + }) + ) } }, 1000) diff --git a/src/renderer/src/components/Popups/AssistantMemoryPopup.tsx b/src/renderer/src/components/Popups/AssistantMemoryPopup.tsx index 6daca680eb..d84a39913d 100644 --- a/src/renderer/src/components/Popups/AssistantMemoryPopup.tsx +++ b/src/renderer/src/components/Popups/AssistantMemoryPopup.tsx @@ -58,7 +58,9 @@ const PopupContainer: React.FC = ({ assistantId, resolve }) => { createdAt: string } - const assistantMemories = useAppSelector((state) => selectAssistantMemoriesByAssistantId(state, assistantId)) as AssistantMemory[] + const assistantMemories = useAppSelector((state) => + selectAssistantMemoriesByAssistantId(state, assistantId) + ) as AssistantMemory[] // 获取分析统计数据 const totalAnalyses = useAppSelector((state) => state.memory?.analysisStats?.totalAnalyses || 0) @@ -217,7 +219,13 @@ const PopupContainer: React.FC = ({ assistantId, resolve }) => { )} /> ) : ( - + )} diff --git a/src/renderer/src/components/Popups/SelectModelPopup.tsx b/src/renderer/src/components/Popups/SelectModelPopup.tsx index f74f79d94e..dae30881d6 100644 --- a/src/renderer/src/components/Popups/SelectModelPopup.tsx +++ b/src/renderer/src/components/Popups/SelectModelPopup.tsx @@ -89,8 +89,7 @@ const PopupContainer: React.FC = ({ model: activeModel, res // 缓存所有模型列表,只在providers变化时重新计算 const allModels = useMemo(() => { - return providers.flatMap((p) => p.models || []) - .filter((m) => !isEmbeddingModel(m) && !isRerankModel(m)) + return providers.flatMap((p) => p.models || []).filter((m) => !isEmbeddingModel(m) && !isRerankModel(m)) }, [providers]) // --- Filter Models for Right Column --- @@ -131,10 +130,13 @@ const PopupContainer: React.FC = ({ model: activeModel, res setSelectedProviderId(providerId) }, []) - const handleModelSelect = useCallback((model: Model) => { - resolve(model) - setOpen(false) - }, [resolve, setOpen]) + const handleModelSelect = useCallback( + (model: Model) => { + resolve(model) + setOpen(false) + }, + [resolve, setOpen] + ) const onCancel = useCallback(() => { setOpen(false) @@ -202,14 +204,17 @@ const PopupContainer: React.FC = ({ model: activeModel, res ref={inputRef} placeholder={t('models.search')} value={searchText} - onChange={useCallback((e: React.ChangeEvent) => { - const value = e.target.value - setSearchText(value) - // 当搜索时,自动选择"all"供应商,以显示所有匹配的模型 - if (value.trim() && selectedProviderId !== 'all') { - setSelectedProviderId('all') - } - }, [selectedProviderId, t])} + onChange={useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value + setSearchText(value) + // 当搜索时,自动选择"all"供应商,以显示所有匹配的模型 + if (value.trim() && selectedProviderId !== 'all') { + setSelectedProviderId('all') + } + }, + [selectedProviderId, t] + )} // 移除焦点事件处理 allowClear autoFocus @@ -266,7 +271,9 @@ const PopupContainer: React.FC = ({ model: activeModel, res {/* Show provider only if not in pinned view or if search is active */} {(selectedProviderId !== PINNED_PROVIDER_ID || searchText) && ( - p.id === m.provider)?.name ?? m.provider} mouseEnterDelay={0.5}> + p.id === m.provider)?.name ?? m.provider} + mouseEnterDelay={0.5}> | {providers.find((p) => p.id === m.provider)?.name ?? m.provider} @@ -382,12 +389,14 @@ const ModelListItem = styled.div<{ $selected: boolean }>` &:hover { background-color: var(--color-background-mute); - .pin-button, .settings-button { + .pin-button, + .settings-button { opacity: 0.5; // Show buttons on hover } } - .pin-button, .settings-button { + .pin-button, + .settings-button { opacity: ${(props) => (props.$selected ? 0.5 : 0)}; // Show if selected or hovered transition: opacity 0.2s; &:hover { diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx index 52c334506e..f42e9fee92 100644 --- a/src/renderer/src/components/QuickPanel/view.tsx +++ b/src/renderer/src/components/QuickPanel/view.tsx @@ -82,11 +82,18 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { return true } + const pattern = lowerSearchText.split('').join('.*') if (tinyPinyin.isSupported() && /[\u4e00-\u9fa5]/.test(filterText)) { - const pinyinText = tinyPinyin.convertToPinyin(filterText, '', true) - if (pinyinText.toLowerCase().includes(lowerSearchText)) { + try { + const pinyinText = tinyPinyin.convertToPinyin(filterText, '', true).toLowerCase() + const regex = new RegExp(pattern, 'ig') + return regex.test(pinyinText) + } catch (error) { return true } + } else { + const regex = new RegExp(pattern, 'ig') + return regex.test(filterText.toLowerCase()) } return false @@ -206,6 +213,7 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement const handleInput = (e: Event) => { + if (isComposing.current) return const target = e.target as HTMLTextAreaElement const cursorPosition = target.selectionStart const textBeforeCursor = target.value.slice(0, cursorPosition) @@ -225,8 +233,9 @@ export const QuickPanelView: React.FC = ({ setInputText }) => { isComposing.current = true } - const handleCompositionEnd = () => { + const handleCompositionEnd = (e: CompositionEvent) => { isComposing.current = false + handleInput(e) } textArea.addEventListener('input', handleInput) diff --git a/src/renderer/src/components/WebdavModals.tsx b/src/renderer/src/components/WebdavModals.tsx index 89ad47aa78..5d9b1bd10f 100644 --- a/src/renderer/src/components/WebdavModals.tsx +++ b/src/renderer/src/components/WebdavModals.tsx @@ -42,8 +42,9 @@ export function useWebdavBackupModal({ backupMethod }: { backupMethod?: typeof b const showBackupModal = useCallback(async () => { // 获取默认文件名 const deviceType = await window.api.system.getDeviceType() + const hostname = await window.api.system.getHostname() const timestamp = dayjs().format('YYYYMMDDHHmmss') - const defaultFileName = `cherry-studio.${timestamp}.${deviceType}.zip` + const defaultFileName = `cherry-studio.${timestamp}.${hostname}.${deviceType}.zip` setCustomFileName(defaultFileName) setIsModalVisible(true) }, []) diff --git a/src/renderer/src/components/app/Navbar.tsx b/src/renderer/src/components/app/Navbar.tsx index 6d5844bc2e..bad3b5c3df 100644 --- a/src/renderer/src/components/app/Navbar.tsx +++ b/src/renderer/src/components/app/Navbar.tsx @@ -58,7 +58,8 @@ const NavbarCenterContainer = styled.div` color: var(--color-text-1); /* 确保标题区域的按钮可点击 */ - & button, & a { + & button, + & a { -webkit-app-region: no-drag; } ` @@ -70,7 +71,6 @@ const NavbarRightContainer = styled.div` padding: 0 12px; padding-right: ${isWindows ? '140px' : 12}; justify-content: flex-end; - -webkit-app-region: no-drag; /* 确保按钮可点击 */ /* 确保所有子元素都可点击 */ & > * { diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index ea65a999b9..8bbaab5239 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -158,10 +158,13 @@ const visionAllowedModels = [ 'grok-vision-beta', 'pixtral', 'gpt-4(?:-[\\w-]+)', + 'gpt-4.1(?:-[\\w-]+)?', 'gpt-4o(?:-[\\w-]+)?', 'gpt-4.5(?:-[\\w-]+)', 'chatgpt-4o(?:-[\\w-]+)?', 'o1(?:-[\\w-]+)?', + 'o3(?:-[\\w-]+)?', + 'o4(?:-[\\w-]+)?', 'deepseek-vl(?:[\\w-]+)?', 'kimi-latest', 'gemma-3(?:-[\\w-]+)' @@ -173,6 +176,7 @@ const visionExcludedModels = [ 'gpt-4-32k', 'gpt-4-\\d+', 'o1-mini', + 'o3-mini', 'o1-preview', 'AIDC-AI/Marco-o1' ] @@ -260,6 +264,7 @@ export function getModelLogo(modelId: string) { minimax: isLight ? MinimaxModelLogo : MinimaxModelLogoDark, o3: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, o1: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, + o4: isLight ? ChatGPTo1ModelLogo : ChatGPTo1ModelLogoDark, 'gpt-3': isLight ? ChatGPT35ModelLogo : ChatGPT35ModelLogoDark, 'gpt-4': isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, gpts: isLight ? ChatGPT4ModelLogo : ChatGPT4ModelLogoDark, @@ -1084,16 +1089,22 @@ export const SYSTEM_MODELS: Record = { ], zhipu: [ { - id: 'glm-zero-preview', + id: 'glm-z1-air', provider: 'zhipu', - name: 'GLM-Zero-Preview', - group: 'GLM-Zero' + name: 'GLM-Z1-AIR', + group: 'GLM-Z1' }, { - id: 'glm-4-0520', + id: 'glm-z1-airx', provider: 'zhipu', - name: 'GLM-4-0520', - group: 'GLM-4' + name: 'GLM-Z1-AIRX', + group: 'GLM-Z1' + }, + { + id: 'glm-z1-flash', + provider: 'zhipu', + name: 'GLM-Z1-FLASH', + group: 'GLM-Z1' }, { id: 'glm-4-long', @@ -1108,9 +1119,9 @@ export const SYSTEM_MODELS: Record = { group: 'GLM-4' }, { - id: 'glm-4-air', + id: 'glm-4-air-250414', provider: 'zhipu', - name: 'GLM-4-Air', + name: 'GLM-4-Air-250414', group: 'GLM-4' }, { @@ -1120,9 +1131,9 @@ export const SYSTEM_MODELS: Record = { group: 'GLM-4' }, { - id: 'glm-4-flash', + id: 'glm-4-flash-250414', provider: 'zhipu', - name: 'GLM-4-Flash', + name: 'GLM-4-Flash-250414', group: 'GLM-4' }, { @@ -1144,9 +1155,9 @@ export const SYSTEM_MODELS: Record = { group: 'GLM-4v' }, { - id: 'glm-4v-plus', + id: 'glm-4v-plus-0111', provider: 'zhipu', - name: 'GLM-4V-Plus', + name: 'GLM-4V-Plus-0111', group: 'GLM-4v' }, { @@ -2211,7 +2222,7 @@ export function isVisionModel(model: Model): boolean { } export function isOpenAIoSeries(model: Model): boolean { - return ['o1', 'o1-2024-12-17'].includes(model.id) || model.id.includes('o3') + return model.id.includes('o1') || model.id.includes('o3') || model.id.includes('o4') } export function isOpenAIWebSearch(model: Model): boolean { return model.id.includes('gpt-4o-search-preview') || model.id.includes('gpt-4o-mini-search-preview') @@ -2234,6 +2245,13 @@ export function isSupportedReasoningEffortModel(model?: Model): boolean { return false } +export function isGrokModel(model?: Model): boolean { + if (!model) { + return false + } + return model.id.includes('grok') +} + export function isGrokReasoningModel(model?: Model): boolean { if (!model) { return false @@ -2263,6 +2281,10 @@ export function isReasoningModel(model?: Model): boolean { return true } + if (model.id.includes('glm-z1')) { + return true + } + return REASONING_REGEX.test(model.id) || model.type?.includes('reasoning') || false } diff --git a/src/renderer/src/hooks/useSettings.ts b/src/renderer/src/hooks/useSettings.ts index 9e75b57dd1..64e2bcee2c 100644 --- a/src/renderer/src/hooks/useSettings.ts +++ b/src/renderer/src/hooks/useSettings.ts @@ -15,7 +15,8 @@ import { setTrayOnClose, setWindowStyle } from '@renderer/store/settings' -import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types' +import { SidebarIcon } from '@renderer/store/settings' +import { ThemeMode, TranslateLanguageVarious } from '@renderer/types' export function useSettings() { const settings = useAppSelector((state) => state.settings) diff --git a/src/renderer/src/hooks/useSidebarIcon.ts b/src/renderer/src/hooks/useSidebarIcon.ts index 0bd252d8ef..f9ec64af79 100644 --- a/src/renderer/src/hooks/useSidebarIcon.ts +++ b/src/renderer/src/hooks/useSidebarIcon.ts @@ -1,4 +1,4 @@ -import { SidebarIcon } from '@renderer/types' +import { SidebarIcon } from '@renderer/store/settings' import { useSettings } from './useSettings' diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index 727aabf8dd..4743a966b3 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -97,7 +97,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) => const data = { ...topic, name: topicName } // Check if _setActiveTopic exists and is a function before calling if (typeof _setActiveTopic === 'function') { - _setActiveTopic(data) + _setActiveTopic(data) } store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) } @@ -114,9 +114,9 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) => // Ensure topic is defined before using it if (summaryText && topic) { const data = { ...topic, name: summaryText } - // Check if _setActiveTopic exists and is a function before calling + // Check if _setActiveTopic exists and is a function before calling if (typeof _setActiveTopic === 'function') { - _setActiveTopic(data) + _setActiveTopic(data) } store.dispatch(updateTopic({ assistantId: assistant.id, topic: data })) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 028420f58a..8db3605c54 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -374,7 +374,7 @@ "no_api_key": "API key is not configured", "provider_disabled": "Model provider is not enabled", "render": { - "description": "Failed to render formula. Please check if the formula format is correct", + "description": "Failed to render message content. Please check if the message content format is correct", "title": "Render Error" }, "user_message_not_found": "Cannot find original user message to resend", @@ -1582,7 +1582,7 @@ "subscribe_name": "Alternative name", "subscribe_name.placeholder": "Alternative name used when the downloaded subscription feed has no name.", "subscribe_add_success": "Subscription feed added successfully!", - "subscribe_delete": "Delete subscription source", + "subscribe_delete": "Delete", "overwrite": "Override search service", "overwrite_tooltip": "Force use search service instead of LLM", "apikey": "API key", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index fa5d78ad20..ba542f9951 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -335,7 +335,7 @@ "no_api_key": "APIキーが設定されていません", "provider_disabled": "モデルプロバイダーが有効になっていません", "render": { - "description": "数式のレンダリングに失敗しました。数式の形式が正しいか確認してください", + "description": "メッセージの内容のレンダリングに失敗しました。メッセージの内容の形式が正しいか確認してください", "title": "レンダリングエラー" }, "user_message_not_found": "元のユーザーメッセージを見つけることができませんでした", @@ -1363,7 +1363,7 @@ "subscribe_name": "代替名", "subscribe_name.placeholder": "ダウンロードしたフィードに名前がない場合に使用される代替名", "subscribe_add_success": "フィードの追加が成功しました!", - "subscribe_delete": "フィードの削除", + "subscribe_delete": "削除", "overwrite": "サービス検索を上書き", "overwrite_tooltip": "大規模言語モデルではなく、サービス検索を使用する", "apikey": "API キー", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f57372678b..b8baa0e932 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -338,7 +338,7 @@ "no_api_key": "Ключ API не настроен", "provider_disabled": "Провайдер моделей не включен", "render": { - "description": "Не удалось рендерить формулу. Пожалуйста, проверьте, правильно ли формат формулы", + "description": "Не удалось рендерить содержимое сообщения. Пожалуйста, проверьте, правильно ли формат содержимого сообщения", "title": "Ошибка рендеринга" }, "user_message_not_found": "Не удалось найти исходное сообщение пользователя", @@ -1366,7 +1366,7 @@ "subscribe_name": "альтернативное имя", "subscribe_name.placeholder": "替代名称, используемый, когда загружаемый подписочный источник не имеет названия", "subscribe_add_success": "Подписка добавлена успешно!", - "subscribe_delete": "Удалить источник подписки", + "subscribe_delete": "Удалить", "overwrite": "Переопределить поставщика поиска", "overwrite_tooltip": "Использовать поставщика поиска вместо LLM", "apikey": "Ключ API", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 491faeff7c..c1fe346f11 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -375,7 +375,7 @@ "no_api_key": "API 密钥未配置", "provider_disabled": "模型提供商未启用", "render": { - "description": "渲染公式失败,请检查公式格式是否正确", + "description": "渲染消息内容失败,请检查消息内容格式是否正确", "title": "渲染错误" }, "user_message_not_found": "无法找到原始用户消息", @@ -1372,6 +1372,20 @@ "description": "描述", "duplicateName": "已存在同名服务器", "editJson": "编辑JSON", + "importServer": "导入服务器", + "importServerDesc": "从JSON文件或文本导入单个MCP服务器配置", + "dropJsonFile": "拖拽JSON文件到此处", + "clickOrDrop": "点击或拖拽文件上传", + "orPasteJson": "或粘贴JSON配置", + "jsonRequired": "请输入JSON配置", + "noServerFound": "未找到服务器配置", + "importSuccess": "服务器导入成功", + "invalidServerFormat": "无效的服务器格式", + "jsonImportError": "导入JSON配置失败", + "fileReadError": "读取文件失败", + "importedServer": "导入的服务器", + "import": "导入", + "importModeHint": "支持两种格式:单个服务器配置或完整的mcpServers配置", "editServer": "编辑服务器", "env": "环境变量", "envTooltip": "格式:KEY=value,每行一个", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 29e13aca0f..6097e39ae1 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -335,7 +335,7 @@ "no_api_key": "API 金鑰未設定", "provider_disabled": "模型供應商未啟用", "render": { - "description": "渲染公式失敗,請檢查公式格式是否正確", + "description": "渲染訊息內容失敗,請檢查訊息內容格式是否正確", "title": "渲染錯誤" }, "user_message_not_found": "無法找到原始用戶訊息", @@ -1362,7 +1362,7 @@ "subscribe_name": "替代名稱", "subscribe_name.placeholder": "當下載的訂閱源沒有名稱時所使用的替代名稱", "subscribe_add_success": "訂閱源添加成功!", - "subscribe_delete": "刪除訂閱源", + "subscribe_delete": "刪除", "title": "網路搜尋", "overwrite": "覆蓋搜尋服務商", "overwrite_tooltip": "強制使用搜尋服務商而不是大語言模型進行搜尋", diff --git a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx index 681a426cf6..4ca7cad362 100644 --- a/src/renderer/src/pages/agents/components/AddAgentPopup.tsx +++ b/src/renderer/src/pages/agents/components/AddAgentPopup.tsx @@ -118,7 +118,7 @@ const PopupContainer: React.FC = ({ resolve }) => { prompt: AGENT_PROMPT, content: promptText }) - formRef.current?.setFieldValue('prompt', generatedText) + form.setFieldsValue({ prompt: generatedText }) } catch (error) { console.error('Error fetching data:', error) } @@ -170,11 +170,9 @@ const PopupContainer: React.FC = ({ resolve }) => { label={t('agents.add.prompt')} rules={[{ required: true }]} style={{ position: 'relative' }}> - -