diff --git a/.oxlintrc.json b/.oxlintrc.json index 20f3955d58..662e527fce 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -22,7 +22,6 @@ "eslint.config.mjs" ], "overrides": [ - // set different env { "env": { "node": true @@ -37,8 +36,7 @@ "src/renderer/**/*.{ts,tsx}", "packages/aiCore/**", "packages/extension-table-plus/**", - "packages/ui/**", - "resources/js/**" + "packages/ui/**" ] }, { @@ -56,74 +54,16 @@ "files": ["src/preload/**"] } ], - // We don't use the React plugin here because its behavior differs slightly from that of ESLint's React plugin. "plugins": ["unicorn", "typescript", "oxc", "import"], "rules": { - "constructor-super": "error", - "for-direction": "error", - "getter-return": "error", "no-array-constructor": "off", - // "import/no-cycle": "error", // tons of error, bro - "no-async-promise-executor": "error", "no-caller": "warn", - "no-case-declarations": "error", - "no-class-assign": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-const-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-args": "error", - "no-dupe-class-members": "error", - "no-dupe-else-if": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", "no-eval": "warn", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", "no-fallthrough": "warn", - "no-func-assign": "error", - "no-global-assign": "error", - "no-import-assign": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-new-native-nonconstructor": "error", - "no-nonoctal-decimal-escape": "error", - "no-obj-calls": "error", - "no-octal": "error", - "no-prototype-builtins": "error", - "no-redeclare": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-setter-return": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-this-before-super": "error", "no-unassigned-vars": "warn", - "no-undef": "error", - "no-unexpected-multiline": "error", - "no-unreachable": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-expressions": "off", // this rule disallow us to use expression to call function, like `condition && fn()` - "no-unused-labels": "error", - "no-unused-private-class-members": "error", + "no-unused-expressions": "off", "no-unused-vars": ["warn", { "caughtErrors": "none" }], - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", "no-useless-rename": "warn", - "no-with": "error", "oxc/bad-array-method-on-arguments": "warn", "oxc/bad-char-at-comparison": "warn", "oxc/bad-comparison-sequence": "warn", @@ -135,19 +75,17 @@ "oxc/erasing-op": "warn", "oxc/missing-throw": "warn", "oxc/number-arg-out-of-range": "warn", - "oxc/only-used-in-recursion": "off", // manually off bacause of existing warning. may turn it on in the future + "oxc/only-used-in-recursion": "off", "oxc/uninvoked-array-callback": "warn", - "require-yield": "error", "typescript/await-thenable": "warn", - // "typescript/ban-ts-comment": "error", - "typescript/no-array-constructor": "error", "typescript/consistent-type-imports": "error", + "typescript/no-array-constructor": "error", "typescript/no-array-delete": "warn", "typescript/no-base-to-string": "warn", "typescript/no-duplicate-enum-values": "error", "typescript/no-duplicate-type-constituents": "warn", "typescript/no-empty-object-type": "off", - "typescript/no-explicit-any": "off", // not safe but too many errors + "typescript/no-explicit-any": "off", "typescript/no-extra-non-null-assertion": "error", "typescript/no-floating-promises": "warn", "typescript/no-for-in-array": "warn", @@ -156,7 +94,7 @@ "typescript/no-misused-new": "error", "typescript/no-misused-spread": "warn", "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "off", // it's off now. but may turn it on. + "typescript/no-non-null-asserted-optional-chain": "off", "typescript/no-redundant-type-constituents": "warn", "typescript/no-require-imports": "off", "typescript/no-this-alias": "error", @@ -174,20 +112,18 @@ "typescript/triple-slash-reference": "error", "typescript/unbound-method": "warn", "unicorn/no-await-in-promise-methods": "warn", - "unicorn/no-empty-file": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-empty-file": "off", "unicorn/no-invalid-fetch-options": "warn", "unicorn/no-invalid-remove-event-listener": "warn", - "unicorn/no-new-array": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-new-array": "off", "unicorn/no-single-promise-in-promise-methods": "warn", - "unicorn/no-thenable": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-thenable": "off", "unicorn/no-unnecessary-await": "warn", "unicorn/no-useless-fallback-in-spread": "warn", "unicorn/no-useless-length-check": "warn", - "unicorn/no-useless-spread": "off", // manually off bacause of existing warning. may turn it on in the future + "unicorn/no-useless-spread": "off", "unicorn/prefer-set-size": "warn", - "unicorn/prefer-string-starts-ends-with": "warn", - "use-isnan": "error", - "valid-typeof": "error" + "unicorn/prefer-string-starts-ends-with": "warn" }, "settings": { "jsdoc": { diff --git a/CLAUDE.md b/CLAUDE.md index 159c4be9e4..1ff53e2fe3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,8 +11,7 @@ This file provides guidance to AI coding assistants when working with code in th - **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`. - **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references. - **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications. -- **Write conventional commits with emoji**: Commit small, focused changes using emoji-prefixed Conventional Commit messages (e.g., `✨ feat:`, `🐛 fix:`, `♻️ refactor:`, ` -📝 docs:`). +- **Write conventional commits**: Commit small, focused changes using Conventional Commit messages (e.g., `feat:`, `fix:`, `refactor:`, `docs:`). ## Development Commands diff --git a/README.md b/README.md index c3d3f915a1..1223f73ed0 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Cherry Studio is a desktop client that supports multiple LLM providers, availabl 1. **Diverse LLM Provider Support**: - ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more -- 🔗 AI Web Service Integration: Claude, Perplexity, Poe, and others +- 🔗 AI Web Service Integration: Claude, Perplexity, [Poe](https://poe.com/), and others - 💻 Local Model Support with Ollama, LM Studio 2. **AI Assistants & Conversations**: @@ -238,10 +238,6 @@ The Enterprise Edition addresses core challenges in team collaboration by centra ## ✨ Online Demo -> 🚧 **Public Beta Notice** -> -> The Enterprise Edition is currently in its early public beta stage, and we are actively iterating and optimizing its features. We are aware that it may not be perfectly stable yet. If you encounter any issues or have valuable suggestions during your trial, we would be very grateful if you could contact us via email to provide feedback. - **🔗 [Cherry Studio Enterprise](https://www.cherry-ai.com/enterprise)** ## Version Comparison @@ -249,7 +245,7 @@ The Enterprise Edition addresses core challenges in team collaboration by centra | Feature | Community Edition | Enterprise Edition | | :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | **Open Source** | ✅ Yes | ⭕️ Partially released to customers | -| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee | +| **Cost** | [AGPL-3.0 License](https://github.com/CherryHQ/cherry-studio?tab=AGPL-3.0-1-ov-file) | Buyout / Subscription Fee | | **Admin Backend** | — | ● Centralized **Model** Access
● **Employee** Management
● Shared **Knowledge Base**
● **Access** Control
● **Data** Backup | | **Server** | — | ✅ Dedicated Private Deployment | @@ -262,8 +258,12 @@ We believe the Enterprise Edition will become your team's AI productivity engine # 🔗 Related Projects +- [new-api](https://github.com/QuantumNous/new-api): The next-generation LLM gateway and AI asset management system supports multiple languages. + - [one-api](https://github.com/songquanpeng/one-api): LLM API management and distribution system supporting mainstream models like OpenAI, Azure, and Anthropic. Features a unified API interface, suitable for key management and secondary distribution. +- [Poe](https://poe.com/): Poe gives you access to the best AI, all in one place. Explore GPT-5, Claude Opus 4.1, DeepSeek-R1, Veo 3, ElevenLabs, and millions of others. + - [ublacklist](https://github.com/iorate/ublacklist): Blocks specific sites from appearing in Google search results # 🚀 Contributors diff --git a/components.json b/components.json deleted file mode 100644 index c5aceeb3ce..0000000000 --- a/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "aliases": { - "components": "@renderer/ui/third-party", - "hooks": "@renderer/hooks", - "lib": "@renderer/lib", - "ui": "@renderer/ui", - "utils": "@renderer/utils" - }, - "iconLibrary": "lucide", - "rsc": false, - "style": "new-york", - "tailwind": { - "baseColor": "zinc", - "config": "", - "css": "src/renderer/src/assets/styles/tailwind.css", - "cssVariables": true, - "prefix": "" - }, - "tsx": true -} diff --git a/packages/shared/config/constant.ts b/packages/shared/config/constant.ts index d4706863f5..ed46c4f5f1 100644 --- a/packages/shared/config/constant.ts +++ b/packages/shared/config/constant.ts @@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [ }) } ] + +// resources/scripts should be maintained manually +export const HOME_CHERRY_DIR = '.cherrystudio' diff --git a/resources/js/bridge.js b/resources/js/bridge.js deleted file mode 100644 index f6c0021a63..0000000000 --- a/resources/js/bridge.js +++ /dev/null @@ -1,36 +0,0 @@ -;(() => { - let messageId = 0 - const pendingCalls = new Map() - - function api(method, ...args) { - const id = messageId++ - return new Promise((resolve, reject) => { - pendingCalls.set(id, { resolve, reject }) - window.parent.postMessage({ id, type: 'api-call', method, args }, '*') - }) - } - - window.addEventListener('message', (event) => { - if (event.data.type === 'api-response') { - const { id, result, error } = event.data - const pendingCall = pendingCalls.get(id) - if (pendingCall) { - if (error) { - pendingCall.reject(new Error(error)) - } else { - pendingCall.resolve(result) - } - pendingCalls.delete(id) - } - } - }) - - window.api = new Proxy( - {}, - { - get: (target, prop) => { - return (...args) => api(prop, ...args) - } - } - ) -})() diff --git a/resources/js/utils.js b/resources/js/utils.js deleted file mode 100644 index 36981ac44f..0000000000 --- a/resources/js/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function getQueryParam(paramName) { - const url = new URL(window.location.href) - const params = new URLSearchParams(url.search) - return params.get(paramName) -} diff --git a/resources/scripts/install-bun.js b/resources/scripts/install-bun.js index 1467a4cde4..33ee18d732 100644 --- a/resources/scripts/install-bun.js +++ b/resources/scripts/install-bun.js @@ -7,7 +7,7 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading bun binaries const BUN_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/bun/releases/download' -const DEFAULT_BUN_VERSION = '1.2.17' // Default fallback version +const DEFAULT_BUN_VERSION = '1.3.1' // Default fallback version // Mapping of platform+arch to binary package name const BUN_PACKAGES = { diff --git a/resources/scripts/install-uv.js b/resources/scripts/install-uv.js index 3dc8b3e477..c3d34efc33 100644 --- a/resources/scripts/install-uv.js +++ b/resources/scripts/install-uv.js @@ -7,28 +7,29 @@ const { downloadWithRedirects } = require('./download') // Base URL for downloading uv binaries const UV_RELEASE_BASE_URL = 'https://gitcode.com/CherryHQ/uv/releases/download' -const DEFAULT_UV_VERSION = '0.7.13' +const DEFAULT_UV_VERSION = '0.9.5' // Mapping of platform+arch to binary package name const UV_PACKAGES = { - 'darwin-arm64': 'uv-aarch64-apple-darwin.zip', - 'darwin-x64': 'uv-x86_64-apple-darwin.zip', + 'darwin-arm64': 'uv-aarch64-apple-darwin.tar.gz', + 'darwin-x64': 'uv-x86_64-apple-darwin.tar.gz', 'win32-arm64': 'uv-aarch64-pc-windows-msvc.zip', 'win32-ia32': 'uv-i686-pc-windows-msvc.zip', 'win32-x64': 'uv-x86_64-pc-windows-msvc.zip', - 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.zip', - 'linux-ia32': 'uv-i686-unknown-linux-gnu.zip', - 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.zip', - 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.zip', - 'linux-s390x': 'uv-s390x-unknown-linux-gnu.zip', - 'linux-x64': 'uv-x86_64-unknown-linux-gnu.zip', - 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.zip', + 'linux-arm64': 'uv-aarch64-unknown-linux-gnu.tar.gz', + 'linux-ia32': 'uv-i686-unknown-linux-gnu.tar.gz', + 'linux-ppc64': 'uv-powerpc64-unknown-linux-gnu.tar.gz', + 'linux-ppc64le': 'uv-powerpc64le-unknown-linux-gnu.tar.gz', + 'linux-riscv64': 'uv-riscv64gc-unknown-linux-gnu.tar.gz', + 'linux-s390x': 'uv-s390x-unknown-linux-gnu.tar.gz', + 'linux-x64': 'uv-x86_64-unknown-linux-gnu.tar.gz', + 'linux-armv7l': 'uv-armv7-unknown-linux-gnueabihf.tar.gz', // MUSL variants - 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.zip', - 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.zip', - 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.zip', - 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.zip', - 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.zip' + 'linux-musl-arm64': 'uv-aarch64-unknown-linux-musl.tar.gz', + 'linux-musl-ia32': 'uv-i686-unknown-linux-musl.tar.gz', + 'linux-musl-x64': 'uv-x86_64-unknown-linux-musl.tar.gz', + 'linux-musl-armv6l': 'uv-arm-unknown-linux-musleabihf.tar.gz', + 'linux-musl-armv7l': 'uv-armv7-unknown-linux-musleabihf.tar.gz' } /** @@ -56,6 +57,7 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is const downloadUrl = `${UV_RELEASE_BASE_URL}/${version}/${packageName}` const tempdir = os.tmpdir() const tempFilename = path.join(tempdir, packageName) + const isTarGz = packageName.endsWith('.tar.gz') try { console.log(`Downloading uv ${version} for ${platformKey}...`) @@ -65,34 +67,58 @@ async function downloadUvBinary(platform, arch, version = DEFAULT_UV_VERSION, is console.log(`Extracting ${packageName} to ${binDir}...`) - const zip = new StreamZip.async({ file: tempFilename }) + if (isTarGz) { + // Use tar command to extract tar.gz files (macOS and Linux) + const tempExtractDir = path.join(tempdir, `uv-extract-${Date.now()}`) + fs.mkdirSync(tempExtractDir, { recursive: true }) - // Get all entries in the zip file - const entries = await zip.entries() + execSync(`tar -xzf "${tempFilename}" -C "${tempExtractDir}"`, { stdio: 'inherit' }) - // Extract files directly to binDir, flattening the directory structure - for (const entry of Object.values(entries)) { - if (!entry.isDirectory) { - // Get just the filename without path - const filename = path.basename(entry.name) - const outputPath = path.join(binDir, filename) - - console.log(`Extracting ${entry.name} -> ${filename}`) - await zip.extract(entry.name, outputPath) - // Make executable files executable on Unix-like systems - if (platform !== 'win32') { - try { + // Find all files in the extracted directory and move them to binDir + const findAndMoveFiles = (dir) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + findAndMoveFiles(fullPath) + } else { + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + fs.copyFileSync(fullPath, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + // Make executable on Unix-like systems fs.chmodSync(outputPath, 0o755) - } catch (chmodError) { - console.error(`Warning: Failed to set executable permissions on ${filename}`) - return 102 } } - console.log(`Extracted ${entry.name} -> ${outputPath}`) } + + findAndMoveFiles(tempExtractDir) + + // Clean up temporary extraction directory + fs.rmSync(tempExtractDir, { recursive: true }) + } else { + // Use StreamZip for zip files (Windows) + const zip = new StreamZip.async({ file: tempFilename }) + + // Get all entries in the zip file + const entries = await zip.entries() + + // Extract files directly to binDir, flattening the directory structure + for (const entry of Object.values(entries)) { + if (!entry.isDirectory) { + // Get just the filename without path + const filename = path.basename(entry.name) + const outputPath = path.join(binDir, filename) + + console.log(`Extracting ${entry.name} -> ${filename}`) + await zip.extract(entry.name, outputPath) + console.log(`Extracted ${entry.name} -> ${outputPath}`) + } + } + + await zip.close() } - await zip.close() fs.unlinkSync(tempFilename) console.log(`Successfully installed uv ${version} for ${platform}-${arch}`) return 0 diff --git a/resources/scripts/ipService.js b/resources/scripts/ipService.js deleted file mode 100644 index 8e997659a7..0000000000 --- a/resources/scripts/ipService.js +++ /dev/null @@ -1,88 +0,0 @@ -const https = require('https') -const { loggerService } = require('@logger') - -const logger = loggerService.withContext('IpService') - -/** - * 获取用户的IP地址所在国家 - * @returns {Promise} 返回国家代码,默认为'CN' - */ -async function getIpCountry() { - return new Promise((resolve) => { - // 添加超时控制 - const timeout = setTimeout(() => { - logger.info('IP Address Check Timeout, default to China Mirror') - resolve('CN') - }, 5000) - - const options = { - hostname: 'ipinfo.io', - path: '/json', - method: 'GET', - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', - 'Accept-Language': 'en-US,en;q=0.9' - } - } - - const req = https.request(options, (res) => { - clearTimeout(timeout) - let data = '' - - res.on('data', (chunk) => { - data += chunk - }) - - res.on('end', () => { - try { - const parsed = JSON.parse(data) - const country = parsed.country || 'CN' - logger.info(`Detected user IP address country: ${country}`) - resolve(country) - } catch (error) { - logger.error('Failed to parse IP address information:', error.message) - resolve('CN') - } - }) - }) - - req.on('error', (error) => { - clearTimeout(timeout) - logger.error('Failed to get IP address information:', error.message) - resolve('CN') - }) - - req.end() - }) -} - -/** - * 检查用户是否在中国 - * @returns {Promise} 如果用户在中国返回true,否则返回false - */ -async function isUserInChina() { - const country = await getIpCountry() - return country.toLowerCase() === 'cn' -} - -/** - * 根据用户位置获取适合的npm镜像URL - * @returns {Promise} 返回npm镜像URL - */ -async function getNpmRegistryUrl() { - const inChina = await isUserInChina() - if (inChina) { - logger.info('User in China, using Taobao npm mirror') - return 'https://registry.npmmirror.com' - } else { - logger.info('User not in China, using default npm mirror') - return 'https://registry.npmjs.org' - } -} - -module.exports = { - getIpCountry, - isUserInChina, - getNpmRegistryUrl -} diff --git a/src/main/services/CodeToolsService.ts b/src/main/services/CodeToolsService.ts index 3a93a40d79..82c9c64f87 100644 --- a/src/main/services/CodeToolsService.ts +++ b/src/main/services/CodeToolsService.ts @@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process' import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant' import { codeTools, + HOME_CHERRY_DIR, MACOS_TERMINALS, MACOS_TERMINALS_WITH_COMMANDS, terminalApps, @@ -66,7 +67,7 @@ class CodeToolsService { } public async getBunPath() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const bunName = await getBinaryName('bun') const bunPath = path.join(dir, bunName) return bunPath @@ -362,7 +363,7 @@ class CodeToolsService { private async isPackageInstalled(cliTool: string): Promise { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) // Ensure bin directory exists @@ -389,7 +390,7 @@ class CodeToolsService { logger.info(`${cliTool} is installed, getting current version`) try { const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) const { stdout } = await execAsync(`"${executablePath}" --version`, { @@ -500,7 +501,7 @@ class CodeToolsService { try { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) const registryUrl = await this.getNpmRegistryUrl() const installEnvPrefix = isWin @@ -550,7 +551,7 @@ class CodeToolsService { const packageName = await this.getPackageName(cliTool) const bunPath = await this.getBunPath() const executableName = await this.getCliExecutableName(cliTool) - const binDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : '')) logger.debug(`Package name: ${packageName}`) @@ -652,7 +653,7 @@ class CodeToolsService { baseCommand = `${baseCommand} ${configParams}` } - const bunInstallPath = path.join(os.homedir(), '.cherrystudio') + const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR) if (isInstalled) { // If already installed, run executable directly (with optional update message) diff --git a/src/main/services/MCPService.ts b/src/main/services/MCPService.ts index ed19a3a88a..c403552fd2 100644 --- a/src/main/services/MCPService.ts +++ b/src/main/services/MCPService.ts @@ -31,6 +31,7 @@ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js' import { nanoid } from '@reduxjs/toolkit' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { MCPProgressEvent } from '@shared/config/types' import { IpcChannel } from '@shared/IpcChannel' import { defaultAppHeaders } from '@shared/utils' @@ -715,7 +716,7 @@ class McpService { } public async getInstallInfo() { - const dir = path.join(os.homedir(), '.cherrystudio', 'bin') + const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const uvName = await getBinaryName('uv') const bunName = await getBinaryName('bun') const uvPath = path.join(dir, uvName) diff --git a/src/main/services/OvmsManager.ts b/src/main/services/OvmsManager.ts index f319200ac3..3a32d74ecf 100644 --- a/src/main/services/OvmsManager.ts +++ b/src/main/services/OvmsManager.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os' import { promisify } from 'node:util' import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import * as fs from 'fs-extra' import * as path from 'path' @@ -145,7 +146,7 @@ class OvmsManager { */ public async runOvms(): Promise<{ success: boolean; message?: string }> { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') const runBatPath = path.join(ovmsDir, 'run.bat') @@ -195,7 +196,7 @@ class OvmsManager { */ public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> { const homeDir = homedir() - const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe') + const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe') try { // Check if OVMS executable exists @@ -273,7 +274,7 @@ class OvmsManager { } const homeDir = homedir() - const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json') + const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json') try { if (!(await fs.pathExists(configPath))) { logger.warn(`Config file does not exist: ${configPath}`) @@ -304,7 +305,7 @@ class OvmsManager { private async applyModelPath(modelDirPath: string): Promise { const homeDir = homedir() - const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch') + const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch') if (!(await fs.pathExists(patchDir))) { return true } @@ -355,7 +356,7 @@ class OvmsManager { logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`) const homeDir = homedir() - const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const pathModel = path.join(ovdndDir, 'models', modelId) try { @@ -468,7 +469,7 @@ class OvmsManager { */ public async checkModelExists(modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -495,7 +496,7 @@ class OvmsManager { */ public async updateModelConfig(modelName: string, modelId: string): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { @@ -548,7 +549,7 @@ class OvmsManager { */ public async getModels(): Promise { const homeDir = homedir() - const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms') + const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms') const configPath = path.join(ovmsDir, 'models', 'config.json') try { diff --git a/src/main/services/SpanCacheService.ts b/src/main/services/SpanCacheService.ts index 6d88a5cdc2..9fe9c7d7f1 100644 --- a/src/main/services/SpanCacheService.ts +++ b/src/main/services/SpanCacheService.ts @@ -4,6 +4,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/ import { convertSpanToSpanEntity } from '@mcp-trace/trace-core' import { SpanStatusCode } from '@opentelemetry/api' import type { ReadableSpan } from '@opentelemetry/sdk-trace-base' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import fs from 'fs/promises' import * as os from 'os' import * as path from 'path' @@ -17,7 +18,7 @@ class SpanCacheService implements TraceCache { pri constructor() { - this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace') + this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace') } createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => { diff --git a/src/main/services/mcp/oauth/callback.ts b/src/main/services/mcp/oauth/callback.ts index 81d435f867..c13ecd5c07 100644 --- a/src/main/services/mcp/oauth/callback.ts +++ b/src/main/services/mcp/oauth/callback.ts @@ -1,4 +1,6 @@ import { loggerService } from '@logger' +import { configManager } from '@main/services/ConfigManager' +import { locales } from '@main/utils/locales' import type EventEmitter from 'events' import http from 'http' import { URL } from 'url' @@ -7,6 +9,36 @@ import type { OAuthCallbackServerOptions } from './types' const logger = loggerService.withContext('MCP:OAuthCallbackServer') +function getTranslation(key: string): string { + const language = configManager.getLanguage() + const localeData = locales[language] + + if (!localeData) { + logger.warn(`No locale data found for language: ${language}`) + return key + } + + const translations = localeData.translation as any + if (!translations) { + logger.warn(`No translations found for language: ${language}`) + return key + } + + const keys = key.split('.') + let value = translations + + for (const k of keys) { + if (value && typeof value === 'object' && k in value) { + value = value[k] + } else { + logger.warn(`Translation key not found: ${key} (failed at: ${k})`) + return key // fallback to key if translation not found + } + } + + return typeof value === 'string' ? value : key +} + export class CallBackServer { private server: Promise private events: EventEmitter @@ -28,6 +60,55 @@ export class CallBackServer { if (code) { // Emit the code event this.events.emit('auth-code-received', code) + // Send success response to browser + const title = getTranslation('settings.mcp.oauth.callback.title') + const message = getTranslation('settings.mcp.oauth.callback.message') + + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }) + res.end(` + + + + + ${title} + + + +
+

${title}

+

${message}

+
+ + + `) + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }) + res.end('Missing authorization code') } } catch (error) { logger.error('Error processing OAuth callback:', error as Error) diff --git a/src/main/services/ocr/builtin/OvOcrService.ts b/src/main/services/ocr/builtin/OvOcrService.ts index 6e0eee1c37..052682be64 100644 --- a/src/main/services/ocr/builtin/OvOcrService.ts +++ b/src/main/services/ocr/builtin/OvOcrService.ts @@ -1,5 +1,6 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types' import { isImageFileMetadata } from '@types' import { exec } from 'child_process' @@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService' const logger = loggerService.withContext('OvOcrService') const execAsync = promisify(exec) -const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat') +const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat') export class OvOcrService extends OcrBaseService { constructor() { @@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService { } private getOvOcrPath(): string { - return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr') } private getImgDir(): string { diff --git a/src/main/utils/file.ts b/src/main/utils/file.ts index 17155f423b..1432dccc8a 100644 --- a/src/main/utils/file.ts +++ b/src/main/utils/file.ts @@ -5,7 +5,7 @@ import os from 'node:os' import path from 'node:path' import { loggerService } from '@logger' -import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant' +import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant' import type { FileMetadata, NotesTreeNode } from '@types' import { FileTypes } from '@types' import chardet from 'chardet' @@ -160,7 +160,7 @@ export function getNotesDir() { } export function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function getCacheDir() { @@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) { } export function getMcpDir() { - return path.join(os.homedir(), '.cherrystudio', 'mcp') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp') } /** diff --git a/src/main/utils/init.ts b/src/main/utils/init.ts index 63cf69e89b..20884b1eeb 100644 --- a/src/main/utils/init.ts +++ b/src/main/utils/init.ts @@ -3,6 +3,7 @@ import os from 'node:os' import path from 'node:path' import { isLinux, isPortable, isWin } from '@main/constant' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { app } from 'electron' // Please don't import any other modules which is not node/electron built-in modules @@ -17,7 +18,7 @@ function hasWritePermission(path: string) { } function getConfigDir() { - return path.join(os.homedir(), '.cherrystudio', 'config') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'config') } export function initAppDataDir() { diff --git a/src/main/utils/process.ts b/src/main/utils/process.ts index f028f2d3c7..f36e86861d 100644 --- a/src/main/utils/process.ts +++ b/src/main/utils/process.ts @@ -1,4 +1,5 @@ import { loggerService } from '@logger' +import { HOME_CHERRY_DIR } from '@shared/config/constant' import { spawn } from 'child_process' import fs from 'fs' import os from 'os' @@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise { export async function getBinaryPath(name?: string): Promise { if (!name) { - return path.join(os.homedir(), '.cherrystudio', 'bin') + return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') } const binaryName = await getBinaryName(name) - const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin') + const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin') const binariesDirExists = fs.existsSync(binariesDir) return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName } diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 3a36fb658a..1d7123a47b 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -418,6 +418,8 @@ export function getAnthropicReasoningParams(assistant: Assistant, model: Model): /** * 获取 Gemini 推理参数 * 从 GeminiAPIClient 中提取的逻辑 + * 注意:Gemini/GCP 端点所使用的 thinkingBudget 等参数应该按照驼峰命名法传递 + * 而在 Google 官方提供的 OpenAI 兼容端点中则使用蛇形命名法 thinking_budget */ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Record { if (!isReasoningModel(model)) { @@ -431,8 +433,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (reasoningEffort === undefined) { return { thinkingConfig: { - include_thoughts: false, - ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {}) + includeThoughts: false, + ...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {}) } } } @@ -442,7 +444,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re if (effortRatio > 1) { return { thinkingConfig: { - include_thoughts: true + includeThoughts: true } } } @@ -452,8 +454,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re return { thinkingConfig: { - ...(budget > 0 ? { thinking_budget: budget } : {}), - include_thoughts: true + ...(budget > 0 ? { thinkingBudget: budget } : {}), + includeThoughts: true } } } diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index b1836f3fa7..b3b920085a 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -20,11 +20,11 @@ import { updateMessageAndBlocksThunk, updateTranslationBlockThunk } from '@renderer/store/thunk/messageThunk' -import type { Assistant, Model, Topic, TranslateLanguageCode } from '@renderer/types' +import { type Assistant, type Model, objectKeys, type Topic, type TranslateLanguageCode } from '@renderer/types' import type { Message, MessageBlock } from '@renderer/types/newMessage' import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { abortCompletion } from '@renderer/utils/abortController' -import { throttle } from 'lodash' +import { difference, throttle } from 'lodash' import { useCallback } from 'react' const logger = loggerService.withContext('UseMessageOperations') @@ -82,10 +82,12 @@ export function useMessageOperations(topic: Topic) { logger.error('[editMessage] Topic prop is not valid.') return } - + const uiStates = ['multiModelMessageStyle', 'foldSelected'] as const satisfies (keyof Message)[] + const extraUpdate = difference(objectKeys(updates), uiStates) + const isUiUpdateOnly = extraUpdate.length === 0 const messageUpdates: Partial & Pick = { id: messageId, - updatedAt: new Date().toISOString(), + updatedAt: isUiUpdateOnly ? undefined : new Date().toISOString(), ...updates } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index fbffb92777..0695075051 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -3863,6 +3863,12 @@ "usage": "Usage", "version": "Version" }, + "oauth": { + "callback": { + "message": "You can close this page and return to Cherry Studio", + "title": "Authentication Successful" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Available Prompts", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 26fb5dbe75..37659a7dd7 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以关闭此页面并返回 Cherry Studio", + "title": "认证成功" + } + }, "prompts": { "arguments": "参数", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8b16b3e94e..5016bcfe1d 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -3863,6 +3863,12 @@ "usage": "用法", "version": "版本" }, + "oauth": { + "callback": { + "message": "您可以關閉此頁面並返回 Cherry Studio", + "title": "認證成功" + } + }, "prompts": { "arguments": "參數", "availablePrompts": "可用提示", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index d94e74422b..59a1f8489e 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -3863,6 +3863,12 @@ "usage": "Verwendung", "version": "Version" }, + "oauth": { + "callback": { + "message": "Sie können diese Seite schließen und zu Cherry Studio zurückkehren", + "title": "Authentifizierung erfolgreich" + } + }, "prompts": { "arguments": "Parameter", "availablePrompts": "Verfügbare Prompts", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 069cd8da8b..c77b757c05 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -3863,6 +3863,12 @@ "usage": "Χρήση", "version": "Έκδοση" }, + "oauth": { + "callback": { + "message": "Μπορείτε να κλείσετε αυτήν τη σελίδα και να επιστρέψετε στο Cherry Studio", + "title": "Επιτυχής Ταυτοποίηση" + } + }, "prompts": { "arguments": "Ορίσματα", "availablePrompts": "Διαθέσιμες Υποδείξεις", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f3c7342b21..722730c645 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versión" }, + "oauth": { + "callback": { + "message": "Puede cerrar esta página y volver a Cherry Studio", + "title": "Autenticación Exitosa" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Indicaciones disponibles", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 79dd7c4141..47ceca85e5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -3863,6 +3863,12 @@ "usage": "Utilisation", "version": "Version" }, + "oauth": { + "callback": { + "message": "Vous pouvez fermer cette page et retourner à Cherry Studio", + "title": "Authentification Réussie" + } + }, "prompts": { "arguments": "Arguments", "availablePrompts": "Invites disponibles", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 9655d8fb7b..409a5ba997 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -3863,6 +3863,12 @@ "usage": "使用法", "version": "バージョン" }, + "oauth": { + "callback": { + "message": "このページを閉じてCherry Studioに戻ることができます", + "title": "認証成功" + } + }, "prompts": { "arguments": "引数", "availablePrompts": "利用可能なプロンプト", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 4be4a3dd97..b7cd03ceca 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -3863,6 +3863,12 @@ "usage": "Uso", "version": "Versão" }, + "oauth": { + "callback": { + "message": "Você pode fechar esta página e retornar ao Cherry Studio", + "title": "Autenticação Bem-Sucedida" + } + }, "prompts": { "arguments": "Argumentos", "availablePrompts": "Dicas disponíveis", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 241fde8fd3..87921d9c5e 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -3863,6 +3863,12 @@ "usage": "Использование", "version": "Версия" }, + "oauth": { + "callback": { + "message": "Вы можете закрыть эту страницу и вернуться в Cherry Studio", + "title": "Аутентификация Успешна" + } + }, "prompts": { "arguments": "Аргументы", "availablePrompts": "Доступные подсказки", diff --git a/src/renderer/src/pages/home/ChatNavbar.tsx b/src/renderer/src/pages/home/ChatNavbar.tsx index 47120e7020..2ccc2aba48 100644 --- a/src/renderer/src/pages/home/ChatNavbar.tsx +++ b/src/renderer/src/pages/home/ChatNavbar.tsx @@ -84,7 +84,7 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveAssistant, activeTo )} {isTopNavbar && !showAssistants && ( - + toggleShowAssistants()} style={{ marginRight: 8 }}> diff --git a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx index 90b284d6c4..611216919a 100644 --- a/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx +++ b/src/renderer/src/pages/home/Messages/AgentSessionMessages.tsx @@ -5,11 +5,13 @@ import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { getGroupedMessages } from '@renderer/services/MessagesService' import { type Topic, TopicType } from '@renderer/types' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' +import { Spin } from 'antd' import { memo, useMemo } from 'react' import styled from 'styled-components' import MessageGroup from './MessageGroup' import NarrowLayout from './NarrowLayout' +import PermissionModeDisplay from './PermissionModeDisplay' import { MessagesContainer, ScrollContainer } from './shared' const logger = loggerService.withContext('AgentSessionMessages') @@ -67,8 +69,12 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { groupedMessages.map(([key, groupMessages]) => ( )) + ) : session ? ( + ) : ( - {session ? 'No messages yet.' : 'Loading session...'} + + + )} @@ -77,10 +83,10 @@ const AgentSessionMessages: React.FC = ({ agentId, sessionId }) => { ) } -const EmptyState = styled.div` - color: var(--color-text-3); - font-size: 12px; - text-align: center; +const LoadingState = styled.div` + display: flex; + justify-content: center; + align-items: center; padding: 20px 0; ` diff --git a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx index fb12b8bfc7..742dc8ca22 100644 --- a/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx +++ b/src/renderer/src/pages/home/Messages/Blocks/ErrorBlock.tsx @@ -301,7 +301,7 @@ const BuiltinError = ({ error }: { error: SerializedError }) => { ) } -// 作为 base,渲染公共字段,应当在 ErrorDetailList 中渲染 +// Base component to render common fields, should be rendered inside ErrorDetailList const AiSdkErrorBase = ({ error }: { error: SerializedAiSdkError }) => { const { t } = useTranslation() const { highlightCode } = useCodeStyle() @@ -366,6 +366,13 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { {isSerializedAiSdkAPICallError(error) && ( <> + {error.responseBody && ( + + {t('error.responseBody')}: + + + )} + {error.requestBodyValues && ( {t('error.requestBodyValues')}: @@ -390,13 +397,6 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => { )} - {error.responseBody && ( - - {t('error.responseBody')}: - - - )} - {error.data && ( {t('error.data')}: diff --git a/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx new file mode 100644 index 0000000000..c8ad773484 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/PermissionModeDisplay.tsx @@ -0,0 +1,82 @@ +import { permissionModeCards } from '@renderer/config/agent' +import SessionSettingsPopup from '@renderer/pages/settings/AgentSettings/SessionSettingsPopup' +import type { GetAgentSessionResponse, PermissionMode } from '@renderer/types' +import { FileEdit, Lightbulb, Shield, ShieldOff } from 'lucide-react' +import type { FC } from 'react' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' + +interface Props { + session: GetAgentSessionResponse + agentId: string +} + +const getPermissionModeConfig = (mode: PermissionMode) => { + switch (mode) { + case 'default': + return { + icon: + } + case 'plan': + return { + icon: + } + case 'acceptEdits': + return { + icon: + } + case 'bypassPermissions': + return { + icon: + } + default: + return { + icon: + } + } +} + +const PermissionModeDisplay: FC = ({ session, agentId }) => { + const { t } = useTranslation() + + const permissionMode = session?.configuration?.permission_mode ?? 'default' + + const modeCard = useMemo(() => { + return permissionModeCards.find((card) => card.mode === permissionMode) + }, [permissionMode]) + + const modeConfig = useMemo(() => getPermissionModeConfig(permissionMode), [permissionMode]) + + const handleClick = () => { + SessionSettingsPopup.show({ + agentId, + sessionId: session.id, + tab: 'tooling' + }) + } + + if (!modeCard) { + return null + } + + return ( +
+
+
{modeConfig.icon}
+
+
+ {t(modeCard.titleKey, modeCard.titleFallback)} +
+
+ {t(modeCard.descriptionKey, modeCard.descriptionFallback)}{' '} + {t(modeCard.behaviorKey, modeCard.behaviorFallback)} +
+
+
+
+ ) +} + +export default PermissionModeDisplay diff --git a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx index d92b6461a4..9b9d98054d 100644 --- a/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx +++ b/src/renderer/src/pages/home/Messages/Tools/MessageAgentTools/BashTool.tsx @@ -1,10 +1,12 @@ import type { CollapseProps } from 'antd' -import { Tag } from 'antd' +import { Popover, Tag } from 'antd' import { Terminal } from 'lucide-react' import { ToolTitle } from './GenericTools' import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types' +const MAX_TAG_LENGTH = 100 + export function BashTool({ input, output @@ -15,6 +17,13 @@ export function BashTool({ // 如果有输出,计算输出行数 const outputLines = output ? output.split('\n').length : 0 + // 处理命令字符串的截断 + const command = input.command + const needsTruncate = command.length > MAX_TAG_LENGTH + const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command + + const tagContent = {displayCommand} + return { key: 'tool', label: ( @@ -26,7 +35,15 @@ export function BashTool({ stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} />
- {input.command} + {needsTruncate ? ( + {command}
} + trigger="hover"> + {tagContent} + + ) : ( + tagContent + )} ), diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index fc3e1a76a1..837d727975 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -95,7 +95,7 @@ const HeaderNavbar: FC = ({ paddingRight: 0, minWidth: 'auto' }}> - + toggleShowAssistants()}> diff --git a/src/renderer/src/pages/notes/HeaderNavbar.tsx b/src/renderer/src/pages/notes/HeaderNavbar.tsx index f9b24a4c48..a05cbc7b40 100644 --- a/src/renderer/src/pages/notes/HeaderNavbar.tsx +++ b/src/renderer/src/pages/notes/HeaderNavbar.tsx @@ -181,7 +181,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand )} {!showWorkspace && ( - + diff --git a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx index e353799c5b..40dc2249a6 100644 --- a/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/ToolingSettings.tsx @@ -459,11 +459,22 @@ export const ToolingSettings: FC = ({ agentBase, upda key={server.id} className="border border-default-200" title={ -
-
- {server.name} +
+
+
+ {server.logoUrl && ( + {`${server.name} + )} + {server.name} +
{server.description ? ( - {server.description} + + {server.description} + ) : null}
= ({ items={pluginTypeTabItems} className="w-full" size="small" + centered />
diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 7b13f14f7a..1906d2e55d 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -71,7 +71,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 171, + version: 172, blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index 5ca545d838..7fb7f40d2e 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -2628,132 +2628,6 @@ const migrateConfig = { return state } }, - '162': (state: RootState) => { - try { - // @ts-ignore - if (state?.agents?.agents) { - // @ts-ignore - state.assistants.presets = [...state.agents.agents] - // @ts-ignore - delete state.agents.agents - } - - if (state.settings.sidebarIcons) { - state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { - // @ts-ignore - return icon === 'agents' ? 'store' : icon - }) - } - - state.llm.providers.forEach((provider) => { - if (provider.anthropicApiHost) { - return - } - - switch (provider.id) { - case 'deepseek': - provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' - break - case 'moonshot': - provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' - break - case 'zhipu': - provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' - break - case 'dashscope': - provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy' - break - case 'modelscope': - provider.anthropicApiHost = 'https://api-inference.modelscope.cn' - break - case 'aihubmix': - provider.anthropicApiHost = 'https://aihubmix.com' - break - case 'new-api': - provider.anthropicApiHost = 'http://localhost:3000' - break - case 'grok': - provider.anthropicApiHost = 'https://api.x.ai' - } - }) - return state - } catch (error) { - logger.error('migrate 162 error', error as Error) - return state - } - }, - '163': (state: RootState) => { - try { - addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) - state.llm.providers.forEach((provider) => { - if (provider.id === 'cherryin') { - provider.anthropicApiHost = 'https://open.cherryin.net' - } - }) - state.paintings.ovms_paintings = [] - return state - } catch (error) { - logger.error('migrate 163 error', error as Error) - return state - } - }, - '164': (state: RootState) => { - try { - addMiniApp(state, 'ling') - return state - } catch (error) { - logger.error('migrate 164 error', error as Error) - return state - } - }, - '165': (state: RootState) => { - try { - addMiniApp(state, 'huggingchat') - return state - } catch (error) { - logger.error('migrate 165 error', error as Error) - return state - } - }, - '166': (state: RootState) => { - try { - if (state.assistants.presets === undefined) { - state.assistants.presets = [] - } - state.assistants.presets.forEach((preset) => { - if (!preset.settings) { - preset.settings = DEFAULT_ASSISTANT_SETTINGS - } else if (!preset.settings.toolUseMode) { - preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode - } - }) - // 更新阿里云百炼的 Anthropic API 地址 - const dashscopeProvider = state.llm.providers.find((provider) => provider.id === 'dashscope') - if (dashscopeProvider) { - dashscopeProvider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' - } - - state.llm.providers.forEach((provider) => { - if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { - provider.type = 'new-api' - } - if (provider.id === SystemProviderIds.longcat) { - // https://longcat.chat/platform/docs/zh/#anthropic-api-%E6%A0%BC%E5%BC%8F - if (!provider.anthropicApiHost) { - provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' - } - } - }) - return state - } catch (error) { - logger.error('migrate 166 error', error as Error) - return state - } - }, '167': (state: RootState) => { try { addProvider(state, 'huggingface') @@ -2822,6 +2696,98 @@ const migrateConfig = { logger.error('migrate 171 error', error as Error) return state } + }, + '172': (state: RootState) => { + try { + // Add ling and huggingchat mini apps + addMiniApp(state, 'ling') + addMiniApp(state, 'huggingchat') + + // Add ovocr provider and clear ovms paintings + addOcrProvider(state, BUILTIN_OCR_PROVIDERS_MAP.ovocr) + if (isEmpty(state.paintings.ovms_paintings)) { + state.paintings.ovms_paintings = [] + } + + // Migrate agents to assistants presets + // @ts-ignore + if (state?.agents?.agents) { + // @ts-ignore + state.assistants.presets = [...state.agents.agents] + // @ts-ignore + delete state.agents.agents + } + + // Initialize assistants presets + if (state.assistants.presets === undefined) { + state.assistants.presets = [] + } + + // Migrate assistants presets + state.assistants.presets.forEach((preset) => { + if (!preset.settings) { + preset.settings = DEFAULT_ASSISTANT_SETTINGS + } else if (!preset.settings.toolUseMode) { + preset.settings.toolUseMode = DEFAULT_ASSISTANT_SETTINGS.toolUseMode + } + }) + + // Migrate sidebar icons + if (state.settings.sidebarIcons) { + state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + state.settings.sidebarIcons.disabled = state.settings.sidebarIcons.disabled.map((icon) => { + // @ts-ignore + return icon === 'agents' ? 'store' : icon + }) + } + + // Migrate llm providers + state.llm.providers.forEach((provider) => { + if (provider.id === SystemProviderIds['new-api'] && provider.type !== 'new-api') { + provider.type = 'new-api' + } + + switch (provider.id) { + case 'deepseek': + provider.anthropicApiHost = 'https://api.deepseek.com/anthropic' + break + case 'moonshot': + provider.anthropicApiHost = 'https://api.moonshot.cn/anthropic' + break + case 'zhipu': + provider.anthropicApiHost = 'https://open.bigmodel.cn/api/anthropic' + break + case 'dashscope': + provider.anthropicApiHost = 'https://dashscope.aliyuncs.com/apps/anthropic' + break + case 'modelscope': + provider.anthropicApiHost = 'https://api-inference.modelscope.cn' + break + case 'aihubmix': + provider.anthropicApiHost = 'https://aihubmix.com' + break + case 'new-api': + provider.anthropicApiHost = 'http://localhost:3000' + break + case 'grok': + provider.anthropicApiHost = 'https://api.x.ai' + break + case 'cherryin': + provider.anthropicApiHost = 'https://open.cherryin.net' + break + case 'longcat': + provider.anthropicApiHost = 'https://api.longcat.chat/anthropic' + break + } + }) + return state + } catch (error) { + logger.error('migrate 172 error', error as Error) + return state + } } } diff --git a/src/renderer/src/ui/context-menu.tsx b/src/renderer/src/ui/context-menu.tsx deleted file mode 100644 index 7fdd27c38f..0000000000 --- a/src/renderer/src/ui/context-menu.tsx +++ /dev/null @@ -1,207 +0,0 @@ -'use client' - -import * as React from 'react' -import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' -import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react' -import { cn } from '@renderer/utils' - -function ContextMenu({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuTrigger({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuPortal({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSub({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuRadioGroup({ ...props }: React.ComponentProps) { - return -} - -function ContextMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - {children} - - - ) -} - -function ContextMenuSubContent({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuContent({ className, ...props }: React.ComponentProps) { - return ( - - - - ) -} - -function ContextMenuItem({ - className, - inset, - variant = 'default', - ...props -}: React.ComponentProps & { - inset?: boolean - variant?: 'default' | 'destructive' -}) { - return ( - - ) -} - -function ContextMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function ContextMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean -}) { - return ( - - ) -} - -function ContextMenuSeparator({ className, ...props }: React.ComponentProps) { - return ( - - ) -} - -function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { - return ( - - ) -} - -export { - ContextMenu, - ContextMenuTrigger, - ContextMenuContent, - ContextMenuItem, - ContextMenuCheckboxItem, - ContextMenuRadioItem, - ContextMenuLabel, - ContextMenuSeparator, - ContextMenuShortcut, - ContextMenuGroup, - ContextMenuPortal, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuRadioGroup -}