mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-02 18:39:06 +08:00
Merge branch 'main' of github.com:CherryHQ/cherry-studio into v2
This commit is contained in:
commit
f32fa08c41
@ -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": {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
12
README.md
12
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<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// resources/scripts should be maintained manually
|
||||
export const HOME_CHERRY_DIR = '.cherrystudio'
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
})()
|
||||
@ -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)
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,88 +0,0 @@
|
||||
const https = require('https')
|
||||
const { loggerService } = require('@logger')
|
||||
|
||||
const logger = loggerService.withContext('IpService')
|
||||
|
||||
/**
|
||||
* 获取用户的IP地址所在国家
|
||||
* @returns {Promise<string>} 返回国家代码,默认为'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<boolean>} 如果用户在中国返回true,否则返回false
|
||||
*/
|
||||
async function isUserInChina() {
|
||||
const country = await getIpCountry()
|
||||
return country.toLowerCase() === 'cn'
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户位置获取适合的npm镜像URL
|
||||
* @returns {Promise<string>} 返回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
|
||||
}
|
||||
@ -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<boolean> {
|
||||
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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<ModelConfig[]> {
|
||||
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 {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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<http.Server>
|
||||
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(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
color: #2d3748;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
p {
|
||||
color: #718096;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>${title}</h1>
|
||||
<p>${message}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'text/plain' })
|
||||
res.end('Missing authorization code')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error processing OAuth callback:', error as Error)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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<string> {
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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<string, any> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Message> & Pick<Message, 'id'> = {
|
||||
id: messageId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedAt: isUiUpdateOnly ? undefined : new Date().toISOString(),
|
||||
...updates
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -3863,6 +3863,12 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"oauth": {
|
||||
"callback": {
|
||||
"message": "您可以关闭此页面并返回 Cherry Studio",
|
||||
"title": "认证成功"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"arguments": "参数",
|
||||
"availablePrompts": "可用提示",
|
||||
|
||||
@ -3863,6 +3863,12 @@
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"oauth": {
|
||||
"callback": {
|
||||
"message": "您可以關閉此頁面並返回 Cherry Studio",
|
||||
"title": "認證成功"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"arguments": "參數",
|
||||
"availablePrompts": "可用提示",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -3863,6 +3863,12 @@
|
||||
"usage": "Χρήση",
|
||||
"version": "Έκδοση"
|
||||
},
|
||||
"oauth": {
|
||||
"callback": {
|
||||
"message": "Μπορείτε να κλείσετε αυτήν τη σελίδα και να επιστρέψετε στο Cherry Studio",
|
||||
"title": "Επιτυχής Ταυτοποίηση"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"arguments": "Ορίσματα",
|
||||
"availablePrompts": "Διαθέσιμες Υποδείξεις",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -3863,6 +3863,12 @@
|
||||
"usage": "使用法",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"oauth": {
|
||||
"callback": {
|
||||
"message": "このページを閉じてCherry Studioに戻ることができます",
|
||||
"title": "認証成功"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"arguments": "引数",
|
||||
"availablePrompts": "利用可能なプロンプト",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -3863,6 +3863,12 @@
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
"oauth": {
|
||||
"callback": {
|
||||
"message": "Вы можете закрыть эту страницу и вернуться в Cherry Studio",
|
||||
"title": "Аутентификация Успешна"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"arguments": "Аргументы",
|
||||
"availablePrompts": "Доступные подсказки",
|
||||
|
||||
@ -84,7 +84,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</Tooltip>
|
||||
)}
|
||||
{isTopNavbar && !showAssistants && (
|
||||
<Tooltip placement="bottom" content={t('navbar.show_sidebar')} delay={800}>
|
||||
<Tooltip placement="bottom" content={t('navbar.show_sidebar')} delay={800} placement="right">
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8 }}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
|
||||
@ -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<Props> = ({ agentId, sessionId }) => {
|
||||
groupedMessages.map(([key, groupMessages]) => (
|
||||
<MessageGroup key={key} messages={groupMessages} topic={derivedTopic} />
|
||||
))
|
||||
) : session ? (
|
||||
<PermissionModeDisplay session={session} agentId={agentId} />
|
||||
) : (
|
||||
<EmptyState>{session ? 'No messages yet.' : 'Loading session...'}</EmptyState>
|
||||
<LoadingState>
|
||||
<Spin size="small" />
|
||||
</LoadingState>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
</ContextMenu>
|
||||
@ -77,10 +83,10 @@ const AgentSessionMessages: React.FC<Props> = ({ 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;
|
||||
`
|
||||
|
||||
|
||||
@ -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 && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.responseBody')}:</ErrorDetailLabel>
|
||||
<CodeViewer value={error.responseBody} className="source-view" language="json" expanded />
|
||||
</ErrorDetailItem>
|
||||
)}
|
||||
|
||||
{error.requestBodyValues && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.requestBodyValues')}:</ErrorDetailLabel>
|
||||
@ -390,13 +397,6 @@ const AiSdkError = ({ error }: { error: SerializedAiSdkErrorUnion }) => {
|
||||
</ErrorDetailItem>
|
||||
)}
|
||||
|
||||
{error.responseBody && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.responseBody')}:</ErrorDetailLabel>
|
||||
<CodeViewer value={error.responseBody} className="source-view" language="json" expanded />
|
||||
</ErrorDetailItem>
|
||||
)}
|
||||
|
||||
{error.data && (
|
||||
<ErrorDetailItem>
|
||||
<ErrorDetailLabel>{t('error.data')}:</ErrorDetailLabel>
|
||||
|
||||
@ -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: <Shield size={18} color="var(--color-primary)" />
|
||||
}
|
||||
case 'plan':
|
||||
return {
|
||||
icon: <Lightbulb size={18} color="#faad14" />
|
||||
}
|
||||
case 'acceptEdits':
|
||||
return {
|
||||
icon: <FileEdit size={18} color="#52c41a" />
|
||||
}
|
||||
case 'bypassPermissions':
|
||||
return {
|
||||
icon: <ShieldOff size={18} color="var(--color-error)" />
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: <Shield size={18} color="var(--color-primary)" />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const PermissionModeDisplay: FC<Props> = ({ 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 (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className="mx-2 cursor-pointer rounded-lg border-[0.5px] border-[var(--color-border)] px-3 py-2">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex shrink-0 items-center justify-center">{modeConfig.icon}</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap font-semibold text-[var(--color-text-1)] text-xs">
|
||||
{t(modeCard.titleKey, modeCard.titleFallback)}
|
||||
</div>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-[var(--color-text-2)] leading-[1.4]">
|
||||
{t(modeCard.descriptionKey, modeCard.descriptionFallback)}{' '}
|
||||
{t(modeCard.behaviorKey, modeCard.behaviorFallback)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionModeDisplay
|
||||
@ -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 = <Tag className="whitespace-pre-wrap break-all font-mono">{displayCommand}</Tag>
|
||||
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
@ -26,7 +35,15 @@ export function BashTool({
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
<div className="mt-1">
|
||||
<Tag className="whitespace-pre-wrap break-all font-mono">{input.command}</Tag>
|
||||
{needsTruncate ? (
|
||||
<Popover
|
||||
content={<div className="max-w-xl whitespace-pre-wrap break-all font-mono">{command}</div>}
|
||||
trigger="hover">
|
||||
{tagContent}
|
||||
</Popover>
|
||||
) : (
|
||||
tagContent
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
@ -95,7 +95,7 @@ const HeaderNavbar: FC<Props> = ({
|
||||
paddingRight: 0,
|
||||
minWidth: 'auto'
|
||||
}}>
|
||||
<Tooltip placement="bottom" content={t('navbar.show_sidebar')} delay={800}>
|
||||
<Tooltip placement="bottom" content={t('navbar.show_sidebar')} delay={800} placement="right">
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
|
||||
@ -181,7 +181,7 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, onExpand
|
||||
</Tooltip>
|
||||
)}
|
||||
{!showWorkspace && (
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
|
||||
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8} placement="right">
|
||||
<NavbarIcon onClick={handleToggleShowWorkspace}>
|
||||
<PanelRightClose size={18} />
|
||||
</NavbarIcon>
|
||||
|
||||
@ -459,11 +459,22 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
|
||||
key={server.id}
|
||||
className="border border-default-200"
|
||||
title={
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium text-sm">{server.name}</span>
|
||||
<div className="flex items-center justify-between gap-2 py-3">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{server.logoUrl && (
|
||||
<img
|
||||
src={server.logoUrl}
|
||||
alt={`${server.name} logo`}
|
||||
className="h-5 w-5 rounded object-cover"
|
||||
/>
|
||||
)}
|
||||
<span className="truncate font-medium text-sm">{server.name}</span>
|
||||
</div>
|
||||
{server.description ? (
|
||||
<span className="line-clamp-2 text-foreground-500 text-xs">{server.description}</span>
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-all text-foreground-500 text-xs">
|
||||
{server.description}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Switch
|
||||
|
||||
@ -263,6 +263,7 @@ export const PluginBrowser: FC<PluginBrowserProps> = ({
|
||||
items={pluginTypeTabItems}
|
||||
className="w-full"
|
||||
size="small"
|
||||
centered
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -71,7 +71,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 171,
|
||||
version: 172,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs', 'toolPermissions'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[inset]:pl-8 data-[state=open]:text-accent-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: 'default' | 'destructive'
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"data-[variant=destructive]:*:[svg]:!text-destructive relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[disabled]:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...props}>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn('px-2 py-1.5 font-medium text-foreground text-sm data-[inset]:pl-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn('ml-auto text-muted-foreground text-xs tracking-widest', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user