Compare commits
3 Commits
v1.1.10
...
feat/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3499cd449b | ||
|
|
a97c3d9695 | ||
|
|
9145e998c4 |
@@ -1,8 +1,8 @@
|
||||
# provider: generic
|
||||
# url: http://127.0.0.1:8080
|
||||
# updaterCacheDirName: cherry-studio-updater
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
# provider: generic
|
||||
# url: https://cherrystudio.ocool.online
|
||||
# provider: github
|
||||
# repo: cherry-studio
|
||||
# owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
|
||||
@@ -22,8 +22,7 @@ export default defineConfig({
|
||||
'@llm-tools/embedjs-loader-sitemap',
|
||||
'@llm-tools/embedjs-libsql',
|
||||
'@llm-tools/embedjs-loader-image',
|
||||
'p-queue',
|
||||
'webdav'
|
||||
'p-queue'
|
||||
]
|
||||
}),
|
||||
...visualizerPlugin('main')
|
||||
|
||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.1.10",
|
||||
"version": "1.1.8",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -50,10 +50,13 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.28#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.28-8e4393fa2d.patch",
|
||||
"@llm-tools/embedjs-libsql": "^0.1.28",
|
||||
"@llm-tools/embedjs-loader-csv": "^0.1.28",
|
||||
@@ -68,8 +71,8 @@
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"adm-zip": "^0.5.16",
|
||||
"apache-arrow": "^18.1.0",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
@@ -79,33 +82,29 @@
|
||||
"fetch-socks": "^1.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"socks-proxy-agent": "^8.0.3",
|
||||
"tar": "^7.4.3",
|
||||
"tokenx": "^0.4.1",
|
||||
"undici": "^7.4.0",
|
||||
"webdav": "^5.8.0",
|
||||
"webdav": "4.11.4",
|
||||
"zipread": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "^0.4.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@llm-tools/embedjs-loader-image": "^0.1.28",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
@@ -144,9 +143,7 @@
|
||||
"lint-staged": "^15.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.4",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A4.77.3#~/.yarn/patches/openai-npm-4.77.3-59c6d42e7a.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -171,7 +168,6 @@
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"tokenx": "^0.4.1",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "^5.0.12"
|
||||
|
||||
13
packages/artifacts/package-lock.json
generated
Normal file
13
packages/artifacts/package-lock.json
generated
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@cherry-studio/artifacts",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
1358
packages/database/package-lock.json
generated
Normal file
1358
packages/database/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
314
resources/scripts/install-node.js
Normal file
314
resources/scripts/install-node.js
Normal file
@@ -0,0 +1,314 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const https = require('https')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
// 配置
|
||||
const NODE_VERSION = process.env.NODE_VERSION || '18.18.0' // 默认版本
|
||||
const NODE_RELEASE_BASE_URL = 'https://nodejs.org/dist'
|
||||
|
||||
// 平台映射
|
||||
const NODE_PACKAGES = {
|
||||
'darwin-arm64': `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||||
'darwin-x64': `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
||||
'win32-x64': `node-v${NODE_VERSION}-win32-x64.zip`,
|
||||
'win32-ia32': `node-v${NODE_VERSION}-win32-x86.zip`,
|
||||
'linux-x64': `node-v${NODE_VERSION}-linux-x64.tar.gz`,
|
||||
'linux-arm64': `node-v${NODE_VERSION}-linux-arm64.tar.gz`,
|
||||
}
|
||||
|
||||
// 辅助函数 - 递归复制目录
|
||||
function copyFolderRecursiveSync(source, target) {
|
||||
// 检查目标目录是否存在,不存在则创建
|
||||
if (!fs.existsSync(target)) {
|
||||
fs.mkdirSync(target, { recursive: true });
|
||||
}
|
||||
|
||||
// 读取源目录中的所有文件和文件夹
|
||||
const files = fs.readdirSync(source);
|
||||
|
||||
// 循环处理每个文件/文件夹
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(source, file);
|
||||
const targetPath = path.join(target, file);
|
||||
|
||||
// 检查是文件还是文件夹
|
||||
if (fs.statSync(sourcePath).isDirectory()) {
|
||||
// 如果是文件夹,递归复制
|
||||
copyFolderRecursiveSync(sourcePath, targetPath);
|
||||
} else {
|
||||
// 如果是文件,直接复制
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 二进制文件存放目录
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
|
||||
// 创建二进制文件存放目录
|
||||
async function createBinariesDir() {
|
||||
if (!fs.existsSync(binariesDir)) {
|
||||
console.log(`Creating binaries directory at ${binariesDir}`)
|
||||
fs.mkdirSync(binariesDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前平台对应的包名
|
||||
function getPackageForPlatform() {
|
||||
const platform = os.platform()
|
||||
const arch = os.arch()
|
||||
const key = `${platform}-${arch}`
|
||||
|
||||
console.log(`Current platform: ${platform}, architecture: ${arch}`)
|
||||
|
||||
if (!NODE_PACKAGES[key]) {
|
||||
throw new Error(`Unsupported platform/architecture: ${key}`)
|
||||
}
|
||||
|
||||
return NODE_PACKAGES[key]
|
||||
}
|
||||
|
||||
// 下载 Node.js
|
||||
async function downloadNodeJs() {
|
||||
const packageName = getPackageForPlatform()
|
||||
const downloadUrl = `${NODE_RELEASE_BASE_URL}/v${NODE_VERSION}/${packageName}`
|
||||
const tempFilePath = path.join(os.tmpdir(), packageName)
|
||||
|
||||
console.log(`Downloading Node.js v${NODE_VERSION} from ${downloadUrl}`)
|
||||
console.log(`Temp file path: ${tempFilePath}`)
|
||||
|
||||
// 如果临时文件已存在,先删除
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(tempFilePath)
|
||||
|
||||
https.get(downloadUrl, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Download started, status code: ${response.statusCode}`)
|
||||
|
||||
response.pipe(file)
|
||||
|
||||
file.on('finish', () => {
|
||||
file.close()
|
||||
console.log('Download completed')
|
||||
resolve(tempFilePath)
|
||||
})
|
||||
|
||||
file.on('error', (err) => {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
reject(err)
|
||||
})
|
||||
}).on('error', (err) => {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
fs.unlinkSync(tempFilePath)
|
||||
}
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 解压 Node.js 包
|
||||
async function extractNodeJs(filePath) {
|
||||
const platform = os.platform()
|
||||
const extractDir = path.join(os.tmpdir(), `node-v${NODE_VERSION}-extract`)
|
||||
|
||||
if (fs.existsSync(extractDir)) {
|
||||
console.log(`Removing existing extract directory: ${extractDir}`)
|
||||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log(`Creating extract directory: ${extractDir}`)
|
||||
fs.mkdirSync(extractDir, { recursive: true })
|
||||
|
||||
console.log(`Extracting to ${extractDir}`)
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Windows 使用内置的解压工具
|
||||
try {
|
||||
const AdmZip = require('adm-zip')
|
||||
console.log(`Using adm-zip to extract ${filePath}`)
|
||||
const zip = new AdmZip(filePath)
|
||||
zip.extractAllTo(extractDir, true)
|
||||
console.log(`Extraction completed using adm-zip`)
|
||||
} catch (error) {
|
||||
console.error(`Error using adm-zip: ${error}`)
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
// Linux/Mac 使用 tar
|
||||
try {
|
||||
console.log(`Using tar to extract ${filePath} to ${extractDir}`)
|
||||
execSync(`tar -xzf "${filePath}" -C "${extractDir}"`, { stdio: 'inherit' })
|
||||
console.log(`Extraction completed using tar`)
|
||||
} catch (error) {
|
||||
console.error(`Error using tar: ${error}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return extractDir
|
||||
}
|
||||
|
||||
// 安装 Node.js
|
||||
async function installNodeJs(extractDir) {
|
||||
const platform = os.platform()
|
||||
console.log(`Finding extracted Node.js directory in ${extractDir}`)
|
||||
|
||||
const items = fs.readdirSync(extractDir)
|
||||
console.log(`Found items in extract directory: ${items.join(', ')}`)
|
||||
|
||||
// 找到包含"node-v"的目录名
|
||||
const folderName = items.find(item => item.startsWith('node-v'))
|
||||
|
||||
if (!folderName) {
|
||||
throw new Error(`Could not find Node.js directory in ${extractDir}`)
|
||||
}
|
||||
|
||||
console.log(`Found Node.js directory: ${folderName}`)
|
||||
const nodeBinPath = path.join(extractDir, folderName, 'bin')
|
||||
|
||||
console.log(`Node.js bin path: ${nodeBinPath}`)
|
||||
|
||||
// 复制 node 和 npm
|
||||
if (platform === 'win32') {
|
||||
// Windows
|
||||
console.log('Installing Node.js binaries for Windows')
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'node.exe'),
|
||||
path.join(binariesDir, 'node.exe')
|
||||
)
|
||||
console.log(`Copied node.exe to ${path.join(binariesDir, 'node.exe')}`)
|
||||
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'npm.cmd'),
|
||||
path.join(binariesDir, 'npm.cmd')
|
||||
)
|
||||
console.log(`Copied npm.cmd to ${path.join(binariesDir, 'npm.cmd')}`)
|
||||
|
||||
fs.copyFileSync(
|
||||
path.join(extractDir, folderName, 'npx.cmd'),
|
||||
path.join(binariesDir, 'npx.cmd')
|
||||
)
|
||||
console.log(`Copied npx.cmd to ${path.join(binariesDir, 'npx.cmd')}`)
|
||||
} else {
|
||||
// Linux/Mac
|
||||
console.log('Installing Node.js binaries for Linux/Mac')
|
||||
fs.copyFileSync(
|
||||
path.join(nodeBinPath, 'node'),
|
||||
path.join(binariesDir, 'node')
|
||||
)
|
||||
console.log(`Copied node to ${path.join(binariesDir, 'node')}`)
|
||||
|
||||
// 创建npm脚本,指向正确路径
|
||||
const npmScript = `#!/usr/bin/env node
|
||||
require("./node_modules/npm/lib/cli.js")(process)`;
|
||||
fs.writeFileSync(path.join(binariesDir, 'npm'), npmScript);
|
||||
console.log(`Created npm script at ${path.join(binariesDir, 'npm')}`);
|
||||
|
||||
// 创建npx脚本,指向正确路径
|
||||
const npxScript = `#!/usr/bin/env node
|
||||
require("./node_modules/npm/bin/npx-cli.js")`;
|
||||
fs.writeFileSync(path.join(binariesDir, 'npx'), npxScript);
|
||||
console.log(`Created npx script at ${path.join(binariesDir, 'npx')}`);
|
||||
|
||||
// 设置执行权限
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'node')}"`)
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'npm')}"`)
|
||||
execSync(`chmod +x "${path.join(binariesDir, 'npx')}"`)
|
||||
console.log('Set executable permissions for Node.js binaries')
|
||||
}
|
||||
|
||||
// 复制 npm 相关文件和目录
|
||||
const npmDir = path.join(binariesDir, 'node_modules', 'npm')
|
||||
fs.mkdirSync(npmDir, { recursive: true })
|
||||
console.log(`Created npm directory at ${npmDir}`)
|
||||
|
||||
// 复制 npm 目录的内容
|
||||
const srcNpmDir = path.join(extractDir, folderName, 'lib', 'node_modules', 'npm')
|
||||
console.log(`Copying npm files from ${srcNpmDir} to ${npmDir}`)
|
||||
|
||||
const files = fs.readdirSync(srcNpmDir)
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = path.join(srcNpmDir, file)
|
||||
const destPath = path.join(npmDir, file)
|
||||
|
||||
if (fs.lstatSync(srcPath).isDirectory()) {
|
||||
// 使用自定义函数代替fs.cpSync,确保兼容性
|
||||
console.log(`Copying directory: ${file}`)
|
||||
copyFolderRecursiveSync(srcPath, destPath)
|
||||
} else {
|
||||
console.log(`Copying file: ${file}`)
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Node.js installation completed successfully')
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
async function cleanup(filePath, extractDir) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`Cleaning up temp file: ${filePath}`)
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
|
||||
if (fs.existsSync(extractDir)) {
|
||||
console.log(`Cleaning up extract directory: ${extractDir}`)
|
||||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
console.log('Cleaned up temporary files')
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 主安装函数
|
||||
async function install() {
|
||||
try {
|
||||
console.log(`Starting Node.js v${NODE_VERSION} installation...`)
|
||||
|
||||
await createBinariesDir()
|
||||
console.log('Binary directory created/verified')
|
||||
|
||||
const filePath = await downloadNodeJs()
|
||||
console.log(`Downloaded Node.js to ${filePath}`)
|
||||
|
||||
const extractDir = await extractNodeJs(filePath)
|
||||
console.log(`Extracted Node.js to ${extractDir}`)
|
||||
|
||||
await installNodeJs(extractDir)
|
||||
console.log('Installed Node.js binaries')
|
||||
|
||||
await cleanup(filePath, extractDir)
|
||||
console.log('Cleanup completed')
|
||||
|
||||
console.log(`Node.js v${NODE_VERSION} has been installed successfully at ${binariesDir}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Installation failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 执行安装
|
||||
install()
|
||||
.then(() => {
|
||||
console.log('Installation process completed successfully')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fatal error during installation:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,130 +0,0 @@
|
||||
/**
|
||||
* OCOOL_API_KEY=sk-abcxxxxxxxxxxxxxxxxxxxxxxx123 ts-node scripts/update-i18n.ts
|
||||
*/
|
||||
|
||||
// OCOOL API KEY
|
||||
const OCOOL_API_KEY = process.env.OCOOL_API_KEY
|
||||
|
||||
const INDEX = [
|
||||
// 语言的名称 代码 用来翻译的模型
|
||||
{ name: 'France', code: 'fr-fr', model: 'qwen2.5-32b-instruct' },
|
||||
{ name: 'Spanish', code: 'es-es', model: 'qwen2.5-32b-instruct' },
|
||||
{ name: 'Portuguese', code: 'pt-pt', model: 'qwen2.5-72b-instruct' },
|
||||
{ name: 'Greek', code: 'el-gr', model: 'qwen-turbo' }
|
||||
]
|
||||
|
||||
const fs = require('fs')
|
||||
import OpenAI from 'openai'
|
||||
|
||||
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: OCOOL_API_KEY,
|
||||
baseURL: 'https://one.ocoolai.com/v1'
|
||||
})
|
||||
|
||||
// 递归遍历翻译
|
||||
async function translate(zh: object, obj: object, target: string, model: string, updateFile) {
|
||||
const texts: { [key: string]: string } = {}
|
||||
for (const e in zh) {
|
||||
if (typeof zh[e] == 'object') {
|
||||
// 遍历下一层
|
||||
if (!obj[e] || typeof obj[e] != 'object') obj[e] = {}
|
||||
await translate(zh[e], obj[e], target, model, updateFile)
|
||||
} else {
|
||||
// 加入到本层待翻译列表
|
||||
if (!obj[e] || typeof obj[e] != 'string') {
|
||||
texts[e] = zh[e]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(texts).length > 0) {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: model,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on Russian language corpora, you are proficient in using the Russian language.
|
||||
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the Russian language.
|
||||
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
|
||||
Output in JSON.
|
||||
######################################################
|
||||
INPUT
|
||||
######################################################
|
||||
${JSON.stringify({
|
||||
confirm: '确定要备份数据吗?',
|
||||
select_model: '选择模型',
|
||||
title: '文件',
|
||||
deeply_thought: '已深度思考(用时 {{secounds}} 秒)'
|
||||
})}
|
||||
######################################################
|
||||
MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
|
||||
######################################################
|
||||
`
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: JSON.stringify({
|
||||
confirm: 'Подтвердите резервное копирование данных?',
|
||||
select_model: 'Выберите Модель',
|
||||
title: 'Файл',
|
||||
deeply_thought: 'Глубоко продумано (заняло {{seconds}} секунд)'
|
||||
})
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `
|
||||
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${target} language corpora, you are proficient in using the ${target} language.
|
||||
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${target} language.
|
||||
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
|
||||
Output in JSON.
|
||||
######################################################
|
||||
INPUT
|
||||
######################################################
|
||||
${JSON.stringify(texts)}
|
||||
######################################################
|
||||
MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
|
||||
######################################################
|
||||
`
|
||||
}
|
||||
]
|
||||
})
|
||||
// 添加翻译后的键值,并打印错译漏译内容
|
||||
try {
|
||||
const result = JSON.parse(completion.choices[0].message.content!)
|
||||
for (const e in texts) {
|
||||
if (result[e] && typeof result[e] === 'string') {
|
||||
obj[e] = result[e]
|
||||
} else {
|
||||
console.log('[warning]', `missing value "${e}" in ${target} translation`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[error]', e)
|
||||
for (const e in texts) {
|
||||
console.log('[warning]', `missing value "${e}" in ${target} translation`)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 删除多余的键值
|
||||
for (const e in obj) {
|
||||
if (!zh[e]) {
|
||||
delete obj[e]
|
||||
}
|
||||
}
|
||||
// 更新文件
|
||||
updateFile()
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
for (const { name, code, model } of INDEX) {
|
||||
const obj = fs.existsSync(`src/renderer/src/i18n/translate/${code}.json`)
|
||||
? JSON.parse(fs.readFileSync(`src/renderer/src/i18n/translate/${code}.json`, 'utf8'))
|
||||
: {}
|
||||
await translate(zh, obj, name, model, () => {
|
||||
fs.writeFileSync(`src/renderer/src/i18n/translate/${code}.json`, JSON.stringify(obj, null, 2), 'utf8')
|
||||
})
|
||||
}
|
||||
})()
|
||||
16
src/@types/index.d.ts
vendored
Normal file
16
src/@types/index.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface NodeAppType {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
author?: string
|
||||
homepage?: string
|
||||
repositoryUrl?: string
|
||||
port?: number
|
||||
installCommand?: string
|
||||
buildCommand?: string
|
||||
startCommand?: string
|
||||
isInstalled: boolean
|
||||
isRunning: boolean
|
||||
url?: string
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import EmbeddingsFactory from './EmbeddingsFactory'
|
||||
|
||||
export default class Embeddings {
|
||||
private sdk: BaseEmbeddings
|
||||
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
|
||||
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
|
||||
}
|
||||
public async init(): Promise<void> {
|
||||
return this.sdk.init()
|
||||
}
|
||||
public async getDimensions(): Promise<number> {
|
||||
return this.sdk.getDimensions()
|
||||
}
|
||||
public async embedDocuments(texts: string[]): Promise<number[][]> {
|
||||
return this.sdk.embedDocuments(texts)
|
||||
}
|
||||
|
||||
public async embedQuery(text: string): Promise<number[]> {
|
||||
return this.sdk.embedQuery(text)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||
import { AzureOpenAiEmbeddings } from '@llm-tools/embedjs-openai/src/azure-openai-embeddings'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
|
||||
import VoyageEmbeddings from './VoyageEmbeddings'
|
||||
|
||||
export default class EmbeddingsFactory {
|
||||
static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
|
||||
const batchSize = 10
|
||||
if (model.includes('voyage')) {
|
||||
return new VoyageEmbeddings({
|
||||
modelName: model,
|
||||
apiKey,
|
||||
outputDimension: dimensions,
|
||||
batchSize: 8
|
||||
})
|
||||
}
|
||||
if (apiVersion !== undefined) {
|
||||
return new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
}
|
||||
return new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
dimensions,
|
||||
batchSize,
|
||||
configuration: { baseURL }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
|
||||
import { BaseEmbeddings } from '@llm-tools/embedjs-interfaces'
|
||||
|
||||
export default class VoyageEmbeddings extends BaseEmbeddings {
|
||||
private model: _VoyageEmbeddings
|
||||
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
|
||||
super()
|
||||
if (!this.configuration) this.configuration = {}
|
||||
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
|
||||
|
||||
if (!this.configuration.outputDimension) {
|
||||
throw new Error('You need to pass in the optional dimensions parameter for this model')
|
||||
}
|
||||
this.model = new _VoyageEmbeddings(this.configuration)
|
||||
}
|
||||
override async getDimensions(): Promise<number> {
|
||||
if (!this.configuration?.outputDimension) {
|
||||
throw new Error('You need to pass in the optional dimensions parameter for this model')
|
||||
}
|
||||
return this.configuration?.outputDimension
|
||||
}
|
||||
|
||||
override async embedDocuments(texts: string[]): Promise<number[][]> {
|
||||
return this.model.embedDocuments(texts)
|
||||
}
|
||||
|
||||
override async embedQuery(text: string): Promise<number[]> {
|
||||
return this.model.embedQuery(text)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { app, ipcMain } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
@@ -22,12 +21,6 @@ if (!app.requestSingleInstanceLock()) {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||
|
||||
// Mac: Hide dock icon before window creation when launch to tray is set
|
||||
const isLaunchToTray = configManager.getLaunchToTray()
|
||||
if (isLaunchToTray) {
|
||||
app.dock?.hide()
|
||||
}
|
||||
|
||||
const mainWindow = windowService.createMainWindow()
|
||||
new TrayService()
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { MCPServer, Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
@@ -25,6 +24,7 @@ import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
import { getFilesDir } from './utils/file'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import NodeAppService from './services/NodeAppService'
|
||||
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
@@ -69,30 +69,11 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
configManager.setLanguage(language)
|
||||
})
|
||||
|
||||
// launch on boot
|
||||
ipcMain.handle('app:set-launch-on-boot', (_, openAtLogin: boolean) => {
|
||||
// Set login item settings for windows and mac
|
||||
// linux is not supported because it requires more file operations
|
||||
if (isWin || isMac) {
|
||||
app.setLoginItemSettings({ openAtLogin })
|
||||
}
|
||||
})
|
||||
|
||||
// launch to tray
|
||||
ipcMain.handle('app:set-launch-to-tray', (_, isActive: boolean) => {
|
||||
configManager.setLaunchToTray(isActive)
|
||||
})
|
||||
|
||||
// tray
|
||||
ipcMain.handle('app:set-tray', (_, isActive: boolean) => {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
// to tray on close
|
||||
ipcMain.handle('app:set-tray-on-close', (_, isActive: boolean) => {
|
||||
configManager.setTrayOnClose(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||
@@ -277,6 +258,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
ipcMain.handle('mcp:cleanup', async () => mcpService.cleanup())
|
||||
|
||||
// Shell API
|
||||
ipcMain.handle('shell:openExternal', async (_, url: string) => {
|
||||
try {
|
||||
log.info(`Opening external URL: ${url}`)
|
||||
return await shell.openExternal(url)
|
||||
} catch (error) {
|
||||
log.error('Error opening external URL:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('app:is-binary-exist', (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle('app:get-binary-path', (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle('app:install-uv-binary', () => runInstallScript('install-uv.js'))
|
||||
@@ -296,4 +288,53 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('copilot:get-token', CopilotService.getToken)
|
||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||
|
||||
// Node app management
|
||||
const nodeAppService = NodeAppService.getInstance()
|
||||
|
||||
ipcMain.handle('nodeapp:list', async () => await nodeAppService.getAllApps())
|
||||
|
||||
ipcMain.handle('nodeapp:add', async (_, app) => await nodeAppService.addApp(app))
|
||||
|
||||
ipcMain.handle('nodeapp:install', async (_, appId) => await nodeAppService.installApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:update', async (_, appId) => await nodeAppService.updateApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:start', async (_, appId) => await nodeAppService.startApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:stop', async (_, appId) => await nodeAppService.stopApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:uninstall', async (_, appId) => await nodeAppService.uninstallApp(appId))
|
||||
|
||||
ipcMain.handle('nodeapp:deploy-zip', async (_, zipPath, options) => await nodeAppService.deployFromZip(zipPath, options))
|
||||
|
||||
ipcMain.handle('nodeapp:deploy-git', async (_, repoUrl, options) => await nodeAppService.deployFromGit(repoUrl, options))
|
||||
|
||||
ipcMain.handle('nodeapp:check-node', async () => {
|
||||
const isNodeInstalled = await isBinaryExists('node')
|
||||
return isNodeInstalled
|
||||
})
|
||||
|
||||
ipcMain.handle('nodeapp:install-node', async () => {
|
||||
return await nodeAppService.installNodeJs()
|
||||
})
|
||||
|
||||
// Listen for changes in Node.js apps and notify renderer
|
||||
nodeAppService.on('apps-updated', (apps) => {
|
||||
mainWindow?.webContents.send('nodeapp:updated', apps)
|
||||
})
|
||||
|
||||
app.on('before-quit', () => nodeAppService.cleanup())
|
||||
|
||||
// 运行简单命令
|
||||
ipcMain.handle('app:run-command', async (_, command: string) => {
|
||||
try {
|
||||
const { execSync } = require('child_process')
|
||||
const result = execSync(command).toString()
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error('Error running command:', error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export default class DefaultReranker extends BaseReranker {
|
||||
constructor(base: KnowledgeBaseParams) {
|
||||
super(base)
|
||||
}
|
||||
|
||||
async rerank(): Promise<ExtractChunkData[]> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
@@ -10,15 +10,9 @@ export default class JinaReranker extends BaseReranker {
|
||||
}
|
||||
|
||||
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
? this.base.rerankBaseURL.slice(0, -1)
|
||||
: this.base.rerankBaseURL
|
||||
|
||||
// 必须携带/v1,否则会404
|
||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||
baseURL = `${baseURL}/v1`
|
||||
}
|
||||
|
||||
const url = `${baseURL}/rerank`
|
||||
|
||||
const requestBody = {
|
||||
@@ -46,9 +40,9 @@ export default class JinaReranker extends BaseReranker {
|
||||
})
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Jina Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
} catch (error) {
|
||||
console.error('Jina Reranker API 错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import BaseReranker from './BaseReranker'
|
||||
import DefaultReranker from './DefaultReranker'
|
||||
import JinaReranker from './JinaReranker'
|
||||
import SiliconFlowReranker from './SiliconFlowReranker'
|
||||
import VoyageReranker from './VoyageReranker'
|
||||
|
||||
export default class RerankerFactory {
|
||||
static create(base: KnowledgeBaseParams): BaseReranker {
|
||||
@@ -12,8 +11,6 @@ export default class RerankerFactory {
|
||||
return new SiliconFlowReranker(base)
|
||||
} else if (base.rerankModelProvider === 'jina') {
|
||||
return new JinaReranker(base)
|
||||
} else if (base.rerankModelProvider === 'voyageai') {
|
||||
return new VoyageReranker(base)
|
||||
}
|
||||
return new DefaultReranker(base)
|
||||
}
|
||||
|
||||
@@ -10,15 +10,9 @@ export default class SiliconFlowReranker extends BaseReranker {
|
||||
}
|
||||
|
||||
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
const baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
? this.base.rerankBaseURL.slice(0, -1)
|
||||
: this.base.rerankBaseURL
|
||||
|
||||
// 必须携带/v1,否则会404
|
||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||
baseURL = `${baseURL}/v1`
|
||||
}
|
||||
|
||||
const url = `${baseURL}/rerank`
|
||||
|
||||
const requestBody = {
|
||||
@@ -48,9 +42,9 @@ export default class SiliconFlowReranker extends BaseReranker {
|
||||
})
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('SiliconFlow Reranker API 错误:', error.status)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
} catch (error) {
|
||||
console.error('SiliconFlow Reranker API 错误:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { KnowledgeBaseParams } from '@types'
|
||||
import axios from 'axios'
|
||||
|
||||
import BaseReranker from './BaseReranker'
|
||||
|
||||
export default class VoyageReranker extends BaseReranker {
|
||||
constructor(base: KnowledgeBaseParams) {
|
||||
super(base)
|
||||
}
|
||||
|
||||
public rerank = async (query: string, searchResults: ExtractChunkData[]): Promise<ExtractChunkData[]> => {
|
||||
let baseURL = this.base?.rerankBaseURL?.endsWith('/')
|
||||
? this.base.rerankBaseURL.slice(0, -1)
|
||||
: this.base.rerankBaseURL
|
||||
|
||||
if (baseURL && !baseURL.endsWith('/v1')) {
|
||||
baseURL = `${baseURL}/v1`
|
||||
}
|
||||
|
||||
const url = `${baseURL}/rerank`
|
||||
|
||||
const requestBody = {
|
||||
model: this.base.rerankModel,
|
||||
query,
|
||||
documents: searchResults.map((doc) => doc.pageContent),
|
||||
top_k: this.base.topN,
|
||||
return_documents: false,
|
||||
truncation: true
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(url, requestBody, {
|
||||
headers: {
|
||||
...this.defaultHeaders()
|
||||
}
|
||||
})
|
||||
|
||||
const rerankResults = data.data
|
||||
|
||||
const resultMap = new Map(rerankResults.map((result: any) => [result.index, result.relevance_score || 0]))
|
||||
|
||||
return searchResults
|
||||
.map((doc: ExtractChunkData, index: number) => {
|
||||
const score = resultMap.get(index)
|
||||
if (score === undefined) return undefined
|
||||
|
||||
return {
|
||||
...doc,
|
||||
score
|
||||
}
|
||||
})
|
||||
.filter((doc): doc is ExtractChunkData => doc !== undefined)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
} catch (error: any) {
|
||||
console.error('Voyage Reranker API 错误:', error.message || error)
|
||||
throw new Error(`${error} - BaseUrl: ${baseURL}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,6 @@ export class ConfigManager {
|
||||
this.store.set('theme', theme)
|
||||
}
|
||||
|
||||
getLaunchToTray(): boolean {
|
||||
return !!this.store.get('launchToTray', false)
|
||||
}
|
||||
|
||||
setLaunchToTray(value: boolean) {
|
||||
this.store.set('launchToTray', value)
|
||||
}
|
||||
|
||||
getTray(): boolean {
|
||||
return !!this.store.get('tray', true)
|
||||
}
|
||||
@@ -47,14 +39,6 @@ export class ConfigManager {
|
||||
this.notifySubscribers('tray', value)
|
||||
}
|
||||
|
||||
getTrayOnClose(): boolean {
|
||||
return !!this.store.get('trayOnClose', true)
|
||||
}
|
||||
|
||||
setTrayOnClose(value: boolean) {
|
||||
this.store.set('trayOnClose', value)
|
||||
}
|
||||
|
||||
getZoomFactor(): number {
|
||||
return this.store.get('zoomFactor', 1) as number
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ import { FileType } from '@types'
|
||||
import fs from 'fs'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
import { proxyManager } from './ProxyManager'
|
||||
|
||||
export class GeminiService {
|
||||
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
||||
private static readonly CACHE_DURATION = 3000
|
||||
|
||||
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
|
||||
proxyManager.setGlobalProxy()
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
const uploadResult = await fileManager.uploadFile(file.path, {
|
||||
mimeType: 'application/pdf',
|
||||
@@ -29,6 +31,7 @@ export class GeminiService {
|
||||
file: FileType,
|
||||
apiKey: string
|
||||
): Promise<FileMetadataResponse | undefined> {
|
||||
proxyManager.setGlobalProxy()
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
|
||||
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
||||
@@ -52,11 +55,13 @@ export class GeminiService {
|
||||
}
|
||||
|
||||
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
|
||||
proxyManager.setGlobalProxy()
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
return await fileManager.listFiles()
|
||||
}
|
||||
|
||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
||||
proxyManager.setGlobalProxy()
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
await fileManager.deleteFile(fileId)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { LibSqlDb } from '@llm-tools/embedjs-libsql'
|
||||
import { SitemapLoader } from '@llm-tools/embedjs-loader-sitemap'
|
||||
import { WebLoader } from '@llm-tools/embedjs-loader-web'
|
||||
import Embeddings from '@main/embeddings/Embeddings'
|
||||
import { AzureOpenAiEmbeddings, OpenAiEmbeddings } from '@llm-tools/embedjs-openai'
|
||||
import { addFileLoader } from '@main/loader'
|
||||
import Reranker from '@main/reranker/Reranker'
|
||||
import { proxyManager } from '@main/services/ProxyManager'
|
||||
import { windowService } from '@main/services/WindowService'
|
||||
import { getInstanceName } from '@main/utils'
|
||||
import { getAllFiles } from '@main/utils/file'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem } from '@types'
|
||||
@@ -113,20 +115,30 @@ class KnowledgeService {
|
||||
baseURL,
|
||||
dimensions
|
||||
}: KnowledgeBaseParams): Promise<RAGApplication> => {
|
||||
let ragApplication: RAGApplication
|
||||
const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
|
||||
try {
|
||||
ragApplication = await new RAGApplicationBuilder()
|
||||
.setModel('NO_MODEL')
|
||||
.setEmbeddingModel(embeddings)
|
||||
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
|
||||
.build()
|
||||
} catch (e) {
|
||||
Logger.error(e)
|
||||
throw new Error(`Failed to create RAGApplication: ${e}`)
|
||||
}
|
||||
|
||||
return ragApplication
|
||||
const batchSize = 10
|
||||
return new RAGApplicationBuilder()
|
||||
.setModel('NO_MODEL')
|
||||
.setEmbeddingModel(
|
||||
apiVersion
|
||||
? new AzureOpenAiEmbeddings({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIApiVersion: apiVersion,
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
configuration: { httpAgent: proxyManager.getProxyAgent() },
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
: new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
configuration: { baseURL, httpAgent: proxyManager.getProxyAgent() },
|
||||
dimensions,
|
||||
batchSize
|
||||
})
|
||||
)
|
||||
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
|
||||
.build()
|
||||
}
|
||||
|
||||
public create = async (_: Electron.IpcMainInvokeEvent, base: KnowledgeBaseParams): Promise<void> => {
|
||||
@@ -414,6 +426,7 @@ class KnowledgeService {
|
||||
}
|
||||
|
||||
public add = (_: Electron.IpcMainInvokeEvent, options: KnowledgeBaseAddItemOptions): Promise<LoaderReturn> => {
|
||||
proxyManager.setGlobalProxy()
|
||||
return new Promise((resolve) => {
|
||||
const { base, item, forceReload = false } = options
|
||||
const optionsNonNullableAttribute = { base, item, forceReload }
|
||||
|
||||
1351
src/main/services/NodeAppService.ts
Normal file
1351
src/main/services/NodeAppService.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,25 @@
|
||||
import { ProxyConfig as _ProxyConfig, session } from 'electron'
|
||||
import { socksDispatcher } from 'fetch-socks'
|
||||
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
type ProxyMode = 'system' | 'custom' | 'none'
|
||||
|
||||
export interface ProxyConfig {
|
||||
mode: ProxyMode
|
||||
url?: string
|
||||
url?: string | null
|
||||
}
|
||||
|
||||
export class ProxyManager {
|
||||
private config: ProxyConfig
|
||||
private proxyAgent: GeneralProxyAgent | null = null
|
||||
private proxyAgent: HttpsProxyAgent | null = null
|
||||
private proxyUrl: string | null = null
|
||||
private systemProxyInterval: NodeJS.Timeout | null = null
|
||||
|
||||
constructor() {
|
||||
this.config = {
|
||||
mode: 'none'
|
||||
mode: 'none',
|
||||
url: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ export class ProxyManager {
|
||||
if (this.config.mode === 'system') {
|
||||
await this.setSystemProxy()
|
||||
this.monitorSystemProxy()
|
||||
} else if (this.config.mode === 'custom') {
|
||||
} else if (this.config.mode == 'custom') {
|
||||
await this.setCustomProxy()
|
||||
} else {
|
||||
await this.clearProxy()
|
||||
@@ -71,13 +73,11 @@ export class ProxyManager {
|
||||
private async setSystemProxy(): Promise<void> {
|
||||
try {
|
||||
await this.setSessionsProxy({ mode: 'system' })
|
||||
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
|
||||
const [protocol, address] = proxyString.split(';')[0].split(' ')
|
||||
const url = protocol === 'PROXY' ? `http://${address}` : null
|
||||
if (url && url !== this.config.url) {
|
||||
this.config.url = url.toLowerCase()
|
||||
this.setEnvironment(this.config.url)
|
||||
this.proxyAgent = new GeneralProxyAgent()
|
||||
const url = await this.resolveSystemProxy()
|
||||
if (url && url !== this.proxyUrl) {
|
||||
this.proxyUrl = url.toLowerCase()
|
||||
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
|
||||
this.setEnvironment(this.proxyUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set system proxy:', error)
|
||||
@@ -88,9 +88,10 @@ export class ProxyManager {
|
||||
private async setCustomProxy(): Promise<void> {
|
||||
try {
|
||||
if (this.config.url) {
|
||||
this.setEnvironment(this.config.url)
|
||||
this.proxyAgent = new GeneralProxyAgent()
|
||||
await this.setSessionsProxy({ proxyRules: this.config.url })
|
||||
this.proxyUrl = this.config.url.toLowerCase()
|
||||
this.proxyAgent = new HttpsProxyAgent(this.proxyUrl)
|
||||
this.setEnvironment(this.proxyUrl)
|
||||
await this.setSessionsProxy({ proxyRules: this.proxyUrl })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set custom proxy:', error)
|
||||
@@ -98,31 +99,45 @@ export class ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
private clearEnvironment(): void {
|
||||
private async clearProxy(): Promise<void> {
|
||||
delete process.env.HTTP_PROXY
|
||||
delete process.env.HTTPS_PROXY
|
||||
delete process.env.grpc_proxy
|
||||
delete process.env.http_proxy
|
||||
delete process.env.https_proxy
|
||||
}
|
||||
|
||||
private async clearProxy(): Promise<void> {
|
||||
this.clearEnvironment()
|
||||
await this.setSessionsProxy({ mode: 'direct' })
|
||||
await this.setSessionsProxy({})
|
||||
this.config = { mode: 'none' }
|
||||
this.proxyAgent = null
|
||||
this.proxyUrl = null
|
||||
}
|
||||
|
||||
getProxyAgent(): GeneralProxyAgent | null {
|
||||
private async resolveSystemProxy(): Promise<string | null> {
|
||||
try {
|
||||
return await this.resolveElectronProxy()
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve system proxy:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveElectronProxy(): Promise<string | null> {
|
||||
try {
|
||||
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
|
||||
const [protocol, address] = proxyString.split(';')[0].split(' ')
|
||||
return protocol === 'PROXY' ? `http://${address}` : null
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve electron proxy:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
getProxyAgent(): HttpsProxyAgent | null {
|
||||
return this.proxyAgent
|
||||
}
|
||||
|
||||
getProxyUrl(): string {
|
||||
return this.config.url || ''
|
||||
getProxyUrl(): string | null {
|
||||
return this.proxyUrl
|
||||
}
|
||||
|
||||
setGlobalProxy() {
|
||||
const proxyUrl = this.config.url
|
||||
const proxyUrl = this.proxyUrl
|
||||
if (proxyUrl) {
|
||||
const [protocol, address] = proxyUrl.split('://')
|
||||
const [host, port] = address.split(':')
|
||||
|
||||
@@ -115,20 +115,7 @@ const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutForm
|
||||
}
|
||||
|
||||
export function registerShortcuts(window: BrowserWindow) {
|
||||
window.once('ready-to-show', () => {
|
||||
if (configManager.getLaunchToTray()) {
|
||||
registerOnlyUniversalShortcuts()
|
||||
}
|
||||
})
|
||||
|
||||
//only for clearer code
|
||||
const registerOnlyUniversalShortcuts = () => {
|
||||
register(true)
|
||||
}
|
||||
|
||||
//onlyUniversalShortcuts is used to register shortcuts that are not window specific, like show_app & mini_window
|
||||
//onlyUniversalShortcuts is needed when we launch to tray
|
||||
const register = (onlyUniversalShortcuts: boolean = false) => {
|
||||
const register = () => {
|
||||
if (window.isDestroyed()) return
|
||||
|
||||
const shortcuts = configManager.getShortcuts()
|
||||
@@ -145,11 +132,6 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
// only register universal shortcuts when needed
|
||||
if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
const handler = getShortcutHandler(shortcut)
|
||||
if (!handler) {
|
||||
return
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { proxyManager } from '@main/services/ProxyManager'
|
||||
import { WebDavConfig } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
import { HttpProxyAgent } from 'http-proxy-agent'
|
||||
import Stream from 'stream'
|
||||
import { BufferLike, createClient, GetFileContentsOptions, PutFileContentsOptions, WebDAVClient } from 'webdav'
|
||||
export default class WebDav {
|
||||
@@ -8,12 +10,15 @@ export default class WebDav {
|
||||
|
||||
constructor(params: WebDavConfig) {
|
||||
this.webdavPath = params.webdavPath
|
||||
const url = proxyManager.getProxyUrl()
|
||||
|
||||
this.instance = createClient(params.webdavHost, {
|
||||
username: params.webdavUser,
|
||||
password: params.webdavPass,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity
|
||||
maxContentLength: Infinity,
|
||||
httpAgent: url ? new HttpProxyAgent(url) : undefined,
|
||||
httpsAgent: proxyManager.getProxyAgent()
|
||||
})
|
||||
|
||||
this.putFileContents = this.putFileContents.bind(this)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isDev, isLinux, isMac, isWin } from '@main/constant'
|
||||
import { isDev, isLinux, isWin } from '@main/constant'
|
||||
import { getFilesDir } from '@main/utils/file'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
@@ -39,6 +39,8 @@ export class WindowService {
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isLinux = process.platform === 'linux'
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -144,12 +146,7 @@ export class WindowService {
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.webContents.setZoomFactor(configManager.getZoomFactor())
|
||||
|
||||
// show window only when laucn to tray not set
|
||||
const isLaunchToTray = configManager.getLaunchToTray()
|
||||
if (!isLaunchToTray) {
|
||||
mainWindow.show()
|
||||
}
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
// 处理全屏相关事件
|
||||
@@ -258,20 +255,12 @@ export class WindowService {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// 托盘及关闭行为设置
|
||||
const isShowTray = configManager.getTray()
|
||||
const isTrayOnClose = configManager.getTrayOnClose()
|
||||
|
||||
// 没有开启托盘,或者开启了托盘,但设置了直接关闭,应执行直接退出
|
||||
if (!isShowTray || (isShowTray && !isTrayOnClose)) {
|
||||
// 如果是Windows或Linux,直接退出
|
||||
// mac按照系统默认行为,不退出
|
||||
if (isWin || isLinux) {
|
||||
return app.quit()
|
||||
}
|
||||
// 没有开启托盘,且是Windows或Linux系统,直接退出
|
||||
const notInTray = !configManager.getTray()
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
//上述逻辑以下,是“开启托盘+设置关闭时最小化到托盘”的情况
|
||||
// 如果是Windows或Linux,且处于全屏状态,则退出应用
|
||||
if (this.wasFullScreen) {
|
||||
if (isWin || isLinux) {
|
||||
@@ -282,13 +271,8 @@ export class WindowService {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
|
||||
if (isMac && isTrayOnClose) {
|
||||
app.dock?.hide() //for mac to hide to tray
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
@@ -317,9 +301,6 @@ export class WindowService {
|
||||
this.mainWindow = this.createMainWindow()
|
||||
this.mainWindow.focus()
|
||||
}
|
||||
|
||||
//for mac users, when window is shown, should show dock icon (dock may be set to hide when launch)
|
||||
app.dock?.show()
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
@@ -329,6 +310,9 @@ export class WindowService {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.hide()
|
||||
}
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.hide()
|
||||
}
|
||||
@@ -343,6 +327,8 @@ export class WindowService {
|
||||
return
|
||||
}
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.miniWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 520,
|
||||
@@ -417,6 +403,7 @@ export class WindowService {
|
||||
}
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.selectionMenuWindow = new BrowserWindow({
|
||||
width: 280,
|
||||
|
||||
@@ -6,13 +6,13 @@ import path from 'path'
|
||||
|
||||
import { getResourcePath } from '.'
|
||||
|
||||
export function runInstallScript(scriptPath: string): Promise<void> {
|
||||
export function runInstallScript(scriptPath: string, env?: NodeJS.ProcessEnv): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
||||
log.info(`Running script at: ${installScriptPath}`)
|
||||
|
||||
const nodeProcess = spawn(process.execPath, [installScriptPath], {
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
|
||||
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', ...env }
|
||||
})
|
||||
|
||||
nodeProcess.stdout.on('data', (data) => {
|
||||
|
||||
30
src/preload/index.d.ts
vendored
30
src/preload/index.d.ts
vendored
@@ -23,10 +23,7 @@ declare global {
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
setLaunchOnBoot: (isActive: boolean) => void
|
||||
setLaunchToTray: (isActive: boolean) => void
|
||||
setTray: (isActive: boolean) => void
|
||||
setTrayOnClose: (isActive: boolean) => void
|
||||
restartTray: () => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||
@@ -166,10 +163,37 @@ declare global {
|
||||
logout: () => Promise<void>
|
||||
getUser: (token: string) => Promise<{ login: string; avatar: string }>
|
||||
}
|
||||
nodeapp: {
|
||||
list: () => Promise<any[]>
|
||||
add: (app: any) => Promise<any>
|
||||
install: (appId: string) => Promise<any | null>
|
||||
update: (appId: string) => Promise<any | null>
|
||||
start: (appId: string) => Promise<{ port: number; url: string } | null>
|
||||
stop: (appId: string) => Promise<boolean>
|
||||
uninstall: (appId: string) => Promise<boolean>
|
||||
deployZip: (zipPath: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => Promise<{ port: number; url: string } | null>
|
||||
deployGit: (repoUrl: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => Promise<{ port: number; url: string } | null>
|
||||
checkNode: () => Promise<boolean>
|
||||
installNode: () => Promise<boolean>
|
||||
onUpdated: (callback: (apps: any[]) => void) => () => void
|
||||
}
|
||||
isBinaryExist: (name: string) => Promise<boolean>
|
||||
getBinaryPath: (name: string) => Promise<string>
|
||||
installUVBinary: () => Promise<void>
|
||||
installBunBinary: () => Promise<void>
|
||||
run: (command: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||
showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
||||
setLaunchOnBoot: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-on-boot', isActive),
|
||||
setLaunchToTray: (isActive: boolean) => ipcRenderer.invoke('app:set-launch-to-tray', isActive),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
||||
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke('app:set-tray-on-close', isActive),
|
||||
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||
@@ -36,6 +33,38 @@ const api = {
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:restoreFromWebdav', webdavConfig),
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => ipcRenderer.invoke('backup:listWebdavFiles', webdavConfig)
|
||||
},
|
||||
nodeapp: {
|
||||
list: () => ipcRenderer.invoke('nodeapp:list'),
|
||||
add: (app: any) => ipcRenderer.invoke('nodeapp:add', app),
|
||||
install: (appId: string) => ipcRenderer.invoke('nodeapp:install', appId),
|
||||
update: (appId: string) => ipcRenderer.invoke('nodeapp:update', appId),
|
||||
start: (appId: string) => ipcRenderer.invoke('nodeapp:start', appId),
|
||||
stop: (appId: string) => ipcRenderer.invoke('nodeapp:stop', appId),
|
||||
uninstall: (appId: string) => ipcRenderer.invoke('nodeapp:uninstall', appId),
|
||||
deployZip: (zipPath: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => ipcRenderer.invoke('nodeapp:deploy-zip', zipPath, options),
|
||||
deployGit: (repoUrl: string, options?: {
|
||||
name?: string;
|
||||
port?: number;
|
||||
startCommand?: string;
|
||||
installCommand?: string;
|
||||
buildCommand?: string;
|
||||
}) => ipcRenderer.invoke('nodeapp:deploy-git', repoUrl, options),
|
||||
checkNode: () => ipcRenderer.invoke('nodeapp:check-node'),
|
||||
installNode: () => ipcRenderer.invoke('nodeapp:install-node'),
|
||||
onUpdated: (callback: (apps: any[]) => void) => {
|
||||
const eventListener = (_: any, apps: any[]) => callback(apps)
|
||||
ipcRenderer.on('nodeapp:updated', eventListener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('nodeapp:updated', eventListener)
|
||||
}
|
||||
}
|
||||
},
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||
@@ -127,7 +156,7 @@ const api = {
|
||||
cleanup: () => ipcRenderer.invoke('mcp:cleanup')
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url)
|
||||
},
|
||||
copilot: {
|
||||
getAuthMessage: (headers?: Record<string, string>) => ipcRenderer.invoke('copilot:get-auth-message', headers),
|
||||
@@ -143,7 +172,8 @@ const api = {
|
||||
isBinaryExist: (name: string) => ipcRenderer.invoke('app:is-binary-exist', name),
|
||||
getBinaryPath: (name: string) => ipcRenderer.invoke('app:get-binary-path', name),
|
||||
installUVBinary: () => ipcRenderer.invoke('app:install-uv-binary'),
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary')
|
||||
installBunBinary: () => ipcRenderer.invoke('app:install-bun-binary'),
|
||||
run: (command: string) => ipcRenderer.invoke('app:run-command', command)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -17,6 +17,7 @@ import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import NodeAppsPage from './pages/nodeapps/NodeAppsPage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
@@ -41,6 +42,7 @@ function App(): JSX.Element {
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/nodeapps" element={<NodeAppsPage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -8,7 +8,7 @@ const ReasoningIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tooltip title={t('models.type.reasoning')} placement="top">
|
||||
<Tooltip title={t('models.reasoning')} placement="top">
|
||||
<Icon className="iconfont icon-thinking" {...(props as any)} />
|
||||
</Tooltip>
|
||||
</Container>
|
||||
|
||||
@@ -9,7 +9,7 @@ const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>,
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tooltip title={t('models.type.vision')} placement="top">
|
||||
<Tooltip title={t('models.vision')} placement="top">
|
||||
<Icon {...(props as any)} />
|
||||
</Tooltip>
|
||||
</Container>
|
||||
|
||||
@@ -9,7 +9,7 @@ const WebSearchIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tooltip title={t('models.type.websearch')} placement="top">
|
||||
<Tooltip title={t('models.websearch')} placement="top">
|
||||
<Icon {...(props as any)} />
|
||||
</Tooltip>
|
||||
</Container>
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
isReasoningModel,
|
||||
isRerankModel,
|
||||
isVisionModel,
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
@@ -33,9 +32,8 @@ const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true, showReasoning =
|
||||
{isWebSearchModel(model) && <WebSearchIcon />}
|
||||
{showReasoning && isReasoningModel(model) && <ReasoningIcon />}
|
||||
{showToolsCalling && isFunctionCallingModel(model) && <ToolsCallingIcon />}
|
||||
{isEmbeddingModel(model) && <Tag color="orange">{t('models.type.embedding')}</Tag>}
|
||||
{showFree && isFreeModel(model) && <Tag color="green">{t('models.type.free')}</Tag>}
|
||||
{isRerankModel(model) && <Tag color="geekblue">{t('models.type.rerank')}</Tag>}
|
||||
{isEmbeddingModel(model) && <Tag color="orange">{t('models.embedding')}</Tag>}
|
||||
{showFree && isFreeModel(model) && <Tag color="green">{t('models.free')}</Tag>}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
import { Form, Input, Modal, Select } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface ObsidianExportDialogProps {
|
||||
title: string
|
||||
markdown: string
|
||||
open: boolean // 使用 open 属性替代 visible
|
||||
onClose: (success: boolean) => void
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
title,
|
||||
markdown,
|
||||
obsidianTags,
|
||||
processingMethod,
|
||||
open,
|
||||
onClose
|
||||
}) => {
|
||||
const [state, setState] = useState({
|
||||
title: title,
|
||||
tags: obsidianTags || '',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
source: 'Cherry Studio',
|
||||
processingMethod: processingMethod
|
||||
})
|
||||
|
||||
const handleOk = async () => {
|
||||
//构建content 并复制到粘贴板
|
||||
let content = ''
|
||||
if (state.processingMethod !== '3') {
|
||||
content = `\n---\n${markdown}`
|
||||
} else {
|
||||
content = `---
|
||||
\ntitle: ${state.title}
|
||||
\ncreated: ${state.createdAt}
|
||||
\nsource: ${state.source}
|
||||
\ntags: ${state.tags}
|
||||
\n---\n${markdown}`
|
||||
}
|
||||
if (content === '') {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||
}
|
||||
await navigator.clipboard.writeText(content)
|
||||
markdown = ''
|
||||
exportMarkdownToObsidian(state)
|
||||
onClose(true)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose(false)
|
||||
}
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||
open={open} // 使用 open 属性
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
closable
|
||||
maskClosable
|
||||
centered
|
||||
okButtonProps={{ type: 'primary' }}
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||
<Input
|
||||
value={state.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||
<Input
|
||||
value={state.tags}
|
||||
onChange={(e) => handleChange('tags', e.target.value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_tags_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_created')}>
|
||||
<Input
|
||||
value={state.createdAt}
|
||||
onChange={(e) => handleChange('createdAt', e.target.value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_created_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_source')}>
|
||||
<Input
|
||||
value={state.source}
|
||||
onChange={(e) => handleChange('source', e.target.value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
|
||||
<Select
|
||||
value={state.processingMethod}
|
||||
onChange={(value) => handleChange('processingMethod', value)}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_operate_placeholder')}
|
||||
allowClear>
|
||||
<Option value="1">{i18n.t('chat.topics.export.obsidian_operate_append')}</Option>
|
||||
<Option value="2">{i18n.t('chat.topics.export.obsidian_operate_prepend')}</Option>
|
||||
<Option value="3">{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ObsidianExportDialog
|
||||
228
src/renderer/src/components/ObsidianFolderSelector.tsx
Normal file
228
src/renderer/src/components/ObsidianFolderSelector.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { FileOutlined, FolderOutlined } from '@ant-design/icons'
|
||||
import { Spin, Switch, Tree } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
defaultPath: string
|
||||
obsidianUrl: string
|
||||
obsidianApiKey: string
|
||||
onPathChange: (path: string, isMdFile: boolean) => void
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
title: string
|
||||
key: string
|
||||
isLeaf: boolean
|
||||
isMdFile?: boolean
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
const ObsidianFolderSelector: FC<Props> = ({ defaultPath, obsidianUrl, obsidianApiKey, onPathChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(['/'])
|
||||
const [showMdFiles, setShowMdFiles] = useState<boolean>(false)
|
||||
// 当前选中的节点信息
|
||||
const [currentSelection, setCurrentSelection] = useState({
|
||||
path: defaultPath,
|
||||
isMdFile: false
|
||||
})
|
||||
// 使用key强制Tree组件重新渲染
|
||||
const [treeKey, setTreeKey] = useState<number>(0)
|
||||
|
||||
// 只初始化根节点,不立即加载内容
|
||||
useEffect(() => {
|
||||
initializeRootNode()
|
||||
}, [showMdFiles])
|
||||
|
||||
// 初始化根节点,但不自动加载子节点
|
||||
const initializeRootNode = () => {
|
||||
const rootNode: TreeNode = {
|
||||
title: '/',
|
||||
key: '/',
|
||||
isLeaf: false
|
||||
}
|
||||
|
||||
setTreeData([rootNode])
|
||||
}
|
||||
|
||||
// 异步加载子节点
|
||||
const loadData = async (node: any) => {
|
||||
if (node.isLeaf) return // 如果是叶子节点(md文件),不加载子节点
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
// 确保路径末尾有斜杠
|
||||
const path = node.key === '/' ? '' : node.key
|
||||
const requestPath = path.endsWith('/') ? path : `${path}/`
|
||||
|
||||
const response = await fetch(`${obsidianUrl}vault${requestPath}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${obsidianApiKey}`
|
||||
}
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || (!data?.files && data?.errorCode !== 40400)) {
|
||||
throw new Error('获取文件夹失败')
|
||||
}
|
||||
|
||||
const childNodes: TreeNode[] = (data.files || [])
|
||||
.filter((file: string) => file.endsWith('/') || (showMdFiles && file.endsWith('.md'))) // 根据开关状态决定是否显示md文件
|
||||
.map((file: string) => {
|
||||
// 修复路径问题,避免重复的斜杠
|
||||
const normalizedFile = file.replace('/', '')
|
||||
const isMdFile = file.endsWith('.md')
|
||||
const childPath = requestPath.endsWith('/')
|
||||
? `${requestPath}${normalizedFile}${isMdFile ? '' : '/'}`
|
||||
: `${requestPath}/${normalizedFile}${isMdFile ? '' : '/'}`
|
||||
|
||||
return {
|
||||
title: normalizedFile,
|
||||
key: childPath,
|
||||
isLeaf: isMdFile,
|
||||
isMdFile
|
||||
}
|
||||
})
|
||||
|
||||
// 更新节点的子节点
|
||||
setTreeData((origin) => {
|
||||
const loop = (data: TreeNode[], key: string, children: TreeNode[]): TreeNode[] => {
|
||||
return data.map((item) => {
|
||||
if (item.key === key) {
|
||||
return {
|
||||
...item,
|
||||
children
|
||||
}
|
||||
}
|
||||
if (item.children) {
|
||||
return {
|
||||
...item,
|
||||
children: loop(item.children, key, children)
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
return loop(origin, node.key, childNodes)
|
||||
})
|
||||
} catch (error) {
|
||||
window.message.error(t('chat.topics.export.obsidian_fetch_failed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理开关切换
|
||||
const handleSwitchChange = (checked: boolean) => {
|
||||
setShowMdFiles(checked)
|
||||
// 重置选择
|
||||
setCurrentSelection({
|
||||
path: defaultPath,
|
||||
isMdFile: false
|
||||
})
|
||||
onPathChange(defaultPath, false)
|
||||
|
||||
// 重置Tree状态并强制重新渲染
|
||||
setTreeData([])
|
||||
setExpandedKeys(['/'])
|
||||
|
||||
// 递增key值以强制Tree组件完全重新渲染
|
||||
setTreeKey((prev) => prev + 1)
|
||||
|
||||
// 延迟初始化根节点,让状态完全清除
|
||||
setTimeout(() => {
|
||||
initializeRootNode()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
// 自定义图标,为md文件和文件夹显示不同的图标
|
||||
const renderIcon = (props: any) => {
|
||||
const { data } = props
|
||||
if (data.isMdFile) {
|
||||
return <FileOutlined />
|
||||
}
|
||||
return <FolderOutlined />
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SwitchContainer>
|
||||
<span>{t('chat.topics.export.obsidian_show_md_files')}</span>
|
||||
<Switch checked={showMdFiles} onChange={handleSwitchChange} />
|
||||
</SwitchContainer>
|
||||
<Spin spinning={loading}>
|
||||
<TreeContainer>
|
||||
<Tree
|
||||
key={treeKey} // 使用key来强制重新渲染
|
||||
defaultSelectedKeys={[defaultPath]}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
treeData={treeData}
|
||||
loadData={loadData}
|
||||
onSelect={(selectedKeys, info) => {
|
||||
if (selectedKeys.length > 0) {
|
||||
const path = selectedKeys[0] as string
|
||||
const isMdFile = !!(info.node as any).isMdFile
|
||||
|
||||
setCurrentSelection({
|
||||
path,
|
||||
isMdFile
|
||||
})
|
||||
|
||||
onPathChange?.(path, isMdFile)
|
||||
}
|
||||
}}
|
||||
showLine
|
||||
showIcon
|
||||
icon={renderIcon}
|
||||
/>
|
||||
</TreeContainer>
|
||||
</Spin>
|
||||
<div>
|
||||
{currentSelection.path !== defaultPath && (
|
||||
<SelectedPath>
|
||||
{t('chat.topics.export.obsidian_selected_path')}: {currentSelection.path}
|
||||
</SelectedPath>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px;
|
||||
`
|
||||
|
||||
const TreeContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
height: 320px;
|
||||
`
|
||||
|
||||
const SwitchContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
`
|
||||
|
||||
const SelectedPath = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
margin-top: 5px;
|
||||
padding: 0 10px;
|
||||
word-break: break-all;
|
||||
`
|
||||
|
||||
export default ObsidianFolderSelector
|
||||
@@ -1,52 +1,68 @@
|
||||
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||
import ObsidianFolderSelector from '@renderer/components/ObsidianFolderSelector'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
|
||||
interface ObsidianExportOptions {
|
||||
title: string
|
||||
markdown: string
|
||||
processingMethod: string | '3' // 默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置Obsidian 笔记属性弹窗
|
||||
* @param options.title 标题
|
||||
* @param options.markdown markdown内容
|
||||
* @param options.processingMethod 处理方式
|
||||
* @returns
|
||||
*/
|
||||
// 用于显示 Obsidian 导出对话框
|
||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||
const obsidianValut = store.getState().settings.obsidianValut
|
||||
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||
const { title, markdown } = options
|
||||
const obsidianUrl = store.getState().settings.obsidianUrl
|
||||
const obsidianApiKey = store.getState().settings.obsidianApiKey
|
||||
|
||||
if (!obsidianValut || !obsidianFolder) {
|
||||
if (!obsidianUrl || !obsidianApiKey) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||
return false
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
const root = createRoot(div)
|
||||
try {
|
||||
// 创建一个状态变量来存储选择的路径
|
||||
let selectedPath = '/'
|
||||
let selectedIsMdFile = false
|
||||
|
||||
const handleClose = (success: boolean) => {
|
||||
root.unmount()
|
||||
document.body.removeChild(div)
|
||||
resolve(success)
|
||||
}
|
||||
const obsidianTags = store.getState().settings.obsidianTages
|
||||
root.render(
|
||||
<ObsidianExportDialog
|
||||
title={options.title}
|
||||
markdown={options.markdown}
|
||||
obsidianTags={obsidianTags}
|
||||
processingMethod={options.processingMethod}
|
||||
open={true}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)
|
||||
})
|
||||
// 显示文件夹选择对话框
|
||||
return new Promise<boolean>((resolve) => {
|
||||
window.modal.confirm({
|
||||
title: i18n.t('chat.topics.export.obsidian_select_folder'),
|
||||
content: (
|
||||
<ObsidianFolderSelector
|
||||
defaultPath={selectedPath}
|
||||
obsidianUrl={obsidianUrl}
|
||||
obsidianApiKey={obsidianApiKey}
|
||||
onPathChange={(path, isMdFile) => {
|
||||
selectedPath = path
|
||||
selectedIsMdFile = isMdFile
|
||||
}}
|
||||
/>
|
||||
),
|
||||
width: 600,
|
||||
icon: null,
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
centered: true,
|
||||
okButtonProps: { type: 'primary' },
|
||||
okText: i18n.t('chat.topics.export.obsidian_select_folder.btn'),
|
||||
onOk: () => {
|
||||
// 如果选择的是md文件,则使用选择的文件名而不是传入的标题
|
||||
const fileName = selectedIsMdFile ? selectedPath.split('/').pop()?.replace('.md', '') : title
|
||||
|
||||
exportMarkdownToObsidian(fileName as string, markdown, selectedPath, selectedIsMdFile)
|
||||
resolve(true)
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_fetch_failed'))
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const ObsidianExportPopup = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getModelLogo, isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@@ -76,7 +76,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
// 根据输入的文本筛选模型
|
||||
const getFilteredModels = useCallback(
|
||||
(provider) => {
|
||||
let models = provider.models.filter((m) => !isEmbeddingModel(m) && !isRerankModel(m))
|
||||
let models = provider.models.filter((m) => !isEmbeddingModel(m))
|
||||
|
||||
if (searchText.trim()) {
|
||||
const keywords = searchText.toLowerCase().split(/\s+/).filter(Boolean)
|
||||
|
||||
@@ -130,6 +130,7 @@ const MainMenus: FC = () => {
|
||||
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
|
||||
translate: <TranslationOutlined />,
|
||||
minapp: <i className="iconfont icon-appstore" />,
|
||||
nodeapps: <i className="iconfont icon-code" />,
|
||||
knowledge: <FileSearchOutlined />,
|
||||
files: <FolderOutlined />
|
||||
}
|
||||
@@ -140,6 +141,7 @@ const MainMenus: FC = () => {
|
||||
paintings: '/paintings',
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
nodeapps: '/nodeapps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files'
|
||||
}
|
||||
|
||||
@@ -242,58 +242,6 @@ export const EMBEDDING_MODELS = [
|
||||
{
|
||||
id: 'mistral-embed',
|
||||
max_context: 8000
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-large',
|
||||
max_context: 1024
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-large',
|
||||
max_context: 256
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-large',
|
||||
max_context: 512
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-large',
|
||||
max_context: 2048
|
||||
},
|
||||
{
|
||||
id: 'voyage-3',
|
||||
max_context: 1024
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-lite',
|
||||
max_context: 512
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-3',
|
||||
max_context: 1024
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-3',
|
||||
max_context: 256
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-3',
|
||||
max_context: 512
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-3',
|
||||
max_context: 2048
|
||||
},
|
||||
{
|
||||
id: 'voyage-finance-2',
|
||||
max_context: 1024
|
||||
},
|
||||
{
|
||||
id: 'voyage-law-2',
|
||||
max_context: 1024
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-2',
|
||||
max_context: 1536
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@ import UpstageModelLogo from '@renderer/assets/images/models/upstage.png'
|
||||
import UpstageModelLogoDark from '@renderer/assets/images/models/upstage_dark.png'
|
||||
import ViduModelLogo from '@renderer/assets/images/models/vidu.png'
|
||||
import ViduModelLogoDark from '@renderer/assets/images/models/vidu_dark.png'
|
||||
import VoyageModelLogo from '@renderer/assets/images/models/voyageai.png'
|
||||
import WenxinModelLogo from '@renderer/assets/images/models/wenxin.png'
|
||||
import WenxinModelLogoDark from '@renderer/assets/images/models/wenxin_dark.png'
|
||||
import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
|
||||
@@ -173,11 +172,10 @@ export const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-|dall|cogview|
|
||||
|
||||
// Reasoning models
|
||||
export const REASONING_REGEX =
|
||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*)$/i
|
||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*)$/i
|
||||
|
||||
// Embedding models
|
||||
export const EMBEDDING_REGEX =
|
||||
/(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings|voyage-)/i
|
||||
export const EMBEDDING_REGEX = /(?:^text-|embed|bge-|e5-|LLM2Vec|retrieval|uae-|gte-|jina-clip|jina-embeddings)/i
|
||||
|
||||
// Rerank models
|
||||
export const RERANKING_REGEX = /(?:rerank|re-rank|re-ranker|re-ranking|retrieval|retriever)/i
|
||||
@@ -329,8 +327,7 @@ export function getModelLogo(modelId: string) {
|
||||
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
|
||||
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
|
||||
'bge-': BgeModelLogo,
|
||||
'voyage-': VoyageModelLogo
|
||||
'bge-': BgeModelLogo
|
||||
}
|
||||
|
||||
for (const key in logoMap) {
|
||||
@@ -1105,12 +1102,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'GLM 4V',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v-flash',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4V-Flash',
|
||||
group: 'GLM-4v'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v-plus',
|
||||
provider: 'zhipu',
|
||||
@@ -1804,63 +1795,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'DeepSeek'
|
||||
}
|
||||
],
|
||||
gpustack: [],
|
||||
voyageai: [
|
||||
{
|
||||
id: 'voyage-3-large',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-3-large',
|
||||
group: 'Voyage Embeddings V3'
|
||||
},
|
||||
{
|
||||
id: 'voyage-3',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-3',
|
||||
group: 'Voyage Embeddings V3'
|
||||
},
|
||||
{
|
||||
id: 'voyage-3-lite',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-3-lite',
|
||||
group: 'Voyage Embeddings V3'
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-3',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-code-3',
|
||||
group: 'Voyage Embeddings V3'
|
||||
},
|
||||
{
|
||||
id: 'voyage-finance-3',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-finance-3',
|
||||
group: 'Voyage Embeddings V2'
|
||||
},
|
||||
{
|
||||
id: 'voyage-law-2',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-law-2',
|
||||
group: 'Voyage Embeddings V2'
|
||||
},
|
||||
{
|
||||
id: 'voyage-code-2',
|
||||
provider: 'voyageai',
|
||||
name: 'voyage-code-2',
|
||||
group: 'Voyage Embeddings V2'
|
||||
},
|
||||
{
|
||||
id: 'rerank-2',
|
||||
provider: 'voyageai',
|
||||
name: 'rerank-2',
|
||||
group: 'Voyage Rerank V2'
|
||||
},
|
||||
{
|
||||
id: 'rerank-2-lite',
|
||||
provider: 'voyageai',
|
||||
name: 'rerank-2-lite',
|
||||
group: 'Voyage Rerank V2'
|
||||
}
|
||||
]
|
||||
gpustack: []
|
||||
}
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS = [
|
||||
@@ -1937,8 +1872,6 @@ export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
|
||||
'stabilityai/stable-diffusion-xl-base-1.0'
|
||||
]
|
||||
|
||||
export const GENERATE_IMAGE_MODELS = ['gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-exp']
|
||||
|
||||
export function isTextToImageModel(model: Model): boolean {
|
||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||
}
|
||||
@@ -1956,15 +1889,14 @@ export function isEmbeddingModel(model: Model): boolean {
|
||||
return EMBEDDING_REGEX.test(model.name)
|
||||
}
|
||||
|
||||
if (isRerankModel(model)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return EMBEDDING_REGEX.test(model.id) || model.type?.includes('embedding') || false
|
||||
}
|
||||
|
||||
export function isRerankModel(model: Model): boolean {
|
||||
return model ? RERANKING_REGEX.test(model.id) || false : false
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
return RERANKING_REGEX.test(model.id) || false
|
||||
}
|
||||
|
||||
export function isVisionModel(model: Model): boolean {
|
||||
@@ -2070,28 +2002,6 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
export function isGenerateImageModel(model: Model): boolean {
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
const provider = getProviderByModel(model)
|
||||
|
||||
if (!provider) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isEmbedding = isEmbeddingModel(model)
|
||||
|
||||
if (isEmbedding) {
|
||||
return false
|
||||
}
|
||||
if (GENERATE_IMAGE_MODELS.includes(model.id)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function getOpenAIWebSearchParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||
if (isWebSearchModel(model)) {
|
||||
if (assistant.enableWebSearch) {
|
||||
|
||||
@@ -93,7 +93,7 @@ export const REFERENCE_PROMPT = `Please answer the question based on the referen
|
||||
Please respond in the same language as the user's question.
|
||||
`
|
||||
|
||||
export const FOOTNOTE_PROMPT = `Please answer the question based on the reference materials and use footnote format to cite your sources. Please ignore irrelevant reference materials. If the reference material is not relevant to the question, please answer the question based on your knowledge. The answer should be clearly structured and complete.
|
||||
export const FOOTNOTE_PROMPT = `Please answer the question based on the reference materials and use footnote format to cite your sources. Please ignore irrelevant reference materials.
|
||||
|
||||
## Footnote Format:
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
|
||||
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
|
||||
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
|
||||
import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png'
|
||||
import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png'
|
||||
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
|
||||
import ZeroOneProviderLogo from '@renderer/assets/images/providers/zero-one.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
@@ -87,16 +86,13 @@ const PROVIDER_LOGO_MAP = {
|
||||
o3: O3ProviderLogo,
|
||||
'tencent-cloud-ti': TencentCloudProviderLogo,
|
||||
gpustack: GPUStackProviderLogo,
|
||||
alayanew: AlayaNewProviderLogo,
|
||||
voyageai: VoyageAIProviderLogo
|
||||
alayanew: AlayaNewProviderLogo
|
||||
} as const
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
return PROVIDER_LOGO_MAP[providerId as keyof typeof PROVIDER_LOGO_MAP]
|
||||
}
|
||||
|
||||
export const SUPPORTED_REANK_PROVIDERS = ['silicon', 'jina', 'voyageai']
|
||||
|
||||
export const PROVIDER_CONFIG = {
|
||||
openai: {
|
||||
api: {
|
||||
@@ -562,16 +558,5 @@ export const PROVIDER_CONFIG = {
|
||||
docs: 'https://docs.gpustack.ai/latest/',
|
||||
models: 'https://docs.gpustack.ai/latest/overview/#supported-models'
|
||||
}
|
||||
},
|
||||
voyageai: {
|
||||
api: {
|
||||
url: 'https://api.voyageai.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://www.voyageai.com/',
|
||||
apiKey: 'https://dashboard.voyageai.com/organization/api-keys',
|
||||
docs: 'https://docs.voyageai.com/docs',
|
||||
models: 'https://docs.voyageai.com/docs'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { LanguageVarious } from '@renderer/types'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
import elGR from 'antd/locale/el_GR'
|
||||
import enUS from 'antd/locale/en_US'
|
||||
import esES from 'antd/locale/es_ES'
|
||||
import frFR from 'antd/locale/fr_FR'
|
||||
import jaJP from 'antd/locale/ja_JP'
|
||||
import ptPT from 'antd/locale/pt_PT'
|
||||
import ruRU from 'antd/locale/ru_RU'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import zhTW from 'antd/locale/zh_TW'
|
||||
@@ -57,14 +53,7 @@ function getAntdLocale(language: LanguageVarious) {
|
||||
return ruRU
|
||||
case 'ja-JP':
|
||||
return jaJP
|
||||
case 'el-GR':
|
||||
return elGR
|
||||
case 'es-ES':
|
||||
return esES
|
||||
case 'fr-FR':
|
||||
return frFR
|
||||
case 'pt-PT':
|
||||
return ptPT
|
||||
|
||||
default:
|
||||
return zhCN
|
||||
}
|
||||
|
||||
2
src/renderer/src/env.d.ts
vendored
2
src/renderer/src/env.d.ts
vendored
@@ -20,5 +20,7 @@ declare global {
|
||||
keyv: KeyvStorage
|
||||
mermaid: any
|
||||
store: any
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<{ data: string; success: boolean }>
|
||||
}
|
||||
}
|
||||
|
||||
88
src/renderer/src/hooks/useNodeApps.ts
Normal file
88
src/renderer/src/hooks/useNodeApps.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export function useNodeApps() {
|
||||
const [apps, setApps] = useState<NodeAppType[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Load apps
|
||||
const loadApps = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await window.api.nodeapp.list()
|
||||
setApps(result || [])
|
||||
} catch (error) {
|
||||
console.error('Error loading node apps:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Add app
|
||||
const addApp = useCallback(async (app: NodeAppType) => {
|
||||
const result = await window.api.nodeapp.add(app)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Install app
|
||||
const installApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.install(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Update app
|
||||
const updateApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.update(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Start app
|
||||
const startApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.start(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Stop app
|
||||
const stopApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.stop(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Uninstall app
|
||||
const uninstallApp = useCallback(async (appId: string) => {
|
||||
const result = await window.api.nodeapp.uninstall(appId)
|
||||
await loadApps()
|
||||
return result
|
||||
}, [loadApps])
|
||||
|
||||
// Initialize
|
||||
useEffect(() => {
|
||||
loadApps()
|
||||
|
||||
// Subscribe to app updates
|
||||
const unsubscribe = window.api.nodeapp.onUpdated((updatedApps) => {
|
||||
setApps(updatedApps || [])
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
}, [loadApps])
|
||||
|
||||
return {
|
||||
apps,
|
||||
loading,
|
||||
addApp,
|
||||
installApp,
|
||||
updateApp,
|
||||
startApp,
|
||||
stopApp,
|
||||
uninstallApp,
|
||||
refresh: loadApps
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
SendMessageShortcut,
|
||||
setLaunchOnBoot,
|
||||
setLaunchToTray,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setShowAssistantIcon,
|
||||
setSidebarIcons,
|
||||
@@ -10,8 +8,7 @@ import {
|
||||
setTheme,
|
||||
SettingsState,
|
||||
setTopicPosition,
|
||||
setTray as _setTray,
|
||||
setTrayOnClose,
|
||||
setTray,
|
||||
setWindowStyle
|
||||
} from '@renderer/store/settings'
|
||||
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
@@ -25,30 +22,10 @@ export function useSettings() {
|
||||
setSendMessageShortcut(shortcut: SendMessageShortcut) {
|
||||
dispatch(_setSendMessageShortcut(shortcut))
|
||||
},
|
||||
|
||||
setLaunch(isLaunchOnBoot: boolean | undefined, isLaunchToTray: boolean | undefined = undefined) {
|
||||
if (isLaunchOnBoot !== undefined) {
|
||||
dispatch(setLaunchOnBoot(isLaunchOnBoot))
|
||||
window.api.setLaunchOnBoot(isLaunchOnBoot)
|
||||
}
|
||||
|
||||
if (isLaunchToTray !== undefined) {
|
||||
dispatch(setLaunchToTray(isLaunchToTray))
|
||||
window.api.setLaunchToTray(isLaunchToTray)
|
||||
}
|
||||
setTray(isActive: boolean) {
|
||||
dispatch(setTray(isActive))
|
||||
window.api.setTray(isActive)
|
||||
},
|
||||
|
||||
setTray(isShowTray: boolean | undefined, isTrayOnClose: boolean | undefined = undefined) {
|
||||
if (isShowTray !== undefined) {
|
||||
dispatch(_setTray(isShowTray))
|
||||
window.api.setTray(isShowTray)
|
||||
}
|
||||
if (isTrayOnClose !== undefined) {
|
||||
dispatch(setTrayOnClose(isTrayOnClose))
|
||||
window.api.setTrayOnClose(isTrayOnClose)
|
||||
}
|
||||
},
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
},
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
// Original translation
|
||||
import enUS from './locales/en-us.json'
|
||||
import jaJP from './locales/ja-jp.json'
|
||||
import ruRU from './locales/ru-ru.json'
|
||||
import zhCN from './locales/zh-cn.json'
|
||||
import zhTW from './locales/zh-tw.json'
|
||||
// Machine translation
|
||||
import elGR from './translate/el-gr.json'
|
||||
import esES from './translate/es-es.json'
|
||||
import frFR from './translate/fr-fr.json'
|
||||
import ptPT from './translate/pt-pt.json'
|
||||
|
||||
const resources = {
|
||||
'el-GR': elGR,
|
||||
'en-US': enUS,
|
||||
'es-ES': esES,
|
||||
'fr-FR': frFR,
|
||||
'ja-JP': jaJP,
|
||||
'pt-PT': ptPT,
|
||||
'ru-RU': ruRU,
|
||||
'zh-CN': zhCN,
|
||||
'zh-TW': zhTW
|
||||
'zh-TW': zhTW,
|
||||
'ja-JP': jaJP,
|
||||
'ru-RU': ruRU
|
||||
}
|
||||
|
||||
export const getLanguage = () => {
|
||||
|
||||
@@ -96,16 +96,6 @@
|
||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
"default.name": "Default Assistant",
|
||||
"default.topic.name": "Default Topic",
|
||||
"history": {
|
||||
"assistant_node": "Assistant",
|
||||
"click_to_navigate": "Click to navigate to the message",
|
||||
"coming_soon": "Chat workflow diagram coming soon",
|
||||
"no_messages": "No Messages Found",
|
||||
"start_conversation": "Start a conversation to see the chat flow diagram",
|
||||
"title": "Chat History",
|
||||
"user_node": "User",
|
||||
"view_full_content": "View Full Content"
|
||||
},
|
||||
"input.auto_resize": "Auto resize height",
|
||||
"input.clear": "Clear {{Command}}",
|
||||
"input.clear.content": "Do you want to clear all messages of the current topic?",
|
||||
@@ -115,8 +105,6 @@
|
||||
"input.estimated_tokens.tip": "Estimated tokens",
|
||||
"input.expand": "Expand",
|
||||
"input.file_not_supported": "Model does not support this file type",
|
||||
"input.generate_image": "Generate image",
|
||||
"input.generate_image_not_supported": "The model does not support generating images.",
|
||||
"input.knowledge_base": "Knowledge Base",
|
||||
"input.new.context": "Clear Context {{Command}}",
|
||||
"input.new_topic": "New Topic {{Command}}",
|
||||
@@ -138,13 +126,6 @@
|
||||
"message.quote": "Quote",
|
||||
"message.regenerate.model": "Switch Model",
|
||||
"message.useful": "Helpful",
|
||||
"navigation": {
|
||||
"first": "Already at the first message",
|
||||
"history": "Chat History",
|
||||
"last": "Already at the last message",
|
||||
"next": "Next Message",
|
||||
"prev": "Previous Message"
|
||||
},
|
||||
"resend": "Resend",
|
||||
"save": "Save",
|
||||
"settings.code_collapsible": "Code block collapsible",
|
||||
@@ -176,42 +157,37 @@
|
||||
"topics.edit.placeholder": "Enter new name",
|
||||
"topics.edit.title": "Edit Name",
|
||||
"topics.export.image": "Export as image",
|
||||
"topics.export.joplin": "Export to Joplin",
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.notion": "Export to Notion",
|
||||
"topics.export.obsidian": "Export to Obsidian",
|
||||
"topics.export.obsidian_atributes": "Configure Note Attributes",
|
||||
"topics.export.obsidian_btn": "Confirm",
|
||||
"topics.export.obsidian_created": "Creation Time",
|
||||
"topics.export.obsidian_created_placeholder": "Please select the creation time",
|
||||
"topics.export.obsidian_export_failed": "Export failed",
|
||||
"topics.export.obsidian_export_success": "Export success",
|
||||
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
||||
"topics.export.obsidian_operate": "Operation Method",
|
||||
"topics.export.obsidian_operate_append": "Append",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)",
|
||||
"topics.export.obsidian_operate_placeholder": "Please select the operation method",
|
||||
"topics.export.obsidian_operate_prepend": "Prepend",
|
||||
"topics.export.obsidian_source": "Source",
|
||||
"topics.export.obsidian_source_placeholder": "Please enter the source",
|
||||
"topics.export.obsidian_tags": "Tags",
|
||||
"topics.export.obsidian_tags_placeholder": "Please enter tags, separate multiple tags with commas",
|
||||
"topics.export.obsidian_title": "Title",
|
||||
"topics.export.obsidian_title_placeholder": "Please enter the title",
|
||||
"topics.export.obsidian_title_required": "The title cannot be empty",
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.word": "Export as Word",
|
||||
"topics.export.yuque": "Export to Yuque",
|
||||
"topics.export.obsidian": "Export to Obsidian",
|
||||
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
||||
"topics.export.obsidian_fetch_failed": "Failed to fetch Obsidian folder structure",
|
||||
"topics.export.obsidian_select_folder": "Select Obsidian folder",
|
||||
"topics.export.obsidian_select_folder.btn": "Confirm",
|
||||
"topics.export.obsidian_export_success": "Export success",
|
||||
"topics.export.obsidian_export_failed": "Export failed",
|
||||
"topics.export.obsidian_show_md_files": "Show MD Files",
|
||||
"topics.export.obsidian_selected_path": "Selected Path",
|
||||
"topics.export.joplin": "Export to Joplin",
|
||||
"topics.list": "Topic List",
|
||||
"topics.move_to": "Move to",
|
||||
"topics.new": "New Topic",
|
||||
"topics.pinned": "Pinned Topics",
|
||||
"topics.prompt": "Topic Prompts",
|
||||
"topics.prompt.edit.title": "Edit Topic Prompts",
|
||||
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
||||
"topics.title": "Topics",
|
||||
"topics.unpinned": "Unpinned Topics",
|
||||
"translate": "Translate"
|
||||
"topics.new": "New Topic",
|
||||
"translate": "Translate",
|
||||
"navigation": {
|
||||
"prev": "Previous Message",
|
||||
"next": "Next Message",
|
||||
"first": "Already at the first message",
|
||||
"last": "Already at the last message"
|
||||
}
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Collapse",
|
||||
@@ -221,7 +197,6 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"and": "and",
|
||||
"assistant": "Assistant",
|
||||
"avatar": "Avatar",
|
||||
@@ -230,8 +205,6 @@
|
||||
"chat": "Chat",
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"default": "Default",
|
||||
@@ -241,7 +214,6 @@
|
||||
"download": "Download",
|
||||
"duplicate": "Duplicate",
|
||||
"edit": "Edit",
|
||||
"expand": "Expand",
|
||||
"footnote": "Reference content",
|
||||
"footnotes": "References",
|
||||
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
|
||||
@@ -249,7 +221,6 @@
|
||||
"language": "Language",
|
||||
"model": "Model",
|
||||
"models": "Models",
|
||||
"more": "More",
|
||||
"name": "Name",
|
||||
"paste": "Paste",
|
||||
"prompt": "Prompt",
|
||||
@@ -262,7 +233,12 @@
|
||||
"select": "Select",
|
||||
"topics": "Topics",
|
||||
"warning": "Warning",
|
||||
"you": "You"
|
||||
"you": "You",
|
||||
"copied": "Copied",
|
||||
"confirm": "Confirm",
|
||||
"more": "More",
|
||||
"advanced_settings": "Advanced Settings",
|
||||
"expand": "Expand"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Docs"
|
||||
@@ -320,12 +296,6 @@
|
||||
"title": "Files",
|
||||
"type": "Type"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
"keep_alive_time.placeholder": "Minutes",
|
||||
"keep_alive_time.title": "Keep Alive Time",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Continue Chatting",
|
||||
"locate.message": "Locate the message",
|
||||
@@ -395,13 +365,13 @@
|
||||
"threshold_too_large_or_small": "Threshold cannot be greater than 1 or less than 0",
|
||||
"threshold_tooltip": "Used to evaluate the relevance between the user's question and the content in the knowledge base (0-1)",
|
||||
"title": "Knowledge Base",
|
||||
"topN": "Number of results returned",
|
||||
"topN__too_large_or_small": "The number of results returned cannot be greater than 100 or less than 1.",
|
||||
"topN_placeholder": "Not set",
|
||||
"topN_tooltip": "The number of matching results returned; the larger the value, the more matching results, but also the more tokens consumed.",
|
||||
"url_added": "URL added",
|
||||
"url_placeholder": "Enter URL, multiple URLs separated by Enter",
|
||||
"urls": "URLs"
|
||||
"urls": "URLs",
|
||||
"topN": "Number of results returned",
|
||||
"topN_placeholder": "Not set",
|
||||
"topN__too_large_or_small": "The number of results returned cannot be greater than 100 or less than 1.",
|
||||
"topN_tooltip": "The number of matching results returned; the larger the value, the more matching results, but also the more tokens consumed."
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Arabic",
|
||||
@@ -439,22 +409,22 @@
|
||||
"title": "Mermaid Diagram"
|
||||
},
|
||||
"message": {
|
||||
"attachments": {
|
||||
"pasted_text": "Pasted Text",
|
||||
"pasted_image": "Pasted Image"
|
||||
},
|
||||
"api.check.model.title": "Select the model to use for detection",
|
||||
"api.connection.failed": "Connection failed",
|
||||
"api.connection.success": "Connection successful",
|
||||
"assistant.added.content": "Assistant added successfully",
|
||||
"attachments": {
|
||||
"pasted_image": "Pasted Image",
|
||||
"pasted_text": "Pasted Text"
|
||||
},
|
||||
"backup.failed": "Backup failed",
|
||||
"backup.start.success": "Backup started",
|
||||
"backup.success": "Backup successful",
|
||||
"chat.completion.paused": "Chat completion paused",
|
||||
"citations": "References",
|
||||
"copied": "Copied!",
|
||||
"copy.failed": "Copy failed",
|
||||
"copy.success": "Copied!",
|
||||
"copy.failed": "Copy failed",
|
||||
"error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size",
|
||||
"error.dimension_too_large": "Content size is too large",
|
||||
"error.enter.api.host": "Please enter your API host first",
|
||||
@@ -467,20 +437,18 @@
|
||||
"error.invalid.enter.model": "Please select a model",
|
||||
"error.invalid.proxy.url": "Invalid proxy URL",
|
||||
"error.invalid.webdav": "Invalid WebDAV settings",
|
||||
"error.joplin.export": "Failed to export to Joplin. Please keep Joplin running and check connection status or configuration",
|
||||
"error.joplin.no_config": "Joplin Authorization Token or URL is not configured",
|
||||
"error.markdown.export.preconf": "Failed to export the Markdown file to the preconfigured path",
|
||||
"error.markdown.export.specified": "Failed to export the Markdown file",
|
||||
"error.notion.export": "Failed to export to Notion. Please check connection status and configuration according to documentation",
|
||||
"error.notion.no_api_key": "Notion ApiKey or Notion DatabaseID is not configured",
|
||||
"error.yuque.export": "Failed to export to Yuque. Please check connection status and configuration according to documentation",
|
||||
"error.yuque.no_config": "Yuque Token or Yuque Url is not configured",
|
||||
"error.joplin.no_config": "Joplin Authorization Token or URL is not configured",
|
||||
"error.joplin.export": "Failed to export to Joplin. Please keep Joplin running and check connection status or configuration",
|
||||
"group.delete.content": "Deleting a group message will delete the user's question and all assistant's answers",
|
||||
"group.delete.title": "Delete Group Message",
|
||||
"ignore.knowledge.base": "Web search mode is enabled, ignore knowledge base",
|
||||
"info.notion.block_reach_limit": "Dialogue too long, exporting to Notion in pages",
|
||||
"loading.notion.exporting_progress": "Exporting to Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "Preparing to export to Notion...",
|
||||
"mention.title": "Switch model answer",
|
||||
"message.code_style": "Code style",
|
||||
"message.delete.content": "Are you sure you want to delete this message?",
|
||||
@@ -495,6 +463,8 @@
|
||||
"message.style": "Message style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain",
|
||||
"loading.notion.preparing": "Preparing to export to Notion...",
|
||||
"loading.notion.exporting_progress": "Exporting to Notion ({{current}}/{{total}})...",
|
||||
"regenerate.confirm": "Regenerating will replace current message",
|
||||
"reset.confirm.content": "Are you sure you want to clear all data?",
|
||||
"reset.double.confirm.content": "All data will be lost, do you want to continue?",
|
||||
@@ -503,27 +473,36 @@
|
||||
"restore.success": "Restored successfully",
|
||||
"save.success.title": "Saved successfully",
|
||||
"searching": "Searching the internet...",
|
||||
"success.joplin.export": "Successfully exported to Joplin",
|
||||
"success.markdown.export.preconf": "Successfully exported the Markdown file to the preconfigured path",
|
||||
"success.markdown.export.specified": "Successfully exported the Markdown file",
|
||||
"success.notion.export": "Successfully exported to Notion",
|
||||
"success.yuque.export": "Successfully exported to Yuque",
|
||||
"success.joplin.export": "Successfully exported to Joplin",
|
||||
"switch.disabled": "Please wait for the current reply to complete",
|
||||
"tools": {
|
||||
"completed": "Completed",
|
||||
"invoking": "Invoking"
|
||||
},
|
||||
"topic.added": "New topic added",
|
||||
"upgrade.success.button": "Restart",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"upgrade.success.title": "Upgrade successfully",
|
||||
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again."
|
||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again.",
|
||||
"tools": {
|
||||
"invoking": "Invoking",
|
||||
"completed": "Completed"
|
||||
},
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command."
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Add to sidebar",
|
||||
"sidebar.remove.title": "Remove from sidebar",
|
||||
"title": "MinApp"
|
||||
"add": "Add",
|
||||
"apps.tab.search": "Search apps",
|
||||
"apps.tab.title": "Apps",
|
||||
"empty": "No mini apps",
|
||||
"find": "Find more",
|
||||
"more": "More",
|
||||
"settings.disabled_apps": "Disabled Apps",
|
||||
"sidebar.add.title": "Add to Sidebar",
|
||||
"sidebar.remove.title": "Remove from Sidebar",
|
||||
"title": "Web Apps"
|
||||
},
|
||||
"miniwindow": {
|
||||
"clipboard": {
|
||||
@@ -557,7 +536,7 @@
|
||||
"embedding": "Embedding",
|
||||
"embedding_model": "Embedding Model",
|
||||
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
|
||||
"function_calling": "Function Calling",
|
||||
"free": "Free",
|
||||
"no_matches": "No models available",
|
||||
"parameter_name": "Parameter Name",
|
||||
"parameter_type": {
|
||||
@@ -567,22 +546,22 @@
|
||||
"string": "Text"
|
||||
},
|
||||
"pinned": "Pinned",
|
||||
"rerank_model": "Reordering Model",
|
||||
"rerank_model_support_provider": "Currently, the reordering model only supports some providers ({{provider}})",
|
||||
"rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add.",
|
||||
"reasoning": "Reasoning",
|
||||
"search": "Search models...",
|
||||
"stream_output": "Stream output",
|
||||
"function_calling": "Function Calling",
|
||||
"type": {
|
||||
"embedding": "Embedding",
|
||||
"free": "Free",
|
||||
"function_calling": "Tool",
|
||||
"reasoning": "Reasoning",
|
||||
"rerank": "Reordering",
|
||||
"select": "Select Model Types",
|
||||
"text": "Text",
|
||||
"vision": "Vision",
|
||||
"websearch": "WebSearch"
|
||||
}
|
||||
"function_calling": "Function Calling"
|
||||
},
|
||||
"vision": "Vision",
|
||||
"websearch": "WebSearch",
|
||||
"rerank_model": "Reordering Model",
|
||||
"rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add."
|
||||
},
|
||||
"navbar": {
|
||||
"expand": "Expand Dialog",
|
||||
@@ -628,6 +607,12 @@
|
||||
},
|
||||
"title": "PlantUML Diagram"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
"keep_alive_time.placeholder": "Minutes",
|
||||
"keep_alive_time.title": "Keep Alive Time",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "Explain this concept to me",
|
||||
"summarize": "Summarize this text",
|
||||
@@ -635,12 +620,10 @@
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
"dmxapi": "DMXAPI",
|
||||
@@ -649,7 +632,6 @@
|
||||
"gemini": "Gemini",
|
||||
"gitee-ai": "Gitee AI",
|
||||
"github": "GitHub Models",
|
||||
"gpustack": "GPUStack",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
@@ -679,7 +661,9 @@
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI"
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Are you sure you want to restore data?",
|
||||
@@ -741,30 +725,15 @@
|
||||
"data.title": "Data Directory",
|
||||
"hour_interval_one": "{{count}} hour",
|
||||
"hour_interval_other": "{{count}} hours",
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "Check",
|
||||
"empty_token": "Please enter Joplin Authorization Token",
|
||||
"empty_url": "Please enter Joplin Clipper Service URL",
|
||||
"fail": "Joplin connection verification failed",
|
||||
"success": "Joplin connection verification successful"
|
||||
},
|
||||
"help": "In Joplin options, enable the web clipper (no browser extension needed), confirm the port, and copy the auth token here.",
|
||||
"title": "Joplin Configuration",
|
||||
"token": "Joplin Authorization Token",
|
||||
"token_placeholder": "Joplin Authorization Token",
|
||||
"url": "Joplin Web Clipper Service URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "When enabled, $$ will be forcibly used to mark LaTeX formulas when exporting to Markdown. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"markdown_export.force_dollar_math.title": "Force $$ for LaTeX formulas",
|
||||
"markdown_export.help": "If provided, exports will be automatically saved to this path; otherwise, a save dialog will appear.",
|
||||
"minute_interval_one": "{{count}} minute",
|
||||
"minute_interval_other": "{{count}} minutes",
|
||||
"markdown_export.title": "Markdown Export",
|
||||
"markdown_export.path": "Default Export Path",
|
||||
"markdown_export.path_placeholder": "Export Path",
|
||||
"markdown_export.select": "Select",
|
||||
"markdown_export.title": "Markdown Export",
|
||||
"minute_interval_one": "{{count}} minute",
|
||||
"minute_interval_other": "{{count}} minutes",
|
||||
"markdown_export.help": "If provided, exports will be automatically saved to this path; otherwise, a save dialog will appear.",
|
||||
"markdown_export.force_dollar_math.title": "Force $$ for LaTeX formulas",
|
||||
"markdown_export.force_dollar_math.help": "When enabled, $$ will be forcibly used to mark LaTeX formulas when exporting to Markdown. Note: This option also affects all export methods through Markdown, such as Notion, Yuque, etc.",
|
||||
"notion.api_key": "Notion API Key",
|
||||
"notion.api_key_placeholder": "Enter Notion API Key",
|
||||
"notion.auto_split": "Auto split when exporting",
|
||||
@@ -786,22 +755,11 @@
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.title": "Notion Configuration",
|
||||
"obsidian": {
|
||||
"folder": "Folder",
|
||||
"folder_placeholder": "Please enter the folder name",
|
||||
"tags": "Global Tags",
|
||||
"tags_placeholder": "Please enter the tag name, separate multiple tags with commas",
|
||||
"title": "Obsidian Configuration",
|
||||
"vault": "Vault",
|
||||
"vault_placeholder": "Please enter the vault name"
|
||||
},
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
"autoSync.off": "Off",
|
||||
"backup.button": "Backup to WebDAV",
|
||||
"backup.modal.filename.placeholder": "Please enter backup filename",
|
||||
"backup.modal.title": "Backup to WebDAV",
|
||||
"host": "WebDAV Host",
|
||||
"host.placeholder": "http://localhost:8080",
|
||||
"hour_interval_one": "{{count}} hour",
|
||||
@@ -813,12 +771,14 @@
|
||||
"password": "WebDAV Password",
|
||||
"path": "WebDAV Path",
|
||||
"path.placeholder": "/backup",
|
||||
"restore.button": "Restore from WebDAV",
|
||||
"restore.confirm.content": "Restoring from WebDAV will overwrite current data. Do you want to continue?",
|
||||
"restore.confirm.title": "Confirm Restore",
|
||||
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
||||
"restore.modal.select.placeholder": "Please select a backup file to restore",
|
||||
"backup.modal.title": "Backup to WebDAV",
|
||||
"backup.modal.filename.placeholder": "Please enter backup filename",
|
||||
"restore.modal.title": "Restore from WebDAV",
|
||||
"restore.modal.select.placeholder": "Please select a backup file to restore",
|
||||
"restore.confirm.title": "Confirm Restore",
|
||||
"restore.confirm.content": "Restoring from WebDAV will overwrite current data. Do you want to continue?",
|
||||
"restore.button": "Restore from WebDAV",
|
||||
"restore.content": "Restore from WebDAV will overwrite the current data, continue?",
|
||||
"restore.title": "Restore from WebDAV",
|
||||
"syncError": "Backup Error",
|
||||
"syncStatus": "Backup Status",
|
||||
@@ -839,6 +799,36 @@
|
||||
"title": "Yuque Configuration",
|
||||
"token": "Yuque Token",
|
||||
"token_placeholder": "Please enter the Yuque Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"check": {
|
||||
"button": "Check",
|
||||
"empty_url": "Please enter the Obsidian REST API URL first",
|
||||
"empty_api_key": "Please enter the Obsidian API Key first",
|
||||
"fail": "Obsidian connection verification failed",
|
||||
"success": "Obsidian connection verification successful"
|
||||
},
|
||||
"help": "Install the Obsidian plugin Local REST API first, then get the Obsidian API Key",
|
||||
"url": "Obsidian Knowledge Base URL",
|
||||
"url_placeholder": "http://127.0.0.1:27123/",
|
||||
"title": "Obsidian Configuration",
|
||||
"api_key": "Obsidian API Key",
|
||||
"api_key_placeholder": "Please enter the Obsidian API Key"
|
||||
},
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "Check",
|
||||
"empty_url": "Please enter Joplin Clipper Service URL",
|
||||
"empty_token": "Please enter Joplin Authorization Token",
|
||||
"fail": "Joplin connection verification failed",
|
||||
"success": "Joplin connection verification successful"
|
||||
},
|
||||
"title": "Joplin Configuration",
|
||||
"help": "In Joplin options, enable the web clipper (no browser extension needed), confirm the port, and copy the auth token here.",
|
||||
"url": "Joplin Web Clipper Service URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"token": "Joplin Authorization Token",
|
||||
"token_placeholder": "Joplin Authorization Token"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "Assistant Settings",
|
||||
@@ -884,15 +874,12 @@
|
||||
"input.target_language.english": "English",
|
||||
"input.target_language.japanese": "Japanese",
|
||||
"input.target_language.russian": "Russian",
|
||||
"launch.onboot": "Start Automatically on Boot",
|
||||
"launch.title": "Launch",
|
||||
"launch.totray": "Minimize to Tray on Launch",
|
||||
"mcp": {
|
||||
"actions": "Actions",
|
||||
"active": "Active",
|
||||
"addError": "Failed to add server",
|
||||
"addServer": "Add Server",
|
||||
"addSuccess": "Server added successfully",
|
||||
"addError": "Failed to add server",
|
||||
"args": "Arguments",
|
||||
"argsTooltip": "Each argument on a new line",
|
||||
"baseUrlTooltip": "Remote server base URL",
|
||||
@@ -901,51 +888,51 @@
|
||||
"config_description": "Configure Model Context Protocol servers",
|
||||
"confirmDelete": "Delete Server",
|
||||
"confirmDeleteMessage": "Are you sure you want to delete the server?",
|
||||
"deleteError": "Failed to delete server",
|
||||
"deleteSuccess": "Server deleted successfully",
|
||||
"dependenciesInstall": "Install Dependencies",
|
||||
"dependenciesInstalling": "Installing dependencies...",
|
||||
"deleteError": "Failed to delete server",
|
||||
"description": "Description",
|
||||
"duplicateName": "A server with this name already exists",
|
||||
"editJson": "Edit JSON",
|
||||
"editServer": "Edit Server",
|
||||
"env": "Environment Variables",
|
||||
"envTooltip": "Format: KEY=value, one per line",
|
||||
"findMore": "Find More MCP Servers",
|
||||
"install": "Install",
|
||||
"installError": "Failed to install dependencies",
|
||||
"installSuccess": "Dependencies installed successfully",
|
||||
"jsonFormatError": "JSON formatting error",
|
||||
"jsonModeHint": "Edit the JSON representation of the MCP server configuration. Please ensure the format is correct before saving.",
|
||||
"jsonSaveError": "Failed to save JSON configuration.",
|
||||
"jsonSaveSuccess": "JSON configuration has been saved.",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"name": "Name",
|
||||
"nameRequired": "Please enter a server name",
|
||||
"noServers": "No servers configured",
|
||||
"npx_list": {
|
||||
"actions": "Actions",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
"description": "Description",
|
||||
"no_packages": "No packages found",
|
||||
"npm": "NPM",
|
||||
"package_name": "Package Name",
|
||||
"scope_placeholder": "Enter npm scope (e.g. @your-org)",
|
||||
"scope_required": "Please enter npm scope",
|
||||
"search": "Search",
|
||||
"search_error": "Search error",
|
||||
"title": "NPX Package List",
|
||||
"usage": "Usage",
|
||||
"version": "Version"
|
||||
},
|
||||
"serverPlural": "servers",
|
||||
"serverSingular": "server",
|
||||
"title": "MCP Servers",
|
||||
"toggleError": "Toggle failed",
|
||||
"type": "Type",
|
||||
"updateError": "Failed to update server",
|
||||
"updateSuccess": "Server updated successfully",
|
||||
"url": "URL"
|
||||
"updateError": "Failed to update server",
|
||||
"url": "URL",
|
||||
"toggleError": "Toggle failed",
|
||||
"dependenciesInstalling": "Installing dependencies...",
|
||||
"dependenciesInstall": "Install Dependencies",
|
||||
"installSuccess": "Dependencies installed successfully",
|
||||
"installError": "Failed to install dependencies",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"install": "Install",
|
||||
"npx_list": {
|
||||
"title": "NPX Package List",
|
||||
"desc": "Search and add npm packages as MCP servers",
|
||||
"scope_placeholder": "Enter npm scope (e.g. @your-org)",
|
||||
"search": "Search",
|
||||
"package_name": "Package Name",
|
||||
"description": "Description",
|
||||
"usage": "Usage",
|
||||
"npm": "NPM",
|
||||
"version": "Version",
|
||||
"actions": "Actions",
|
||||
"scope_required": "Please enter npm scope",
|
||||
"no_packages": "No packages found",
|
||||
"search_error": "Search error"
|
||||
},
|
||||
"editJson": "Edit JSON",
|
||||
"jsonModeHint": "Edit the JSON representation of the MCP server configuration. Please ensure the format is correct before saving.",
|
||||
"jsonFormatError": "JSON formatting error",
|
||||
"jsonSaveSuccess": "JSON configuration has been saved.",
|
||||
"jsonSaveError": "Failed to save JSON configuration."
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
@@ -961,10 +948,6 @@
|
||||
"messages.math_engine": "Math engine",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.navigation": "Message Navigation",
|
||||
"messages.navigation.anchor": "Message Anchor",
|
||||
"messages.navigation.buttons": "Navigation Buttons",
|
||||
"messages.navigation.none": "None",
|
||||
"messages.title": "Message Settings",
|
||||
"messages.use_serif_font": "Use serif font",
|
||||
"model": "Default Model",
|
||||
@@ -985,7 +968,7 @@
|
||||
"models.check.enabled": "Enabled",
|
||||
"models.check.failed": "Failed",
|
||||
"models.check.keys_status_count": "Passed: {{count_passed}} keys, failed: {{count_failed}} keys",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} models passed health checks ({{count_partial}} models had inaccessible keys), {{count_failed}} models completely inaccessible.",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} models passed all keys, {{count_failed}} models failed all keys, {{count_partial}} models failed some keys",
|
||||
"models.check.no_api_keys": "No API keys found, please add API keys first.",
|
||||
"models.check.passed": "Passed",
|
||||
"models.check.select_api_key": "Select the API key to use:",
|
||||
@@ -1027,43 +1010,43 @@
|
||||
"check": "Check",
|
||||
"check_all_keys": "Check All Keys",
|
||||
"check_multiple_keys": "Check Multiple API Keys",
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot authentication failed.",
|
||||
"auth_success": "GitHub Copilot authentication successful.",
|
||||
"auth_success_title": "Certification successful.",
|
||||
"code_failed": "Failed to obtain Device Code, please try again.",
|
||||
"code_generated_desc": "Please copy the device code into the browser link below.",
|
||||
"code_generated_title": "Obtain Device Code",
|
||||
"confirm_login": "Excessive use may lead to your Github account being banned, please use it cautiously!!!!",
|
||||
"confirm_title": "Risk Warning",
|
||||
"connect": "Connect to Github",
|
||||
"custom_headers": "Custom request header",
|
||||
"description": "Your GitHub account needs to subscribe to Copilot.",
|
||||
"expand": "Expand",
|
||||
"headers_description": "Custom request headers (JSON format)",
|
||||
"invalid_json": "JSON format error",
|
||||
"login": "Log in to Github",
|
||||
"logout": "Exit GitHub",
|
||||
"logout_failed": "Exit failed, please try again.",
|
||||
"logout_success": "Successfully logged out.",
|
||||
"model_setting": "Model settings",
|
||||
"open_verification_first": "Please click the link above to access the verification page.",
|
||||
"rate_limit": "Rate limiting",
|
||||
"tooltip": "You need to log in to Github before using Github Copilot"
|
||||
},
|
||||
"delete.content": "Are you sure you want to delete this provider?",
|
||||
"delete.title": "Delete Provider",
|
||||
"docs_check": "Check",
|
||||
"docs_more_details": "for more details",
|
||||
"get_api_key": "Get API Key",
|
||||
"is_not_support_array_content": "Enable compatible mode",
|
||||
"no_models": "Please add models first before checking the API connection",
|
||||
"not_checked": "Not Checked",
|
||||
"remove_duplicate_keys": "Remove Duplicate Keys",
|
||||
"remove_invalid_keys": "Remove Invalid Keys",
|
||||
"search": "Search Providers...",
|
||||
"search_placeholder": "Search model id or name",
|
||||
"title": "Model Provider"
|
||||
"title": "Model Provider",
|
||||
"is_not_support_array_content": "Enable compatible mode",
|
||||
"copilot": {
|
||||
"tooltip": "You need to log in to Github before using Github Copilot",
|
||||
"description": "Your GitHub account needs to subscribe to Copilot.",
|
||||
"login": "Log in to Github",
|
||||
"connect": "Connect to Github",
|
||||
"logout": "Exit GitHub",
|
||||
"auth_success_title": "Certification successful.",
|
||||
"code_generated_title": "Obtain Device Code",
|
||||
"code_generated_desc": "Please copy the device code into the browser link below.",
|
||||
"code_failed": "Failed to obtain Device Code, please try again.",
|
||||
"auth_success": "GitHub Copilot authentication successful.",
|
||||
"auth_failed": "Github Copilot authentication failed.",
|
||||
"logout_success": "Successfully logged out.",
|
||||
"logout_failed": "Exit failed, please try again.",
|
||||
"confirm_title": "Risk Warning",
|
||||
"confirm_login": "Excessive use may lead to your Github account being banned, please use it cautiously!!!!",
|
||||
"rate_limit": "Rate limiting",
|
||||
"custom_headers": "Custom request header",
|
||||
"headers_description": "Custom request headers (JSON format)",
|
||||
"expand": "Expand",
|
||||
"model_setting": "Model settings",
|
||||
"invalid_json": "JSON format error",
|
||||
"open_verification_first": "Please click the link above to access the verification page."
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
@@ -1078,9 +1061,9 @@
|
||||
"quickAssistant": {
|
||||
"click_tray_to_show": "Click the tray icon to start",
|
||||
"enable_quick_assistant": "Enable Quick Assistant",
|
||||
"read_clipboard_at_startup": "Read clipboard at startup",
|
||||
"title": "Quick Assistant",
|
||||
"use_shortcut_to_show": "Right-click the tray icon or use shortcuts to start"
|
||||
"use_shortcut_to_show": "Right-click the tray icon or use shortcuts to start",
|
||||
"read_clipboard_at_startup": "Read clipboard at startup"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "Action",
|
||||
@@ -1117,18 +1100,14 @@
|
||||
"topic.position.left": "Left",
|
||||
"topic.position.right": "Right",
|
||||
"topic.show.time": "Show topic time",
|
||||
"tray.onclose": "Minimize to Tray on Close",
|
||||
"tray.show": "Show Tray Icon",
|
||||
"tray.title": "Tray",
|
||||
"tray.title": "Enable System Tray Icon",
|
||||
"websearch": {
|
||||
"blacklist": "Blacklist",
|
||||
"blacklist_description": "Results from the following websites will not appear in search results",
|
||||
"blacklist_tooltip": "Please use the following format (separated by line breaks)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "Check",
|
||||
"check_failed": "Verification failed",
|
||||
"check_success": "Verification successful",
|
||||
"enhance_mode": "Search enhance mode",
|
||||
"enhance_mode_tooltip": "Use the default model to extract search keywords from the problem and search",
|
||||
"check_failed": "Verification failed",
|
||||
"get_api_key": "Get API Key",
|
||||
"no_provider_selected": "Please select a search service provider before checking.",
|
||||
"search_max_result": "Number of search results",
|
||||
@@ -1136,6 +1115,8 @@
|
||||
"search_provider_placeholder": "Choose a search service provider.",
|
||||
"search_result_default": "Default",
|
||||
"search_with_time": "Search with dates included",
|
||||
"enhance_mode": "Search enhance mode",
|
||||
"enhance_mode_tooltip": "Use the default model to extract search keywords from the problem and search",
|
||||
"tavily": {
|
||||
"api_key": "Tavily API Key",
|
||||
"api_key.placeholder": "Enter Tavily API Key",
|
||||
@@ -1143,6 +1124,74 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "Web Search"
|
||||
},
|
||||
"nodeRequired": "Node.js Required",
|
||||
"nodeSettings": {
|
||||
"title": "Node.js Environment Settings",
|
||||
"description": "Manage the built-in Node.js environment for Cherry Studio. You can select which version of Node.js to install for optimal compatibility.",
|
||||
"status": "Status",
|
||||
"checking": "Checking...",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not Installed",
|
||||
"refresh": "Refresh",
|
||||
"version": "Node.js Version",
|
||||
"versionHelp": "Select the version of Node.js to install",
|
||||
"customVersion": "Custom Version",
|
||||
"customVersionHelp": "If you need a specific version, enter it here (e.g., 18.16.1)",
|
||||
"install": "Install Node.js",
|
||||
"reinstall": "Reinstall Node.js",
|
||||
"installSuccess": "Node.js v{{version}} installed successfully",
|
||||
"installFailed": "Failed to install Node.js"
|
||||
},
|
||||
"nodeSettingsTab": "Node.js Environment",
|
||||
"appsManagerTab": "Apps Manager",
|
||||
"packageDeployerTab": "Deploy Package",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "Advanced Options",
|
||||
"deploy": "Deploy",
|
||||
"deployFailed": "Failed to deploy package",
|
||||
"deploySuccess": "{{name}} has been successfully deployed on port {{port}}",
|
||||
"description": "Upload a ZIP file containing Node.js application code. The package will be automatically extracted and installed.",
|
||||
"fileSelectError": "Error selecting file",
|
||||
"installNode": "Install Node.js",
|
||||
"installNodePrompt": "Node.js is required to deploy applications. Would you like to install it now?",
|
||||
"namePlaceholder": "Enter a name for your deployed application",
|
||||
"nodeInstallFailed": "Failed to install Node.js",
|
||||
"nodeInstallSuccess": "Node.js installed successfully",
|
||||
"nodeNeeded": "Built-in Node.js is required to run applications.",
|
||||
"nodeNotAvailable": "Node.js is not available",
|
||||
"nodeRequired": "Node.js Required",
|
||||
"noFileSelected": "Please select a ZIP file to deploy",
|
||||
"open": "Open in Browser",
|
||||
"selectZip": "Click to select ZIP file",
|
||||
"title": "Deploy Code Package",
|
||||
"moduleTypeError": "Module Type Error",
|
||||
"esModuleError": "ES module syntax detected. Set \"type\": \"module\" in package.json or use .mjs extension.",
|
||||
"convertToCommonJS": "Convert to CommonJS syntax",
|
||||
"nextJsDetected": "Next.js Application Detected",
|
||||
"buildStepAdded": "Build step has been automatically added for Next.js application.",
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command.",
|
||||
"deployPackage": "Deploy Package",
|
||||
"deployFromZip": "From ZIP",
|
||||
"deployFromGit": "From Git",
|
||||
"selectZipFile": "Select ZIP File",
|
||||
"appName": "Application Name",
|
||||
"appNamePlaceholder": "My Application",
|
||||
"port": "Port",
|
||||
"portPlaceholder": "3000",
|
||||
"portTooltip": "The port on which your application will run. Leave empty for automatic port assignment.",
|
||||
"showAdvanced": "Show Advanced Options",
|
||||
"hideAdvanced": "Hide Advanced Options",
|
||||
"installCommand": "Install Command",
|
||||
"buildCommand": "Build Command",
|
||||
"startCommand": "Start Command",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"deploy": "Deploy",
|
||||
"repoUrl": "Git Repository URL",
|
||||
"repoUrlRequired": "Please enter a Git repository URL",
|
||||
"noRepoUrlProvided": "Please provide a Git repository URL",
|
||||
"packageRequired": "Please select a package file"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1180,6 +1229,146 @@
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"nodeapp": {
|
||||
"add": "Add App",
|
||||
"addNew": "Add New Node.js App",
|
||||
"addSuccess": "App added successfully",
|
||||
"author": "Author",
|
||||
"codeRunner": {
|
||||
"description": "Enter your Node.js code below and click 'Run' to execute it. Your code will be run in a temporary Node.js environment.",
|
||||
"emptyCode": "Please enter some code to run",
|
||||
"open": "Open in Browser",
|
||||
"output": "Output",
|
||||
"placeholder": "// Enter your Node.js code here\n// Example:\nconst http = require('http');\n\nconst server = http.createServer((req, res) => {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end('<h1>Hello from Cherry Studio!</h1>');\n});\n\nconst PORT = process.env.PORT || 3000;\nserver.listen(PORT, () => {\n console.log(`Server running on port ${PORT}`);\n});",
|
||||
"run": "Run Code",
|
||||
"success": "Code is running on port {{port}}",
|
||||
"title": "Code Runner"
|
||||
},
|
||||
"codeRunnerTab": "Code Runner",
|
||||
"empty": "No Node.js apps found",
|
||||
"featured": "Featured Apps",
|
||||
"form": {
|
||||
"author": "Author",
|
||||
"authorPlaceholder": "The author of the app",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Brief description of the app's functionality",
|
||||
"homepage": "Homepage",
|
||||
"homepagePlaceholder": "Homepage URL for the application",
|
||||
"installCommand": "Install Command",
|
||||
"installCommandHelp": "Command to install dependencies (defaults to 'npm install')",
|
||||
"buildCommand": "Build Command",
|
||||
"buildCommandHelp": "Command to build the application before starting (e.g. 'npm run build')",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"nextJsHelp": "Apply Next.js-specific optimizations for deployment",
|
||||
"name": "App Name",
|
||||
"nameRequired": "App name is required",
|
||||
"namePlaceholder": "Name of your Node.js application",
|
||||
"port": "Port",
|
||||
"portHelp": "Port the app will run on (detected automatically if not specified)"
|
||||
},
|
||||
"install": "Install",
|
||||
"installSuccess": "{{name}} installed successfully",
|
||||
"installed": "Installed",
|
||||
"marketplaceTab": "Marketplace",
|
||||
"more": "More",
|
||||
"notInstalled": "Not Installed",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "Advanced Options",
|
||||
"deploy": "Deploy",
|
||||
"deployFailed": "Failed to deploy package",
|
||||
"deploySuccess": "{{name}} has been successfully deployed on port {{port}}",
|
||||
"description": "Upload a ZIP file containing Node.js application code. The package will be automatically extracted and installed.",
|
||||
"fileSelectError": "Error selecting file",
|
||||
"installNode": "Install Node.js",
|
||||
"installNodePrompt": "Node.js is required to deploy applications. Would you like to install it now?",
|
||||
"namePlaceholder": "Enter a name for your deployed application",
|
||||
"nodeInstallFailed": "Failed to install Node.js",
|
||||
"nodeInstallSuccess": "Node.js installed successfully",
|
||||
"nodeNeeded": "Built-in Node.js is required to run applications.",
|
||||
"nodeNotAvailable": "Node.js is not available",
|
||||
"nodeRequired": "Node.js Required",
|
||||
"noFileSelected": "Please select a ZIP file to deploy",
|
||||
"open": "Open in Browser",
|
||||
"selectZip": "Click to select ZIP file",
|
||||
"title": "Deploy Code Package",
|
||||
"moduleTypeError": "Module Type Error",
|
||||
"esModuleError": "ES module syntax detected. Set \"type\": \"module\" in package.json or use .mjs extension.",
|
||||
"convertToCommonJS": "Convert to CommonJS syntax",
|
||||
"nextJsDetected": "Next.js Application Detected",
|
||||
"buildStepAdded": "Build step has been automatically added for Next.js application.",
|
||||
"nextJsInfo": "Next.js Application Note",
|
||||
"nextJsDescription": "Next.js applications require a build step before they can be started. Make sure to check 'This is a Next.js application' or manually add 'npm run build' as the build command.",
|
||||
"deployPackage": "Deploy Package",
|
||||
"deployFromZip": "From ZIP",
|
||||
"deployFromGit": "From Git",
|
||||
"selectZipFile": "Select ZIP File",
|
||||
"appName": "Application Name",
|
||||
"appNamePlaceholder": "My Application",
|
||||
"port": "Port",
|
||||
"portPlaceholder": "3000",
|
||||
"portTooltip": "The port on which your application will run. Leave empty for automatic port assignment.",
|
||||
"showAdvanced": "Show Advanced Options",
|
||||
"hideAdvanced": "Hide Advanced Options",
|
||||
"installCommand": "Install Command",
|
||||
"buildCommand": "Build Command",
|
||||
"startCommand": "Start Command",
|
||||
"isNextJs": "This is a Next.js application",
|
||||
"deploy": "Deploy",
|
||||
"repoUrl": "Git Repository URL",
|
||||
"repoUrlRequired": "Please enter a Git repository URL",
|
||||
"noRepoUrlProvided": "Please provide a Git repository URL",
|
||||
"packageRequired": "Please select a package file"
|
||||
},
|
||||
"packageDeployerTab": "Deploy Package",
|
||||
"running": "Running",
|
||||
"start": "Start",
|
||||
"startSuccess": "{{name}} started on port {{port}}",
|
||||
"stop": "Stop",
|
||||
"stopSuccess": "{{name}} stopped successfully",
|
||||
"title": "Node.js Apps",
|
||||
"uninstall": "Uninstall",
|
||||
"uninstallSuccess": "{{name}} uninstalled successfully",
|
||||
"update": "Update",
|
||||
"updateSuccess": "{{name}} updated successfully",
|
||||
"version": "Version",
|
||||
"viewRepository": "View Repository"
|
||||
},
|
||||
"model": {
|
||||
"add_parameter": "Add Parameter",
|
||||
"all": "All",
|
||||
"custom_parameters": "Custom Parameters",
|
||||
"dimensions": "Dimensions {{dimensions}}",
|
||||
"edit": "Edit Model",
|
||||
"embedding": "Embedding",
|
||||
"embedding_model": "Embedding Model",
|
||||
"embedding_model_tooltip": "Add in Settings->Model Provider->Manage",
|
||||
"free": "Free",
|
||||
"no_matches": "No models available",
|
||||
"parameter_name": "Parameter Name",
|
||||
"parameter_type": {
|
||||
"boolean": "Boolean",
|
||||
"json": "JSON",
|
||||
"number": "Number",
|
||||
"string": "Text"
|
||||
},
|
||||
"pinned": "Pinned",
|
||||
"reasoning": "Reasoning",
|
||||
"search": "Search models...",
|
||||
"stream_output": "Stream output",
|
||||
"function_calling": "Function Calling",
|
||||
"type": {
|
||||
"embedding": "Embedding",
|
||||
"reasoning": "Reasoning",
|
||||
"select": "Select Model Types",
|
||||
"text": "Text",
|
||||
"vision": "Vision",
|
||||
"function_calling": "Function Calling"
|
||||
},
|
||||
"vision": "Vision",
|
||||
"websearch": "WebSearch",
|
||||
"rerank_model": "Reordering Model",
|
||||
"rerank_model_tooltip": "Click the Manage button in Settings -> Model Services to add."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,16 +96,6 @@
|
||||
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
|
||||
"default.name": "デフォルトアシスタント",
|
||||
"default.topic.name": "デフォルトトピック",
|
||||
"history": {
|
||||
"assistant_node": "アシスタント",
|
||||
"click_to_navigate": "メッセージに移動",
|
||||
"coming_soon": "チャットワークフロー図がすぐに登場します",
|
||||
"no_messages": "メッセージが見つかりませんでした",
|
||||
"start_conversation": "チャットを開始してチャットワークフロー図を確認してください",
|
||||
"title": "チャット履歴",
|
||||
"user_node": "ユーザー",
|
||||
"view_full_content": "完全な内容を表示"
|
||||
},
|
||||
"input.auto_resize": "高さを自動調整",
|
||||
"input.clear": "クリア {{Command}}",
|
||||
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
|
||||
@@ -115,8 +105,6 @@
|
||||
"input.estimated_tokens.tip": "推定トークン数",
|
||||
"input.expand": "展開",
|
||||
"input.file_not_supported": "モデルはこのファイルタイプをサポートしません",
|
||||
"input.generate_image": "画像を生成する",
|
||||
"input.generate_image_not_supported": "モデルは画像の生成をサポートしていません。",
|
||||
"input.knowledge_base": "ナレッジベース",
|
||||
"input.new.context": "コンテキストをクリア {{Command}}",
|
||||
"input.new_topic": "新しいトピック {{Command}}",
|
||||
@@ -138,13 +126,6 @@
|
||||
"message.quote": "引用",
|
||||
"message.regenerate.model": "モデルを切り替え",
|
||||
"message.useful": "役立つ",
|
||||
"navigation": {
|
||||
"first": "最初のメッセージです",
|
||||
"history": "チャット履歴",
|
||||
"last": "最後のメッセージです",
|
||||
"next": "次のメッセージ",
|
||||
"prev": "前のメッセージ"
|
||||
},
|
||||
"resend": "再送信",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "コードブロック折り畳み",
|
||||
@@ -176,42 +157,37 @@
|
||||
"topics.edit.placeholder": "新しい名前を入力",
|
||||
"topics.edit.title": "名前を編集",
|
||||
"topics.export.image": "画像としてエクスポート",
|
||||
"topics.export.joplin": "Joplin にエクスポート",
|
||||
"topics.export.md": "Markdownとしてエクスポート",
|
||||
"topics.export.notion": "Notion にエクスポート",
|
||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||
"topics.export.obsidian_atributes": "ノートの属性を設定",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "作成日時",
|
||||
"topics.export.obsidian_created_placeholder": "作成日時を選択してください",
|
||||
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||
"topics.export.obsidian_export_success": "エクスポート成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
||||
"topics.export.obsidian_operate": "処理方法",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)",
|
||||
"topics.export.obsidian_operate_placeholder": "処理方法を選択してください",
|
||||
"topics.export.obsidian_operate_prepend": "先頭に追加",
|
||||
"topics.export.obsidian_source": "ソース",
|
||||
"topics.export.obsidian_source_placeholder": "ソースを入力してください",
|
||||
"topics.export.obsidian_tags": "タグ",
|
||||
"topics.export.obsidian_tags_placeholder": "タグを入力してください。複数のタグは英語のコンマで区切ってください",
|
||||
"topics.export.obsidian_title": "タイトル",
|
||||
"topics.export.obsidian_title_placeholder": "タイトルを入力してください",
|
||||
"topics.export.obsidian_title_required": "タイトルは空白にできません",
|
||||
"topics.export.title": "エクスポート",
|
||||
"topics.export.word": "Wordとしてエクスポート",
|
||||
"topics.export.yuque": "語雀にエクスポート",
|
||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
||||
"topics.export.obsidian_fetch_failed": "Obsidian ファイルフォルダ構造取得失敗",
|
||||
"topics.export.obsidian_select_folder": "Obsidian ファイルフォルダ選択",
|
||||
"topics.export.obsidian_select_folder.btn": "確定",
|
||||
"topics.export.obsidian_export_success": "エクスポート成功",
|
||||
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||
"topics.export.obsidian_show_md_files": "mdファイルを表示",
|
||||
"topics.export.obsidian_selected_path": "選択済みパス",
|
||||
"topics.export.joplin": "Joplin にエクスポート",
|
||||
"topics.list": "トピックリスト",
|
||||
"topics.move_to": "移動先",
|
||||
"topics.new": "新しいトピック",
|
||||
"topics.pinned": "トピックを固定",
|
||||
"topics.prompt": "トピック提示語",
|
||||
"topics.prompt.edit.title": "トピック提示語を編集する",
|
||||
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
||||
"topics.title": "トピック",
|
||||
"topics.unpinned": "固定解除",
|
||||
"translate": "翻訳"
|
||||
"topics.new": "新しいトピック",
|
||||
"translate": "翻訳",
|
||||
"navigation": {
|
||||
"prev": "前のメッセージ",
|
||||
"next": "次のメッセージ",
|
||||
"first": "最初のメッセージです",
|
||||
"last": "最後のメッセージです"
|
||||
}
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折りたたむ",
|
||||
@@ -221,7 +197,6 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "追加",
|
||||
"advanced_settings": "詳細設定",
|
||||
"and": "と",
|
||||
"assistant": "アシスタント",
|
||||
"avatar": "アバター",
|
||||
@@ -230,8 +205,6 @@
|
||||
"chat": "チャット",
|
||||
"clear": "クリア",
|
||||
"close": "閉じる",
|
||||
"confirm": "確認",
|
||||
"copied": "コピーされました",
|
||||
"copy": "コピー",
|
||||
"cut": "切り取り",
|
||||
"default": "デフォルト",
|
||||
@@ -241,7 +214,6 @@
|
||||
"download": "ダウンロード",
|
||||
"duplicate": "複製",
|
||||
"edit": "編集",
|
||||
"expand": "展開",
|
||||
"footnote": "引用内容",
|
||||
"footnotes": "脚注",
|
||||
"fullscreen": "全画面モードに入りました。F11キーで終了します",
|
||||
@@ -249,7 +221,6 @@
|
||||
"language": "言語",
|
||||
"model": "モデル",
|
||||
"models": "モデル",
|
||||
"more": "もっと",
|
||||
"name": "名前",
|
||||
"paste": "貼り付け",
|
||||
"prompt": "プロンプト",
|
||||
@@ -262,7 +233,12 @@
|
||||
"select": "選択",
|
||||
"topics": "トピック",
|
||||
"warning": "警告",
|
||||
"you": "あなた"
|
||||
"you": "あなた",
|
||||
"copied": "コピーされました",
|
||||
"confirm": "確認",
|
||||
"more": "もっと",
|
||||
"advanced_settings": "詳細設定",
|
||||
"expand": "展開"
|
||||
},
|
||||
"docs": {
|
||||
"title": "ドキュメント"
|
||||
@@ -320,12 +296,6 @@
|
||||
"title": "ファイル",
|
||||
"type": "タイプ"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
"keep_alive_time.placeholder": "分",
|
||||
"keep_alive_time.title": "保持時間",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "チャットを続ける",
|
||||
"locate.message": "メッセージを探す",
|
||||
@@ -395,13 +365,13 @@
|
||||
"threshold_too_large_or_small": "しきい値は0より大きく1より小さい必要があります",
|
||||
"threshold_tooltip": "ユーザーの質問と知識ベースの内容の関連性を評価するためのしきい値(0-1)",
|
||||
"title": "ナレッジベース",
|
||||
"topN": "返却される結果の数",
|
||||
"topN__too_large_or_small": "結果の数は100より大きくてはならず、1より小さくてはなりません。",
|
||||
"topN_placeholder": "未設定",
|
||||
"topN_tooltip": "返されるマッチ結果の数は、数値が大きいほどマッチ結果が多くなりますが、消費されるトークンも増えます。",
|
||||
"url_added": "URLが追加されました",
|
||||
"url_placeholder": "URLを入力, 複数のURLはEnterで区切る",
|
||||
"urls": "URL"
|
||||
"urls": "URL",
|
||||
"topN": "返却される結果の数",
|
||||
"topN_placeholder": "未設定",
|
||||
"topN__too_large_or_small": "結果の数は100より大きくてはならず、1より小さくてはなりません。",
|
||||
"topN_tooltip": "返されるマッチ結果の数は、数値が大きいほどマッチ結果が多くなりますが、消費されるトークンも増えます。"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "アラビア語",
|
||||
@@ -439,22 +409,22 @@
|
||||
"title": "Mermaid図"
|
||||
},
|
||||
"message": {
|
||||
"attachments": {
|
||||
"pasted_text": "クリップボードファイル",
|
||||
"pasted_image": "クリップボード画像"
|
||||
},
|
||||
"api.check.model.title": "検出に使用するモデルを選択してください",
|
||||
"api.connection.failed": "接続に失敗しました",
|
||||
"api.connection.success": "接続に成功しました",
|
||||
"assistant.added.content": "アシスタントが追加されました",
|
||||
"attachments": {
|
||||
"pasted_image": "クリップボード画像",
|
||||
"pasted_text": "クリップボードファイル"
|
||||
},
|
||||
"backup.failed": "バックアップに失敗しました",
|
||||
"backup.start.success": "バックアップを開始しました",
|
||||
"backup.success": "バックアップに成功しました",
|
||||
"chat.completion.paused": "チャットの完了が一時停止されました",
|
||||
"citations": "参考文献",
|
||||
"copied": "コピーしました!",
|
||||
"copy.failed": "コピーに失敗しました",
|
||||
"copy.success": "コピーしました!",
|
||||
"copy.failed": "コピーに失敗しました",
|
||||
"error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません",
|
||||
"error.dimension_too_large": "内容のサイズが大きすぎます",
|
||||
"error.enter.api.host": "APIホストを入力してください",
|
||||
@@ -467,20 +437,20 @@
|
||||
"error.invalid.enter.model": "モデルを選択してください",
|
||||
"error.invalid.proxy.url": "無効なプロキシURL",
|
||||
"error.invalid.webdav": "無効なWebDAV設定",
|
||||
"error.joplin.export": "Joplin へのエクスポートに失敗しました。Joplin が実行中であることを確認してください",
|
||||
"error.joplin.no_config": "Joplin 認証トークン または URL が設定されていません",
|
||||
"error.markdown.export.preconf": "Markdown ファイルを事前設定されたパスにエクスポートできませんでした",
|
||||
"error.markdown.export.specified": "Markdown ファイルのエクスポートに失敗しました",
|
||||
"error.notion.export": "Notionへのエクスポートに失敗しました。接続状態と設定を確認してください",
|
||||
"error.notion.no_api_key": "Notion ApiKey または Notion DatabaseID が設定されていません",
|
||||
"error.yuque.export": "語雀へのエクスポートに失敗しました。接続状態と設定を確認してください",
|
||||
"error.yuque.no_config": "語雀Token または 知識ベースID が設定されていません",
|
||||
"error.joplin.no_config": "Joplin 認証トークン または URL が設定されていません",
|
||||
"error.joplin.export": "Joplin へのエクスポートに失敗しました。Joplin が実行中であることを確認してください",
|
||||
"group.delete.content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
|
||||
"group.delete.title": "分組メッセージを削除",
|
||||
"loading.notion.preparing": "Notionへのエクスポートを準備中...",
|
||||
"loading.notion.exporting_progress": "Notionにエクスポート中 ({{current}}/{{total}})...",
|
||||
"ignore.knowledge.base": "インターネットモードが有効になっています。ナレッジベースを無視します",
|
||||
"info.notion.block_reach_limit": "会話が長すぎます。Notionにページごとにエクスポートしています",
|
||||
"loading.notion.exporting_progress": "Notionにエクスポート中 ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "Notionへのエクスポートを準備中...",
|
||||
"mention.title": "モデルを切り替える",
|
||||
"message.code_style": "コードスタイル",
|
||||
"message.delete.content": "このメッセージを削除してもよろしいですか?",
|
||||
@@ -503,22 +473,22 @@
|
||||
"restore.success": "復元に成功しました",
|
||||
"save.success.title": "保存に成功しました",
|
||||
"searching": "インターネットで検索中...",
|
||||
"success.joplin.export": "Joplin へのエクスポートに成功しました",
|
||||
"success.markdown.export.preconf": "Markdown ファイルを事前設定されたパスに正常にエクスポートしました",
|
||||
"success.markdown.export.specified": "Markdown ファイルを正常にエクスポートしました",
|
||||
"success.notion.export": "Notionへのエクスポートに成功しました",
|
||||
"success.yuque.export": "語雀へのエクスポートに成功しました",
|
||||
"success.joplin.export": "Joplin へのエクスポートに成功しました",
|
||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||
"tools": {
|
||||
"completed": "完了",
|
||||
"invoking": "呼び出し中"
|
||||
},
|
||||
"topic.added": "新しいトピックが追加されました",
|
||||
"upgrade.success.button": "再起動",
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
"upgrade.success.title": "アップグレードに成功しました",
|
||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。",
|
||||
"tools": {
|
||||
"invoking": "呼び出し中",
|
||||
"completed": "完了"
|
||||
}
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "サイドバーに追加",
|
||||
@@ -557,7 +527,7 @@
|
||||
"embedding": "埋め込み",
|
||||
"embedding_model": "埋め込み模型",
|
||||
"embedding_model_tooltip": "設定->モデルサービス->管理で追加",
|
||||
"function_calling": "関数呼び出し",
|
||||
"free": "無料",
|
||||
"no_matches": "利用可能なモデルがありません",
|
||||
"parameter_name": "パラメータ名",
|
||||
"parameter_type": {
|
||||
@@ -567,22 +537,22 @@
|
||||
"string": "テキスト"
|
||||
},
|
||||
"pinned": "固定済み",
|
||||
"rerank_model": "再順序付けモデル",
|
||||
"rerank_model_support_provider": "現在の再順序付けモデルは、{{provider}} のみサポートしています",
|
||||
"rerank_model_tooltip": "設定->モデルサービスに移動し、管理ボタンをクリックして追加します。",
|
||||
"reasoning": "推論",
|
||||
"search": "モデルを検索...",
|
||||
"stream_output": "ストリーム出力",
|
||||
"function_calling": "関数呼び出し",
|
||||
"type": {
|
||||
"embedding": "埋め込み",
|
||||
"free": "無料",
|
||||
"function_calling": "ツール",
|
||||
"reasoning": "推論",
|
||||
"rerank": "再順序付け",
|
||||
"select": "モデルタイプを選択",
|
||||
"text": "テキスト",
|
||||
"vision": "画像",
|
||||
"websearch": "ウェブ検索"
|
||||
}
|
||||
"function_calling": "関数呼び出し"
|
||||
},
|
||||
"vision": "画像",
|
||||
"websearch": "ウェブ検索",
|
||||
"rerank_model": "再順序付けモデル",
|
||||
"rerank_model_tooltip": "設定->モデルサービスに移動し、管理ボタンをクリックして追加します。"
|
||||
},
|
||||
"navbar": {
|
||||
"expand": "ダイアログを展開",
|
||||
@@ -628,6 +598,12 @@
|
||||
},
|
||||
"title": "PlantUML 図表"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
"keep_alive_time.placeholder": "分",
|
||||
"keep_alive_time.title": "保持時間",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"prompts": {
|
||||
"explanation": "この概念を説明してください",
|
||||
"summarize": "このテキストを要約してください",
|
||||
@@ -635,12 +611,10 @@
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
"dmxapi": "DMXAPI",
|
||||
@@ -649,7 +623,6 @@
|
||||
"gemini": "Gemini",
|
||||
"gitee-ai": "Gitee AI",
|
||||
"github": "GitHub Models",
|
||||
"gpustack": "GPUStack",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
@@ -679,7 +652,9 @@
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI",
|
||||
"voyageai": "Voyage AI"
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "データを復元しますか?",
|
||||
@@ -741,30 +716,15 @@
|
||||
"data.title": "データディレクトリ",
|
||||
"hour_interval_one": "{{count}} 時間",
|
||||
"hour_interval_other": "{{count}} 時間",
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "確認",
|
||||
"empty_token": "Joplin 認証トークン を先に入力してください",
|
||||
"empty_url": "Joplin 剪輯服務 URL を先に入力してください",
|
||||
"fail": "Joplin 接続確認に失敗しました",
|
||||
"success": "Joplin 接続確認に成功しました"
|
||||
},
|
||||
"help": "Joplin オプションで、剪輯サービスを有効にしてください。ポート番号を確認し、認証トークンをコピーしてください",
|
||||
"title": "Joplin 設定",
|
||||
"token": "Joplin 認証トークン",
|
||||
"token_placeholder": "Joplin 認証トークンを入力してください",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "有効にすると、Markdownにエクスポートする際にLaTeX数式を$$で強制的にマークします。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX数式に$$を強制使用",
|
||||
"markdown_export.help": "入力された場合、エクスポート時に自動的にこのパスに保存されます。未入力の場合、保存ダイアログが表示されます。",
|
||||
"minute_interval_one": "{{count}} 分",
|
||||
"minute_interval_other": "{{count}} 分",
|
||||
"markdown_export.title": "Markdown エクスポート",
|
||||
"markdown_export.path": "デフォルトのエクスポートパス",
|
||||
"markdown_export.path_placeholder": "エクスポートパス",
|
||||
"markdown_export.select": "選択",
|
||||
"markdown_export.title": "Markdown エクスポート",
|
||||
"minute_interval_one": "{{count}} 分",
|
||||
"minute_interval_other": "{{count}} 分",
|
||||
"markdown_export.help": "入力された場合、エクスポート時に自動的にこのパスに保存されます。未入力の場合、保存ダイアログが表示されます。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX数式に$$を強制使用",
|
||||
"markdown_export.force_dollar_math.help": "有効にすると、Markdownにエクスポートする際にLaTeX数式を$$で強制的にマークします。注意:この設定はNotion、Yuqueなど、Markdownを通じたすべてのエクスポート方法にも影響します。",
|
||||
"notion.api_key": "Notion APIキー",
|
||||
"notion.api_key_placeholder": "Notion APIキーを入力してください",
|
||||
"notion.auto_split": "ダイアログをエクスポートすると自動ページ分割",
|
||||
@@ -786,22 +746,11 @@
|
||||
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "フォルダー",
|
||||
"folder_placeholder": "フォルダーの名前を入力してください",
|
||||
"tags": "グローバルタグ",
|
||||
"tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください",
|
||||
"title": "Obsidian の設定",
|
||||
"vault": "ヴォールト(保管庫)",
|
||||
"vault_placeholder": "保管庫の名前を入力してください"
|
||||
},
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
"autoSync.off": "オフ",
|
||||
"backup.button": "WebDAVにバックアップ",
|
||||
"backup.modal.filename.placeholder": "バックアップファイル名を入力してください",
|
||||
"backup.modal.title": "WebDAV にバックアップ",
|
||||
"host": "WebDAVホスト",
|
||||
"host.placeholder": "http://localhost:8080",
|
||||
"hour_interval_one": "{{count}} 時間",
|
||||
@@ -814,16 +763,18 @@
|
||||
"path": "WebDAVパス",
|
||||
"path.placeholder": "/backup",
|
||||
"restore.button": "WebDAVから復元",
|
||||
"restore.confirm.content": "WebDAV から復元すると現在のデータが上書きされます。続行しますか?",
|
||||
"restore.confirm.title": "復元を確認",
|
||||
"restore.content": "WebDAVから復元すると現在のデータが上書きされます。続行しますか?",
|
||||
"restore.modal.select.placeholder": "復元するバックアップファイルを選択してください",
|
||||
"restore.modal.title": "WebDAV から復元",
|
||||
"restore.title": "WebDAVから復元",
|
||||
"syncError": "バックアップエラー",
|
||||
"syncStatus": "バックアップ状態",
|
||||
"title": "WebDAV",
|
||||
"user": "WebDAVユーザー"
|
||||
"user": "WebDAVユーザー",
|
||||
"backup.modal.title": "WebDAV にバックアップ",
|
||||
"backup.modal.filename.placeholder": "バックアップファイル名を入力してください",
|
||||
"restore.modal.title": "WebDAV から復元",
|
||||
"restore.modal.select.placeholder": "復元するバックアップファイルを選択してください",
|
||||
"restore.confirm.title": "復元を確認",
|
||||
"restore.confirm.content": "WebDAV から復元すると現在のデータが上書きされます。続行しますか?"
|
||||
},
|
||||
"yuque": {
|
||||
"check": {
|
||||
@@ -839,6 +790,36 @@
|
||||
"title": "Yuque設定",
|
||||
"token": "Yuqueトークン",
|
||||
"token_placeholder": "Yuqueトークンを入力してください"
|
||||
},
|
||||
"obsidian": {
|
||||
"check": {
|
||||
"button": "確認",
|
||||
"empty_url": "Obsidian REST API URL を先に入力してください",
|
||||
"empty_api_key": "Obsidian API Key を先に入力してください",
|
||||
"fail": "Obsidian 接続確認に失敗しました",
|
||||
"success": "Obsidian 接続確認に成功しました"
|
||||
},
|
||||
"help": "Obsidian プラグイン Local REST API を先にインストールしてください。その後、Obsidian API Key を取得してください",
|
||||
"url": "Obsidian 知識ベース URL",
|
||||
"url_placeholder": "http://127.0.0.1:27123/",
|
||||
"title": "Obsidian 設定",
|
||||
"api_key": "Obsidian API Key",
|
||||
"api_key_placeholder": "Obsidian API Key を入力してください"
|
||||
},
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "確認",
|
||||
"empty_url": "Joplin 剪輯服務 URL を先に入力してください",
|
||||
"empty_token": "Joplin 認証トークン を先に入力してください",
|
||||
"fail": "Joplin 接続確認に失敗しました",
|
||||
"success": "Joplin 接続確認に成功しました"
|
||||
},
|
||||
"title": "Joplin 設定",
|
||||
"help": "Joplin オプションで、剪輯サービスを有効にしてください。ポート番号を確認し、認証トークンをコピーしてください",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"token": "Joplin 認証トークン",
|
||||
"token_placeholder": "Joplin 認証トークンを入力してください"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "アシスタント設定",
|
||||
@@ -884,15 +865,12 @@
|
||||
"input.target_language.english": "英語",
|
||||
"input.target_language.japanese": "日本語",
|
||||
"input.target_language.russian": "ロシア語",
|
||||
"launch.onboot": "起動時に自動で開始",
|
||||
"launch.title": "起動",
|
||||
"launch.totray": "起動時にトレイに最小化",
|
||||
"mcp": {
|
||||
"actions": "操作",
|
||||
"active": "有効",
|
||||
"addError": "サーバーの追加に失敗しました",
|
||||
"addServer": "サーバーを追加",
|
||||
"addSuccess": "サーバーが正常に追加されました",
|
||||
"addError": "サーバーの追加に失敗しました",
|
||||
"args": "引数",
|
||||
"argsTooltip": "1行に1つの引数を入力してください",
|
||||
"baseUrlTooltip": "リモートURLアドレス",
|
||||
@@ -903,49 +881,49 @@
|
||||
"confirmDeleteMessage": "本当にこのサーバーを削除しますか?",
|
||||
"deleteError": "サーバーの削除に失敗しました",
|
||||
"deleteSuccess": "サーバーが正常に削除されました",
|
||||
"dependenciesInstall": "依存関係をインストール",
|
||||
"dependenciesInstalling": "依存関係をインストール中...",
|
||||
"description": "説明",
|
||||
"duplicateName": "同じ名前のサーバーが既に存在します",
|
||||
"editJson": "JSONを編集",
|
||||
"editServer": "サーバーを編集",
|
||||
"env": "環境変数",
|
||||
"envTooltip": "形式: KEY=value, 1行に1つ",
|
||||
"findMore": "MCP サーバーを見つける",
|
||||
"install": "インストール",
|
||||
"installError": "依存関係のインストールに失敗しました",
|
||||
"installSuccess": "依存関係のインストールに成功しました",
|
||||
"jsonFormatError": "JSONフォーマットエラー",
|
||||
"jsonModeHint": "MCPサーバー設定のJSON表現を編集します。保存する前に、フォーマットが正しいことを確認してください。",
|
||||
"jsonSaveError": "JSON設定の保存に失敗しました",
|
||||
"jsonSaveSuccess": "JSON設定が保存されました。",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"name": "名前",
|
||||
"nameRequired": "サーバー名を入力してください",
|
||||
"noServers": "サーバーが設定されていません",
|
||||
"npx_list": {
|
||||
"actions": "アクション",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
"description": "説明",
|
||||
"no_packages": "パッケージが見つかりません",
|
||||
"npm": "NPM",
|
||||
"package_name": "パッケージ名",
|
||||
"scope_placeholder": "npm スコープを入力 (例: @your-org)",
|
||||
"scope_required": "npm スコープを入力してください",
|
||||
"search": "検索",
|
||||
"search_error": "パッケージの検索に失敗しました",
|
||||
"title": "NPX パッケージリスト",
|
||||
"usage": "使用法",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"serverPlural": "サーバー",
|
||||
"serverSingular": "サーバー",
|
||||
"title": "MCP サーバー",
|
||||
"toggleError": "切り替えに失敗しました",
|
||||
"type": "タイプ",
|
||||
"updateError": "サーバーの更新に失敗しました",
|
||||
"updateSuccess": "サーバーが正常に更新されました",
|
||||
"url": "URL"
|
||||
"updateError": "サーバーの更新に失敗しました",
|
||||
"url": "URL",
|
||||
"toggleError": "切り替えに失敗しました",
|
||||
"dependenciesInstalling": "依存関係をインストール中...",
|
||||
"dependenciesInstall": "依存関係をインストール",
|
||||
"installSuccess": "依存関係のインストールに成功しました",
|
||||
"installError": "依存関係のインストールに失敗しました",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"install": "インストール",
|
||||
"npx_list": {
|
||||
"title": "NPX パッケージリスト",
|
||||
"desc": "npm パッケージを検索して MCP サーバーとして追加",
|
||||
"scope_placeholder": "npm スコープを入力 (例: @your-org)",
|
||||
"search": "検索",
|
||||
"package_name": "パッケージ名",
|
||||
"description": "説明",
|
||||
"usage": "使用法",
|
||||
"npm": "NPM",
|
||||
"version": "バージョン",
|
||||
"actions": "アクション",
|
||||
"scope_required": "npm スコープを入力してください",
|
||||
"no_packages": "パッケージが見つかりません",
|
||||
"search_error": "パッケージの検索に失敗しました"
|
||||
},
|
||||
"editJson": "JSONを編集",
|
||||
"jsonModeHint": "MCPサーバー設定のJSON表現を編集します。保存する前に、フォーマットが正しいことを確認してください。",
|
||||
"jsonFormatError": "JSONフォーマットエラー",
|
||||
"jsonSaveSuccess": "JSON設定が保存されました。",
|
||||
"jsonSaveError": "JSON設定の保存に失敗しました"
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
@@ -961,10 +939,6 @@
|
||||
"messages.math_engine": "数式エンジン",
|
||||
"messages.metrics": "最初のトークンまでの時間 {{time_first_token_millsec}}ms | トークン速度 {{token_speed}} tok/sec",
|
||||
"messages.model.title": "モデル設定",
|
||||
"messages.navigation": "メッセージナビゲーション",
|
||||
"messages.navigation.anchor": "会話アンカー",
|
||||
"messages.navigation.buttons": "上下ボタン",
|
||||
"messages.navigation.none": "表示しない",
|
||||
"messages.title": "メッセージ設定",
|
||||
"messages.use_serif_font": "セリフフォントを使用",
|
||||
"model": "デフォルトモデル",
|
||||
@@ -985,7 +959,7 @@
|
||||
"models.check.enabled": "開く",
|
||||
"models.check.failed": "失敗",
|
||||
"models.check.keys_status_count": "合格:{{count_passed}}個のキー、不合格:{{count_failed}}個のキー",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 個のモデルが健康チェックを完了しました({{count_partial}} 個のモデルは一部のキーにアクセスできませんでした)、{{count_failed}} 個のモデルは完全にアクセスできませんでした。",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}}個のモデルが成功しました、{{count_failed}}個のモデルが失敗しました、{{count_partial}}個のモデルが一部成功しました",
|
||||
"models.check.no_api_keys": "APIキーが見つかりません。まずAPIキーを追加してください。",
|
||||
"models.check.passed": "成功",
|
||||
"models.check.select_api_key": "使用するAPIキーを選択:",
|
||||
@@ -1027,43 +1001,43 @@
|
||||
"check": "チェック",
|
||||
"check_all_keys": "すべてのキーをチェック",
|
||||
"check_multiple_keys": "複数のAPIキーをチェック",
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilotの認証に失敗しました。",
|
||||
"auth_success": "Github Copilotの認証が成功しました",
|
||||
"auth_success_title": "認証成功",
|
||||
"code_failed": "デバイスコードの取得に失敗しました。再試行してください。",
|
||||
"code_generated_desc": "デバイスコードを下記のブラウザリンクにコピーしてください。",
|
||||
"code_generated_title": "デバイスコードを取得する",
|
||||
"confirm_login": "過度使用すると、あなたのGithubアカウントが停止される可能性があるため、慎重に使用してください!!!!",
|
||||
"confirm_title": "リスク警告",
|
||||
"connect": "GitHubに接続する",
|
||||
"custom_headers": "カスタムリクエストヘッダー",
|
||||
"description": "あなたのGithubアカウントはCopilotを購読する必要があります。",
|
||||
"expand": "展開",
|
||||
"headers_description": "カスタムリクエストヘッダー(JSONフォーマット)",
|
||||
"invalid_json": "JSONフォーマットエラー",
|
||||
"login": "GitHubにログインする",
|
||||
"logout": "GitHubから退出する",
|
||||
"logout_failed": "ログアウトに失敗しました。もう一度お試しください。",
|
||||
"logout_success": "正常にログアウトしました。",
|
||||
"model_setting": "モデル設定",
|
||||
"open_verification_first": "上のリンクをクリックして、確認ページにアクセスしてください。",
|
||||
"rate_limit": "レート制限",
|
||||
"tooltip": "Github Copilot を使用するには、まず Github にログインする必要があります。"
|
||||
},
|
||||
"delete.content": "このプロバイダーを削除してもよろしいですか?",
|
||||
"delete.title": "プロバイダーを削除",
|
||||
"docs_check": "チェック",
|
||||
"docs_more_details": "詳細を確認",
|
||||
"get_api_key": "APIキーを取得",
|
||||
"is_not_support_array_content": "互換モードを有効にする",
|
||||
"no_models": "API接続をチェックする前に、モデルを追加してください",
|
||||
"not_checked": "未チェック",
|
||||
"remove_duplicate_keys": "重複キーを削除",
|
||||
"remove_invalid_keys": "無効なキーを削除",
|
||||
"search": "プロバイダーを検索...",
|
||||
"search_placeholder": "モデルIDまたは名前を検索",
|
||||
"title": "モデルプロバイダー"
|
||||
"title": "モデルプロバイダー",
|
||||
"is_not_support_array_content": "互換モードを有効にする",
|
||||
"copilot": {
|
||||
"tooltip": "Github Copilot を使用するには、まず Github にログインする必要があります。",
|
||||
"description": "あなたのGithubアカウントはCopilotを購読する必要があります。",
|
||||
"login": "GitHubにログインする",
|
||||
"connect": "GitHubに接続する",
|
||||
"logout": "GitHubから退出する",
|
||||
"auth_success_title": "認証成功",
|
||||
"code_generated_title": "デバイスコードを取得する",
|
||||
"code_generated_desc": "デバイスコードを下記のブラウザリンクにコピーしてください。",
|
||||
"code_failed": "デバイスコードの取得に失敗しました。再試行してください。",
|
||||
"auth_success": "Github Copilotの認証が成功しました",
|
||||
"auth_failed": "Github Copilotの認証に失敗しました。",
|
||||
"logout_success": "正常にログアウトしました。",
|
||||
"logout_failed": "ログアウトに失敗しました。もう一度お試しください。",
|
||||
"confirm_title": "リスク警告",
|
||||
"confirm_login": "過度使用すると、あなたのGithubアカウントが停止される可能性があるため、慎重に使用してください!!!!",
|
||||
"rate_limit": "レート制限",
|
||||
"custom_headers": "カスタムリクエストヘッダー",
|
||||
"headers_description": "カスタムリクエストヘッダー(JSONフォーマット)",
|
||||
"expand": "展開",
|
||||
"model_setting": "モデル設定",
|
||||
"invalid_json": "JSONフォーマットエラー",
|
||||
"open_verification_first": "上のリンクをクリックして、確認ページにアクセスしてください。"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
@@ -1078,9 +1052,9 @@
|
||||
"quickAssistant": {
|
||||
"click_tray_to_show": "トレイアイコンをクリックして起動",
|
||||
"enable_quick_assistant": "クイックアシスタントを有効にする",
|
||||
"read_clipboard_at_startup": "起動時にクリップボードを読み取る",
|
||||
"title": "クイックアシスタント",
|
||||
"use_shortcut_to_show": "トレイアイコンを右クリックするか、ショートカットキーで起動できます"
|
||||
"use_shortcut_to_show": "トレイアイコンを右クリックするか、ショートカットキーで起動できます",
|
||||
"read_clipboard_at_startup": "起動時にクリップボードを読み取る"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
@@ -1117,18 +1091,14 @@
|
||||
"topic.position.left": "左",
|
||||
"topic.position.right": "右",
|
||||
"topic.show.time": "トピックの時間を表示",
|
||||
"tray.onclose": "閉じるときにトレイに最小化",
|
||||
"tray.show": "トレイアイコンを表示",
|
||||
"tray.title": "トレイ",
|
||||
"tray.title": "システムトレイアイコンを有効にする",
|
||||
"websearch": {
|
||||
"blacklist": "ブラックリスト",
|
||||
"blacklist_description": "以下のウェブサイトの結果は検索結果に表示されません",
|
||||
"blacklist_tooltip": "以下の形式を使用してください(改行区切り)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "チェック",
|
||||
"check_failed": "検証に失敗しました",
|
||||
"check_success": "検証に成功しました",
|
||||
"enhance_mode": "検索強化モード",
|
||||
"enhance_mode_tooltip": "デフォルトモデルを使用して問題から検索キーワードを抽出し、検索を実行します",
|
||||
"check_failed": "検証に失敗しました",
|
||||
"get_api_key": "APIキーを取得",
|
||||
"no_provider_selected": "検索サービスプロバイダーを選択してから再確認してください。",
|
||||
"search_max_result": "検索結果の数",
|
||||
@@ -1136,6 +1106,8 @@
|
||||
"search_provider_placeholder": "検索サービスプロバイダーを選択する",
|
||||
"search_result_default": "デフォルト",
|
||||
"search_with_time": "日付を含む検索",
|
||||
"enhance_mode": "検索強化モード",
|
||||
"enhance_mode_tooltip": "デフォルトモデルを使用して問題から検索キーワードを抽出し、検索を実行します",
|
||||
"tavily": {
|
||||
"api_key": "Tavily API キー",
|
||||
"api_key.placeholder": "Tavily API キーを入力してください",
|
||||
|
||||
@@ -96,16 +96,6 @@
|
||||
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
||||
"default.name": "Ассистент по умолчанию",
|
||||
"default.topic.name": "Топик по умолчанию",
|
||||
"history": {
|
||||
"assistant_node": "Ассистент",
|
||||
"click_to_navigate": "Перейти к сообщению",
|
||||
"coming_soon": "График работы чата скоро появится",
|
||||
"no_messages": "Сообщения не найдены",
|
||||
"start_conversation": "Начните диалог, чтобы просмотреть график работы чата",
|
||||
"title": "История чата",
|
||||
"user_node": "Пользователь",
|
||||
"view_full_content": "Показать полное содержимое"
|
||||
},
|
||||
"input.auto_resize": "Автоматическая высота",
|
||||
"input.clear": "Очистить {{Command}}",
|
||||
"input.clear.content": "Хотите очистить все сообщения текущего топика?",
|
||||
@@ -115,8 +105,6 @@
|
||||
"input.estimated_tokens.tip": "Затраты токенов",
|
||||
"input.expand": "Развернуть",
|
||||
"input.file_not_supported": "Модель не поддерживает этот тип файла",
|
||||
"input.generate_image": "Сгенерировать изображение",
|
||||
"input.generate_image_not_supported": "Модель не поддерживает генерацию изображений.",
|
||||
"input.knowledge_base": "База знаний",
|
||||
"input.new.context": "Очистить контекст {{Command}}",
|
||||
"input.new_topic": "Новый топик {{Command}}",
|
||||
@@ -138,13 +126,6 @@
|
||||
"message.quote": "Цитата",
|
||||
"message.regenerate.model": "Переключить модель",
|
||||
"message.useful": "Полезно",
|
||||
"navigation": {
|
||||
"first": "Уже первое сообщение",
|
||||
"history": "История чата",
|
||||
"last": "Уже последнее сообщение",
|
||||
"next": "Следующее сообщение",
|
||||
"prev": "Предыдущее сообщение"
|
||||
},
|
||||
"resend": "Переотправить",
|
||||
"save": "Сохранить",
|
||||
"settings.code_collapsible": "Блок кода свернут",
|
||||
@@ -176,42 +157,37 @@
|
||||
"topics.edit.placeholder": "Введите новый заголовок",
|
||||
"topics.edit.title": "Редактировать заголовок",
|
||||
"topics.export.image": "Экспорт как изображение",
|
||||
"topics.export.joplin": "Экспорт в Joplin",
|
||||
"topics.export.md": "Экспорт как markdown",
|
||||
"topics.export.notion": "Экспорт в Notion",
|
||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||
"topics.export.obsidian_atributes": "Настроить атрибуты заметки",
|
||||
"topics.export.obsidian_btn": "Подтвердить",
|
||||
"topics.export.obsidian_created": "Дата создания",
|
||||
"topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания",
|
||||
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
||||
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
||||
"topics.export.obsidian_operate": "Метод обработки",
|
||||
"topics.export.obsidian_operate_append": "Добавить в конец",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)",
|
||||
"topics.export.obsidian_operate_placeholder": "Пожалуйста, выберите метод обработки",
|
||||
"topics.export.obsidian_operate_prepend": "Добавить в начало",
|
||||
"topics.export.obsidian_source": "Источник",
|
||||
"topics.export.obsidian_source_placeholder": "Пожалуйста, введите источник",
|
||||
"topics.export.obsidian_tags": "Тэги",
|
||||
"topics.export.obsidian_tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке",
|
||||
"topics.export.obsidian_title": "Заголовок",
|
||||
"topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок",
|
||||
"topics.export.obsidian_title_required": "Заголовок не может быть пустым",
|
||||
"topics.export.title": "Экспорт",
|
||||
"topics.export.word": "Экспорт как Word",
|
||||
"topics.export.yuque": "Экспорт в Yuque",
|
||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
||||
"topics.export.obsidian_fetch_failed": "Не удалось получить структуру файлов Obsidian",
|
||||
"topics.export.obsidian_select_folder": "Выберите папку Obsidian",
|
||||
"topics.export.obsidian_select_folder.btn": "Определить",
|
||||
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
||||
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||
"topics.export.obsidian_show_md_files": "Показать файлы MD",
|
||||
"topics.export.obsidian_selected_path": "Выбранный путь",
|
||||
"topics.export.joplin": "Экспорт в Joplin",
|
||||
"topics.list": "Список топиков",
|
||||
"topics.move_to": "Переместить в",
|
||||
"topics.new": "Новый топик",
|
||||
"topics.pinned": "Закрепленные темы",
|
||||
"topics.prompt": "Тематические подсказки",
|
||||
"topics.prompt.edit.title": "Редактировать подсказки темы",
|
||||
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
||||
"topics.title": "Топики",
|
||||
"topics.unpinned": "Открепленные темы",
|
||||
"translate": "Перевести"
|
||||
"topics.new": "Новый топик",
|
||||
"translate": "Перевести",
|
||||
"navigation": {
|
||||
"prev": "Предыдущее сообщение",
|
||||
"next": "Следующее сообщение",
|
||||
"first": "Уже первое сообщение",
|
||||
"last": "Уже последнее сообщение"
|
||||
}
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "Свернуть",
|
||||
@@ -221,7 +197,6 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "Добавить",
|
||||
"advanced_settings": "Дополнительные настройки",
|
||||
"and": "и",
|
||||
"assistant": "Ассистент",
|
||||
"avatar": "Аватар",
|
||||
@@ -230,8 +205,6 @@
|
||||
"chat": "Чат",
|
||||
"clear": "Очистить",
|
||||
"close": "Закрыть",
|
||||
"confirm": "Подтверждение",
|
||||
"copied": "Скопировано",
|
||||
"copy": "Копировать",
|
||||
"cut": "Вырезать",
|
||||
"default": "По умолчанию",
|
||||
@@ -241,7 +214,6 @@
|
||||
"download": "Скачать",
|
||||
"duplicate": "Дублировать",
|
||||
"edit": "Редактировать",
|
||||
"expand": "Развернуть",
|
||||
"footnote": "Цитируемый контент",
|
||||
"footnotes": "Сноски",
|
||||
"fullscreen": "Вы вошли в полноэкранный режим. Нажмите F11 для выхода",
|
||||
@@ -249,7 +221,6 @@
|
||||
"language": "Язык",
|
||||
"model": "Модель",
|
||||
"models": "Модели",
|
||||
"more": "Ещё",
|
||||
"name": "Имя",
|
||||
"paste": "Вставить",
|
||||
"prompt": "Промпт",
|
||||
@@ -262,7 +233,12 @@
|
||||
"select": "Выбрать",
|
||||
"topics": "Топики",
|
||||
"warning": "Предупреждение",
|
||||
"you": "Вы"
|
||||
"you": "Вы",
|
||||
"confirm": "Подтверждение",
|
||||
"copied": "Скопировано",
|
||||
"more": "Ещё",
|
||||
"advanced_settings": "Дополнительные настройки",
|
||||
"expand": "Развернуть"
|
||||
},
|
||||
"docs": {
|
||||
"title": "Документация"
|
||||
@@ -320,12 +296,6 @@
|
||||
"title": "Файлы",
|
||||
"type": "Тип"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
"keep_alive_time.placeholder": "Минуты",
|
||||
"keep_alive_time.title": "Время жизни модели",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Продолжить чат",
|
||||
"locate.message": "Найти сообщение",
|
||||
@@ -395,13 +365,13 @@
|
||||
"threshold_too_large_or_small": "Порог не может быть больше 1 или меньше 0",
|
||||
"threshold_tooltip": "Используется для оценки соответствия между пользовательским вопросом и содержимым в базе знаний (0-1)",
|
||||
"title": "База знаний",
|
||||
"topN": "Количество возвращаемых результатов",
|
||||
"topN__too_large_or_small": "Количество возвращаемых результатов не может быть больше 100 или меньше 1.",
|
||||
"topN_placeholder": "Не установлено",
|
||||
"topN_tooltip": "Количество возвращаемых совпадений; чем больше значение, тем больше совпадений, но и потребление токенов тоже возрастает.",
|
||||
"url_added": "URL добавлен",
|
||||
"url_placeholder": "Введите URL, несколько URL через Enter",
|
||||
"urls": "URL-адреса"
|
||||
"urls": "URL-адреса",
|
||||
"topN": "Количество возвращаемых результатов",
|
||||
"topN_placeholder": "Не установлено",
|
||||
"topN__too_large_or_small": "Количество возвращаемых результатов не может быть больше 100 или меньше 1.",
|
||||
"topN_tooltip": "Количество возвращаемых совпадений; чем больше значение, тем больше совпадений, но и потребление токенов тоже возрастает."
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "Арабский",
|
||||
@@ -438,23 +408,29 @@
|
||||
},
|
||||
"title": "Диаграмма Mermaid"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
"keep_alive_time.placeholder": "Минуты",
|
||||
"keep_alive_time.title": "Время жизни модели",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"message": {
|
||||
"attachments": {
|
||||
"pasted_text": "Вырезанный текст",
|
||||
"pasted_image": "Вырезанное изображение"
|
||||
},
|
||||
"api.check.model.title": "Выберите модель для проверки",
|
||||
"api.connection.failed": "Соединение не удалось",
|
||||
"api.connection.success": "Соединение успешно",
|
||||
"assistant.added.content": "Ассистент успешно добавлен",
|
||||
"attachments": {
|
||||
"pasted_image": "Вырезанное изображение",
|
||||
"pasted_text": "Вырезанный текст"
|
||||
},
|
||||
"backup.failed": "Создание резервной копии не удалось",
|
||||
"backup.start.success": "Создание резервной копии начато",
|
||||
"backup.success": "Резервная копия успешно создана",
|
||||
"chat.completion.paused": "Завершение чата приостановлено",
|
||||
"citations": "Источники",
|
||||
"copied": "Скопировано!",
|
||||
"copy.failed": "Не удалось скопировать",
|
||||
"copy.success": "Скопировано!",
|
||||
"copy.failed": "Не удалось скопировать",
|
||||
"error.chunk_overlap_too_large": "Перекрытие фрагментов не может быть больше размера фрагмента.",
|
||||
"error.dimension_too_large": "Размер содержимого слишком велик",
|
||||
"error.enter.api.host": "Пожалуйста, введите ваш API хост",
|
||||
@@ -467,20 +443,20 @@
|
||||
"error.invalid.enter.model": "Пожалуйста, выберите модель",
|
||||
"error.invalid.proxy.url": "Неверный URL прокси",
|
||||
"error.invalid.webdav": "Неверные настройки WebDAV",
|
||||
"error.joplin.export": "Не удалось экспортировать в Joplin, пожалуйста, убедитесь, что Joplin запущен и проверьте состояние подключения или настройки",
|
||||
"error.joplin.no_config": "Joplin Authorization Token или URL не настроен",
|
||||
"error.markdown.export.preconf": "Не удалось экспортировать файл Markdown в предуказанный путь",
|
||||
"error.markdown.export.specified": "Не удалось экспортировать файл Markdown",
|
||||
"error.notion.export": "Ошибка экспорта в Notion, пожалуйста, проверьте состояние подключения и настройки в документации",
|
||||
"error.notion.no_api_key": "Notion ApiKey или Notion DatabaseID не настроен",
|
||||
"error.yuque.export": "Ошибка экспорта в Yuque, пожалуйста, проверьте состояние подключения и настройки в документации",
|
||||
"error.yuque.no_config": "Yuque Token или Yuque Url не настроен",
|
||||
"error.joplin.no_config": "Joplin Authorization Token или URL не настроен",
|
||||
"error.joplin.export": "Не удалось экспортировать в Joplin, пожалуйста, убедитесь, что Joplin запущен и проверьте состояние подключения или настройки",
|
||||
"group.delete.content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
|
||||
"group.delete.title": "Удалить группу сообщений",
|
||||
"ignore.knowledge.base": "Режим сети включен, игнорировать базу знаний",
|
||||
"info.notion.block_reach_limit": "Диалог слишком длинный, экспортируется в Notion по страницам",
|
||||
"loading.notion.exporting_progress": "Экспорт в Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "Подготовка к экспорту в Notion...",
|
||||
"loading.notion.exporting_progress": "Экспорт в Notion ({{current}}/{{total}})...",
|
||||
"mention.title": "Переключить модель ответа",
|
||||
"message.code_style": "Стиль кода",
|
||||
"message.delete.content": "Вы уверены, что хотите удалить это сообщение?",
|
||||
@@ -503,22 +479,22 @@
|
||||
"restore.success": "Успешно восстановлено",
|
||||
"save.success.title": "Успешно сохранено",
|
||||
"searching": "Поиск в Интернете...",
|
||||
"success.joplin.export": "Успешный экспорт в Joplin",
|
||||
"success.markdown.export.preconf": "Файл Markdown успешно экспортирован в предуказанный путь",
|
||||
"success.markdown.export.specified": "Файл Markdown успешно экспортирован",
|
||||
"success.notion.export": "Успешный экспорт в Notion",
|
||||
"success.yuque.export": "Успешный экспорт в Yuque",
|
||||
"success.joplin.export": "Успешный экспорт в Joplin",
|
||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||
"tools": {
|
||||
"completed": "Завершено",
|
||||
"invoking": "Вызов"
|
||||
},
|
||||
"topic.added": "Новый топик добавлен",
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
"upgrade.success.title": "Обновление успешно",
|
||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.",
|
||||
"tools": {
|
||||
"invoking": "Вызов",
|
||||
"completed": "Завершено"
|
||||
}
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "Добавить в боковую панель",
|
||||
@@ -557,7 +533,7 @@
|
||||
"embedding": "Встраиваемые",
|
||||
"embedding_model": "Встраиваемые модели",
|
||||
"embedding_model_tooltip": "Добавьте в настройки->модель сервиса->управление",
|
||||
"function_calling": "Вызов функции",
|
||||
"free": "Бесплатные",
|
||||
"no_matches": "Нет доступных моделей",
|
||||
"parameter_name": "Имя параметра",
|
||||
"parameter_type": {
|
||||
@@ -567,22 +543,22 @@
|
||||
"string": "Текст"
|
||||
},
|
||||
"pinned": "Закреплено",
|
||||
"rerank_model": "Модель переупорядочивания",
|
||||
"rerank_model_support_provider": "Текущая модель переупорядочивания поддерживается только некоторыми поставщиками ({{provider}})",
|
||||
"rerank_model_tooltip": "В настройках -> Служба модели нажмите кнопку \"Управление\", чтобы добавить.",
|
||||
"reasoning": "Рассуждение",
|
||||
"search": "Поиск моделей...",
|
||||
"stream_output": "Потоковый вывод",
|
||||
"function_calling": "Вызов функции",
|
||||
"type": {
|
||||
"embedding": "Встраиваемые",
|
||||
"free": "Бесплатные",
|
||||
"function_calling": "Инструкция",
|
||||
"reasoning": "Рассуждение",
|
||||
"rerank": "Переупорядочить",
|
||||
"select": "Выберите тип модели",
|
||||
"text": "Текст",
|
||||
"vision": "Визуальные",
|
||||
"websearch": "Веб-поисковые"
|
||||
}
|
||||
"vision": "Изображение",
|
||||
"function_calling": "Вызов функции"
|
||||
},
|
||||
"vision": "Визуальные",
|
||||
"websearch": "Веб-поисковые",
|
||||
"rerank_model": "Модель переупорядочивания",
|
||||
"rerank_model_tooltip": "В настройках -> Служба модели нажмите кнопку \"Управление\", чтобы добавить."
|
||||
},
|
||||
"navbar": {
|
||||
"expand": "Развернуть диалоговое окно",
|
||||
@@ -635,12 +611,10 @@
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "Baichuan",
|
||||
"baidu-cloud": "Baidu Cloud",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "Alibaba Cloud",
|
||||
"deepseek": "DeepSeek",
|
||||
"dmxapi": "DMXAPI",
|
||||
@@ -649,7 +623,6 @@
|
||||
"gemini": "Gemini",
|
||||
"gitee-ai": "Gitee AI",
|
||||
"github": "GitHub Models",
|
||||
"gpustack": "GPUStack",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
@@ -679,7 +652,9 @@
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"voyageai": "Voyage AI"
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "Вы уверены, что хотите восстановить данные?",
|
||||
@@ -741,30 +716,15 @@
|
||||
"data.title": "Каталог данных",
|
||||
"hour_interval_one": "{{count}} час",
|
||||
"hour_interval_other": "{{count}} часов",
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "Проверить",
|
||||
"empty_token": "Сначала введите токен Joplin",
|
||||
"empty_url": "Сначала введите URL Joplin",
|
||||
"fail": "Не удалось проверить подключение к Joplin",
|
||||
"success": "Подключение к Joplin успешно проверено"
|
||||
},
|
||||
"help": "Включите Joplin опцию, проверьте порт и скопируйте токен",
|
||||
"title": "Настройка Joplin",
|
||||
"token": "Токен Joplin",
|
||||
"token_placeholder": "Введите токен Joplin",
|
||||
"url": "URL Joplin",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "Если включено, при экспорте в Markdown для обозначения формул LaTeX будет принудительно использоваться $$. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"markdown_export.force_dollar_math.title": "Принудительно использовать $$ для формул LaTeX",
|
||||
"markdown_export.help": "Если указано, файлы будут автоматически сохраняться в этот путь; в противном случае появится диалоговое окно сохранения.",
|
||||
"minute_interval_one": "{{count}} минута",
|
||||
"minute_interval_other": "{{count}} минут",
|
||||
"markdown_export.title": "Экспорт в Markdown",
|
||||
"markdown_export.path": "Путь экспорта по умолчанию",
|
||||
"markdown_export.path_placeholder": "Путь экспорта",
|
||||
"markdown_export.select": "Выбрать",
|
||||
"markdown_export.title": "Экспорт в Markdown",
|
||||
"minute_interval_one": "{{count}} минута",
|
||||
"minute_interval_other": "{{count}} минут",
|
||||
"markdown_export.help": "Если указано, файлы будут автоматически сохраняться в этот путь; в противном случае появится диалоговое окно сохранения.",
|
||||
"markdown_export.force_dollar_math.title": "Принудительно использовать $$ для формул LaTeX",
|
||||
"markdown_export.force_dollar_math.help": "Если включено, при экспорте в Markdown для обозначения формул LaTeX будет принудительно использоваться $$. Примечание: Эта опция также влияет на все методы экспорта через Markdown, такие как Notion, Yuque и т.д.",
|
||||
"notion.api_key": "Ключ API Notion",
|
||||
"notion.api_key_placeholder": "Введите ключ API Notion",
|
||||
"notion.auto_split": "Автоматическое разбиение на страницы при экспорте диалога",
|
||||
@@ -786,22 +746,11 @@
|
||||
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
|
||||
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
|
||||
"notion.title": "Настройки Notion",
|
||||
"obsidian": {
|
||||
"folder": "Папка",
|
||||
"folder_placeholder": "Пожалуйста, введите имя папки",
|
||||
"tags": "Глобальные Теги",
|
||||
"tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.",
|
||||
"title": "Конфигурация Obsidian",
|
||||
"vault": "Хранилище",
|
||||
"vault_placeholder": "Пожалуйста, введите имя хранилища"
|
||||
},
|
||||
"title": "Настройки данных",
|
||||
"webdav": {
|
||||
"autoSync": "Автоматическое резервное копирование",
|
||||
"autoSync.off": "Выключено",
|
||||
"backup.button": "Резервное копирование на WebDAV",
|
||||
"backup.modal.filename.placeholder": "Введите имя файла резервной копии",
|
||||
"backup.modal.title": "Резервное копирование на WebDAV",
|
||||
"host": "Хост WebDAV",
|
||||
"host.placeholder": "http://localhost:8080",
|
||||
"hour_interval_one": "{{count}} час",
|
||||
@@ -814,16 +763,18 @@
|
||||
"path": "Путь WebDAV",
|
||||
"path.placeholder": "/backup",
|
||||
"restore.button": "Восстановление с WebDAV",
|
||||
"restore.confirm.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?",
|
||||
"restore.confirm.title": "Подтверждение восстановления",
|
||||
"restore.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?",
|
||||
"restore.modal.select.placeholder": "Выберите файл резервной копии для восстановления",
|
||||
"restore.modal.title": "Восстановление с WebDAV",
|
||||
"restore.title": "Восстановление с WebDAV",
|
||||
"syncError": "Ошибка резервного копирования",
|
||||
"syncStatus": "Статус резервного копирования",
|
||||
"title": "WebDAV",
|
||||
"user": "Пользователь WebDAV"
|
||||
"user": "Пользователь WebDAV",
|
||||
"backup.modal.title": "Резервное копирование на WebDAV",
|
||||
"backup.modal.filename.placeholder": "Введите имя файла резервной копии",
|
||||
"restore.modal.title": "Восстановление с WebDAV",
|
||||
"restore.modal.select.placeholder": "Выберите файл резервной копии для восстановления",
|
||||
"restore.confirm.title": "Подтверждение восстановления",
|
||||
"restore.confirm.content": "Восстановление с WebDAV перезапишет текущие данные, продолжить?"
|
||||
},
|
||||
"yuque": {
|
||||
"check": {
|
||||
@@ -839,6 +790,36 @@
|
||||
"title": "Настройка Yuque",
|
||||
"token": "Токен Yuque",
|
||||
"token_placeholder": "Введите токен Yuque"
|
||||
},
|
||||
"obsidian": {
|
||||
"check": {
|
||||
"button": "Проверить",
|
||||
"empty_url": "Сначала введите URL REST API Obsidian",
|
||||
"empty_api_key": "Сначала введите API Key Obsidian",
|
||||
"fail": "Не удалось проверить подключение к Obsidian",
|
||||
"success": "Подключение к Obsidian успешно проверено"
|
||||
},
|
||||
"help": "Сначала установите плагин Local REST API Obsidian, затем получите API Key Obsidian",
|
||||
"url": "URL базы знаний Obsidian",
|
||||
"url_placeholder": "http://127.0.0.1:27123/",
|
||||
"title": "Настройка Obsidian",
|
||||
"api_key": "API Key Obsidian",
|
||||
"api_key_placeholder": "Введите API Key Obsidian"
|
||||
},
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "Проверить",
|
||||
"empty_url": "Сначала введите URL Joplin",
|
||||
"empty_token": "Сначала введите токен Joplin",
|
||||
"fail": "Не удалось проверить подключение к Joplin",
|
||||
"success": "Подключение к Joplin успешно проверено"
|
||||
},
|
||||
"title": "Настройка Joplin",
|
||||
"help": "Включите Joplin опцию, проверьте порт и скопируйте токен",
|
||||
"url": "URL Joplin",
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"token": "Токен Joplin",
|
||||
"token_placeholder": "Введите токен Joplin"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "Настройки ассистентов",
|
||||
@@ -884,15 +865,12 @@
|
||||
"input.target_language.english": "Английский",
|
||||
"input.target_language.japanese": "Японский",
|
||||
"input.target_language.russian": "Русский",
|
||||
"launch.onboot": "Автозапуск при включении",
|
||||
"launch.title": "Запуск",
|
||||
"launch.totray": "Свернуть в трей при запуске",
|
||||
"mcp": {
|
||||
"actions": "Действия",
|
||||
"active": "Активен",
|
||||
"addError": "Ошибка добавления сервера",
|
||||
"addServer": "Добавить сервер",
|
||||
"addSuccess": "Сервер успешно добавлен",
|
||||
"addError": "Ошибка добавления сервера",
|
||||
"args": "Аргументы",
|
||||
"argsTooltip": "Каждый аргумент с новой строки",
|
||||
"baseUrlTooltip": "Адрес удаленного URL",
|
||||
@@ -903,49 +881,49 @@
|
||||
"confirmDeleteMessage": "Вы уверены, что хотите удалить этот сервер?",
|
||||
"deleteError": "Не удалось удалить сервер",
|
||||
"deleteSuccess": "Сервер успешно удален",
|
||||
"dependenciesInstall": "Установить зависимости",
|
||||
"dependenciesInstalling": "Установка зависимостей...",
|
||||
"description": "Описание",
|
||||
"duplicateName": "Сервер с таким именем уже существует",
|
||||
"editJson": "Редактировать JSON",
|
||||
"editServer": "Редактировать сервер",
|
||||
"env": "Переменные окружения",
|
||||
"envTooltip": "Формат: KEY=value, по одной на строку",
|
||||
"findMore": "Найти больше MCP серверов",
|
||||
"install": "Установить",
|
||||
"installError": "Не удалось установить зависимости",
|
||||
"installSuccess": "Зависимости успешно установлены",
|
||||
"jsonFormatError": "Ошибка форматирования JSON",
|
||||
"jsonModeHint": "Редактируйте JSON-форматирование конфигурации сервера MCP. Перед сохранением убедитесь, что формат правильный.",
|
||||
"jsonSaveError": "Не удалось сохранить конфигурацию JSON",
|
||||
"jsonSaveSuccess": "JSON конфигурация сохранена",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"name": "Имя",
|
||||
"nameRequired": "Пожалуйста, введите имя сервера",
|
||||
"noServers": "Серверы не настроены",
|
||||
"npx_list": {
|
||||
"actions": "Действия",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
"description": "Описание",
|
||||
"no_packages": "Ничего не найдено",
|
||||
"npm": "NPM",
|
||||
"package_name": "Имя пакета",
|
||||
"scope_placeholder": "Введите область npm (например, @your-org)",
|
||||
"scope_required": "Пожалуйста, введите область npm",
|
||||
"search": "Поиск",
|
||||
"search_error": "Ошибка поиска",
|
||||
"title": "Список пакетов NPX",
|
||||
"usage": "Использование",
|
||||
"version": "Версия"
|
||||
},
|
||||
"serverPlural": "серверы",
|
||||
"serverSingular": "сервер",
|
||||
"title": "Серверы MCP",
|
||||
"toggleError": "Переключение не удалось",
|
||||
"type": "Тип",
|
||||
"updateError": "Ошибка обновления сервера",
|
||||
"updateSuccess": "Сервер успешно обновлен",
|
||||
"url": "URL"
|
||||
"updateError": "Ошибка обновления сервера",
|
||||
"url": "URL",
|
||||
"toggleError": "Переключение не удалось",
|
||||
"dependenciesInstalling": "Установка зависимостей...",
|
||||
"dependenciesInstall": "Установить зависимости",
|
||||
"installSuccess": "Зависимости успешно установлены",
|
||||
"installError": "Не удалось установить зависимости",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"install": "Установить",
|
||||
"npx_list": {
|
||||
"title": "Список пакетов NPX",
|
||||
"desc": "Поиск и добавление npm пакетов в качестве MCP серверов",
|
||||
"scope_placeholder": "Введите область npm (например, @your-org)",
|
||||
"search": "Поиск",
|
||||
"package_name": "Имя пакета",
|
||||
"description": "Описание",
|
||||
"usage": "Использование",
|
||||
"npm": "NPM",
|
||||
"version": "Версия",
|
||||
"actions": "Действия",
|
||||
"scope_required": "Пожалуйста, введите область npm",
|
||||
"no_packages": "Ничего не найдено",
|
||||
"search_error": "Ошибка поиска"
|
||||
},
|
||||
"editJson": "Редактировать JSON",
|
||||
"jsonModeHint": "Редактируйте JSON-форматирование конфигурации сервера MCP. Перед сохранением убедитесь, что формат правильный.",
|
||||
"jsonFormatError": "Ошибка форматирования JSON",
|
||||
"jsonSaveSuccess": "JSON конфигурация сохранена",
|
||||
"jsonSaveError": "Не удалось сохранить конфигурацию JSON"
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
@@ -961,10 +939,6 @@
|
||||
"messages.math_engine": "Математический движок",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms до первого токена | {{token_speed}} tok/sec",
|
||||
"messages.model.title": "Настройки модели",
|
||||
"messages.navigation": "Навигация сообщений",
|
||||
"messages.navigation.anchor": "Диалог анкор",
|
||||
"messages.navigation.buttons": "Кнопки пагинации",
|
||||
"messages.navigation.none": "Не показывать",
|
||||
"messages.title": "Настройки сообщений",
|
||||
"messages.use_serif_font": "Использовать serif шрифт",
|
||||
"model": "Модель по умолчанию",
|
||||
@@ -985,7 +959,7 @@
|
||||
"models.check.enabled": "Включено",
|
||||
"models.check.failed": "Не прошло",
|
||||
"models.check.keys_status_count": "Прошло: {{count_passed}} ключей, Не прошло: {{count_failed}} ключей",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} моделей прошли проверку состояния (из них {{count_partial}} моделей недоступны с некоторыми ключами), {{count_failed}} моделей полностью недоступны.",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} модели прошли все ключи, {{count_failed}} модели не прошли все ключи, {{count_partial}} модели не прошли некоторые ключи",
|
||||
"models.check.no_api_keys": "API ключи не найдены, пожалуйста, добавьте API ключи.",
|
||||
"models.check.passed": "Прошло",
|
||||
"models.check.select_api_key": "Выберите API ключ для использования:",
|
||||
@@ -1027,43 +1001,43 @@
|
||||
"check": "Проверить",
|
||||
"check_all_keys": "Проверить все ключи",
|
||||
"check_multiple_keys": "Проверить несколько ключей API",
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot认证失败",
|
||||
"auth_success": "Github Copilot认证成功",
|
||||
"auth_success_title": "Аутентификация успешна",
|
||||
"code_failed": "Получение кода устройства не удалось, пожалуйста, попробуйте еще раз.",
|
||||
"code_generated_desc": "Пожалуйста, скопируйте код устройства в приведенную ниже ссылку браузера.",
|
||||
"code_generated_title": "Получить код устройства",
|
||||
"confirm_login": "Чрезмерное использование может привести к блокировке вашего Github, будьте осторожны!!!!",
|
||||
"confirm_title": "Предупреждение о рисках",
|
||||
"connect": "Подключить Github",
|
||||
"custom_headers": "Пользовательские заголовки запроса",
|
||||
"description": "Ваша учетная запись Github должна подписаться на Copilot.",
|
||||
"expand": "развернуть",
|
||||
"headers_description": "Пользовательские заголовки запроса (формат json)",
|
||||
"invalid_json": "Ошибка формата JSON",
|
||||
"login": "Войти в Github",
|
||||
"logout": "Выйти из Github",
|
||||
"logout_failed": "Не удалось выйти, пожалуйста, повторите попытку.",
|
||||
"logout_success": "Успешно вышел",
|
||||
"model_setting": "Настройки модели",
|
||||
"open_verification_first": "Пожалуйста, сначала щелкните по ссылке выше, чтобы перейти на страницу проверки.",
|
||||
"rate_limit": "Ограничение скорости",
|
||||
"tooltip": "Для использования Github Copilot необходимо сначала войти в Github."
|
||||
},
|
||||
"delete.content": "Вы уверены, что хотите удалить этот провайдер?",
|
||||
"delete.title": "Удалить провайдер",
|
||||
"docs_check": "Проверить",
|
||||
"docs_more_details": "для получения дополнительной информации",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"is_not_support_array_content": "Включить совместимый режим",
|
||||
"no_models": "Пожалуйста, добавьте модели перед проверкой соединения с API",
|
||||
"not_checked": "Не проверено",
|
||||
"remove_duplicate_keys": "Удалить дубликаты ключей",
|
||||
"remove_invalid_keys": "Удалить недействительные ключи",
|
||||
"search": "Поиск поставщиков...",
|
||||
"search_placeholder": "Поиск по ID или имени модели",
|
||||
"title": "Провайдеры моделей"
|
||||
"title": "Провайдеры моделей",
|
||||
"is_not_support_array_content": "Включить совместимый режим",
|
||||
"copilot": {
|
||||
"tooltip": "Для использования Github Copilot необходимо сначала войти в Github.",
|
||||
"description": "Ваша учетная запись Github должна подписаться на Copilot.",
|
||||
"login": "Войти в Github",
|
||||
"connect": "Подключить Github",
|
||||
"logout": "Выйти из Github",
|
||||
"auth_success_title": "Аутентификация успешна",
|
||||
"code_generated_title": "Получить код устройства",
|
||||
"code_generated_desc": "Пожалуйста, скопируйте код устройства в приведенную ниже ссылку браузера.",
|
||||
"code_failed": "Получение кода устройства не удалось, пожалуйста, попробуйте еще раз.",
|
||||
"auth_success": "Github Copilot认证成功",
|
||||
"auth_failed": "Github Copilot认证失败",
|
||||
"logout_success": "Успешно вышел",
|
||||
"logout_failed": "Не удалось выйти, пожалуйста, повторите попытку.",
|
||||
"confirm_title": "Предупреждение о рисках",
|
||||
"confirm_login": "Чрезмерное использование может привести к блокировке вашего Github, будьте осторожны!!!!",
|
||||
"rate_limit": "Ограничение скорости",
|
||||
"custom_headers": "Пользовательские заголовки запроса",
|
||||
"headers_description": "Пользовательские заголовки запроса (формат json)",
|
||||
"expand": "развернуть",
|
||||
"model_setting": "Настройки модели",
|
||||
"invalid_json": "Ошибка формата JSON",
|
||||
"open_verification_first": "Пожалуйста, сначала щелкните по ссылке выше, чтобы перейти на страницу проверки."
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
@@ -1078,9 +1052,9 @@
|
||||
"quickAssistant": {
|
||||
"click_tray_to_show": "Нажмите на иконку трея для запуска",
|
||||
"enable_quick_assistant": "Включить быстрый помощник",
|
||||
"read_clipboard_at_startup": "Чтение буфера обмена при запуске",
|
||||
"title": "Быстрый помощник",
|
||||
"use_shortcut_to_show": "Нажмите на иконку трея или используйте горячие клавиши для запуска"
|
||||
"use_shortcut_to_show": "Нажмите на иконку трея или используйте горячие клавиши для запуска",
|
||||
"read_clipboard_at_startup": "Чтение буфера обмена при запуске"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "Действие",
|
||||
@@ -1117,18 +1091,14 @@
|
||||
"topic.position.left": "Слева",
|
||||
"topic.position.right": "Справа",
|
||||
"topic.show.time": "Показывать время топика",
|
||||
"tray.onclose": "Свернуть в трей при закрытии",
|
||||
"tray.show": "Показать значок в трее",
|
||||
"tray.title": "Трей",
|
||||
"tray.title": "Включить значок системного трея",
|
||||
"websearch": {
|
||||
"blacklist": "Черный список",
|
||||
"blacklist_description": "Результаты из следующих веб-сайтов не будут отображаться в результатах поиска",
|
||||
"blacklist_tooltip": "Пожалуйста, используйте следующий формат (разделенный переносами строк)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "проверка",
|
||||
"check_failed": "Проверка не прошла",
|
||||
"check_success": "Проверка успешна",
|
||||
"enhance_mode": "Режим улучшения поиска",
|
||||
"enhance_mode_tooltip": "Используйте модель по умолчанию для извлечения ключевых слов из проблемы и поиска",
|
||||
"check_failed": "Проверка не прошла",
|
||||
"get_api_key": "Получить ключ API",
|
||||
"no_provider_selected": "Пожалуйста, выберите поставщика поисковых услуг, затем проверьте.",
|
||||
"search_max_result": "Количество результатов поиска",
|
||||
@@ -1136,6 +1106,8 @@
|
||||
"search_provider_placeholder": "Выберите поставщика поисковых услуг",
|
||||
"search_result_default": "По умолчанию",
|
||||
"search_with_time": "Поиск, содержащий дату",
|
||||
"enhance_mode": "Режим улучшения поиска",
|
||||
"enhance_mode_tooltip": "Используйте модель по умолчанию для извлечения ключевых слов из проблемы и поиска",
|
||||
"tavily": {
|
||||
"api_key": "Ключ API Tavily",
|
||||
"api_key.placeholder": "Введите ключ API Tavily",
|
||||
|
||||
@@ -96,16 +96,6 @@
|
||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||
"default.name": "默认助手",
|
||||
"default.topic.name": "默认话题",
|
||||
"history": {
|
||||
"assistant_node": "助手",
|
||||
"click_to_navigate": "点击跳转到对应消息",
|
||||
"coming_soon": "聊天工作流图表即将上线",
|
||||
"no_messages": "没有找到消息",
|
||||
"start_conversation": "开始对话以查看聊天流程图",
|
||||
"title": "聊天历史",
|
||||
"user_node": "用户",
|
||||
"view_full_content": "查看完整内容"
|
||||
},
|
||||
"input.auto_resize": "自动调整高度",
|
||||
"input.clear": "清空消息 {{Command}}",
|
||||
"input.clear.content": "确定要清除当前会话所有消息吗?",
|
||||
@@ -115,8 +105,6 @@
|
||||
"input.estimated_tokens.tip": "预估 token 数",
|
||||
"input.expand": "展开",
|
||||
"input.file_not_supported": "模型不支持此文件类型",
|
||||
"input.generate_image": "生成图片",
|
||||
"input.generate_image_not_supported": "模型不支持生成图片",
|
||||
"input.knowledge_base": "知识库",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new_topic": "新话题 {{Command}}",
|
||||
@@ -138,13 +126,6 @@
|
||||
"message.quote": "引用",
|
||||
"message.regenerate.model": "切换模型",
|
||||
"message.useful": "有用",
|
||||
"navigation": {
|
||||
"first": "已经是第一条消息",
|
||||
"history": "聊天历史",
|
||||
"last": "已经是最后一条消息",
|
||||
"next": "下一条消息",
|
||||
"prev": "上一条消息"
|
||||
},
|
||||
"resend": "重新发送",
|
||||
"save": "保存",
|
||||
"settings.code_collapsible": "代码块可折叠",
|
||||
@@ -176,42 +157,37 @@
|
||||
"topics.edit.placeholder": "输入新名称",
|
||||
"topics.edit.title": "编辑话题名",
|
||||
"topics.export.image": "导出为图片",
|
||||
"topics.export.joplin": "导出到 Joplin",
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.notion": "导出到 Notion",
|
||||
"topics.export.obsidian": "导出到 Obsidian",
|
||||
"topics.export.obsidian_atributes": "配置笔记属性",
|
||||
"topics.export.obsidian_btn": "确定",
|
||||
"topics.export.obsidian_created": "创建时间",
|
||||
"topics.export.obsidian_created_placeholder": "请选择创建时间",
|
||||
"topics.export.obsidian_export_failed": "导出失败",
|
||||
"topics.export.obsidian_export_success": "导出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_operate": "处理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)",
|
||||
"topics.export.obsidian_operate_placeholder": "请选择处理方式",
|
||||
"topics.export.obsidian_operate_prepend": "前置",
|
||||
"topics.export.obsidian_source": "来源",
|
||||
"topics.export.obsidian_source_placeholder": "请输入来源",
|
||||
"topics.export.obsidian_tags": "标签",
|
||||
"topics.export.obsidian_tags_placeholder": "请输入标签,多个标签用英文逗号分隔",
|
||||
"topics.export.obsidian_title": "标题",
|
||||
"topics.export.obsidian_title_placeholder": "请输入标题",
|
||||
"topics.export.obsidian_title_required": "标题不能为空",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.export.yuque": "导出到语雀",
|
||||
"topics.export.obsidian": "导出到 Obsidian",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_fetch_failed": "获取 Obsidian 文件夹结构失败",
|
||||
"topics.export.obsidian_select_folder": "选择 Obsidian 文件夹",
|
||||
"topics.export.obsidian_select_folder.btn": "确定",
|
||||
"topics.export.obsidian_export_success": "导出成功",
|
||||
"topics.export.obsidian_export_failed": "导出失败",
|
||||
"topics.export.obsidian_show_md_files": "显示md文件",
|
||||
"topics.export.obsidian_selected_path": "已选择路径",
|
||||
"topics.export.joplin": "导出到 Joplin",
|
||||
"topics.list": "话题列表",
|
||||
"topics.move_to": "移动到",
|
||||
"topics.new": "开始新对话",
|
||||
"topics.pinned": "固定话题",
|
||||
"topics.prompt": "话题提示词",
|
||||
"topics.prompt.edit.title": "编辑话题提示词",
|
||||
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词",
|
||||
"topics.title": "话题",
|
||||
"topics.unpinned": "取消固定",
|
||||
"translate": "翻译"
|
||||
"topics.new": "开始新对话",
|
||||
"translate": "翻译",
|
||||
"navigation": {
|
||||
"prev": "上一条消息",
|
||||
"next": "下一条消息",
|
||||
"first": "已经是第一条消息",
|
||||
"last": "已经是最后一条消息"
|
||||
}
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "收起",
|
||||
@@ -221,7 +197,6 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "添加",
|
||||
"advanced_settings": "高级设置",
|
||||
"and": "和",
|
||||
"assistant": "智能体",
|
||||
"avatar": "头像",
|
||||
@@ -231,8 +206,8 @@
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"copied": "已复制",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"cut": "剪切",
|
||||
"default": "默认",
|
||||
"delete": "删除",
|
||||
@@ -241,7 +216,6 @@
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
"expand": "展开",
|
||||
"footnote": "引用内容",
|
||||
"footnotes": "引用内容",
|
||||
"fullscreen": "已进入全屏模式,按 F11 退出",
|
||||
@@ -249,7 +223,6 @@
|
||||
"language": "语言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"more": "更多",
|
||||
"name": "名称",
|
||||
"paste": "粘贴",
|
||||
"prompt": "提示词",
|
||||
@@ -262,7 +235,10 @@
|
||||
"select": "选择",
|
||||
"topics": "话题",
|
||||
"warning": "警告",
|
||||
"you": "用户"
|
||||
"you": "用户",
|
||||
"more": "更多",
|
||||
"advanced_settings": "高级设置",
|
||||
"expand": "展开"
|
||||
},
|
||||
"docs": {
|
||||
"title": "帮助文档"
|
||||
@@ -320,12 +296,6 @@
|
||||
"title": "文件",
|
||||
"type": "类型"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在内存中保持的时间(默认:5分钟)",
|
||||
"keep_alive_time.placeholder": "分钟",
|
||||
"keep_alive_time.title": "保持活跃时间",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "继续聊天",
|
||||
"locate.message": "定位到消息",
|
||||
@@ -394,11 +364,11 @@
|
||||
"threshold_placeholder": "未设置",
|
||||
"threshold_too_large_or_small": "阈值不能大于1或小于0",
|
||||
"threshold_tooltip": "用于衡量用户问题与知识库内容之间的相关性(0-1)",
|
||||
"title": "知识库",
|
||||
"topN": "返回结果数量",
|
||||
"topN__too_large_or_small": "返回结果数量不能大于100或小于1",
|
||||
"topN_placeholder": "未设置",
|
||||
"topN__too_large_or_small": "返回结果数量不能大于100或小于1",
|
||||
"topN_tooltip": "返回的匹配结果数量,数值越大,匹配结果越多,但消耗的 Token 也越多",
|
||||
"title": "知识库",
|
||||
"url_added": "网址已添加",
|
||||
"url_placeholder": "请输入网址, 多个网址用回车分隔",
|
||||
"urls": "网址"
|
||||
@@ -439,22 +409,22 @@
|
||||
"title": "Mermaid 图表"
|
||||
},
|
||||
"message": {
|
||||
"attachments": {
|
||||
"pasted_text": "剪切板文件",
|
||||
"pasted_image": "剪切板图片"
|
||||
},
|
||||
"api.check.model.title": "请选择要检测的模型",
|
||||
"api.connection.failed": "连接失败",
|
||||
"api.connection.success": "连接成功",
|
||||
"assistant.added.content": "智能体添加成功",
|
||||
"attachments": {
|
||||
"pasted_image": "剪切板图片",
|
||||
"pasted_text": "剪切板文件"
|
||||
},
|
||||
"backup.failed": "备份失败",
|
||||
"backup.start.success": "开始备份",
|
||||
"backup.success": "备份成功",
|
||||
"chat.completion.paused": "会话已停止",
|
||||
"citations": "引用内容",
|
||||
"copied": "已复制",
|
||||
"copy.failed": "复制失败",
|
||||
"copy.success": "复制成功",
|
||||
"copy.failed": "复制失败",
|
||||
"error.chunk_overlap_too_large": "分段重叠不能大于分段大小",
|
||||
"error.dimension_too_large": "内容尺寸过大",
|
||||
"error.enter.api.host": "请输入您的 API 地址",
|
||||
@@ -467,20 +437,20 @@
|
||||
"error.invalid.enter.model": "请选择一个模型",
|
||||
"error.invalid.proxy.url": "无效的代理地址",
|
||||
"error.invalid.webdav": "无效的 WebDAV 设置",
|
||||
"error.joplin.export": "导出 Joplin 失败,请保持 Joplin 已运行并检查连接状态或检查配置",
|
||||
"error.joplin.no_config": "未配置 Joplin 授权令牌 或 URL",
|
||||
"error.markdown.export.preconf": "导出Markdown文件到预先设定的路径失败",
|
||||
"error.markdown.export.specified": "导出Markdown文件失败",
|
||||
"error.notion.export": "导出 Notion 错误,请检查连接状态并对照文档检查配置",
|
||||
"error.notion.no_api_key": "未配置 Notion API Key 或 Notion Database ID",
|
||||
"error.yuque.export": "导出语雀错误,请检查连接状态并对照文档检查配置",
|
||||
"error.yuque.no_config": "未配置语雀 Token 或 知识库 URL",
|
||||
"error.joplin.no_config": "未配置 Joplin 授权令牌 或 URL",
|
||||
"error.joplin.export": "导出 Joplin 失败,请保持 Joplin 已运行并检查连接状态或检查配置",
|
||||
"group.delete.content": "删除分组消息会删除用户提问和所有助手的回答",
|
||||
"group.delete.title": "删除分组消息",
|
||||
"ignore.knowledge.base": "联网模式开启,忽略知识库",
|
||||
"info.notion.block_reach_limit": "对话过长,正在分页导出到Notion",
|
||||
"loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "正在准备导出到Notion...",
|
||||
"loading.notion.exporting_progress": "正在导出到Notion ({{current}}/{{total}})...",
|
||||
"mention.title": "切换模型回答",
|
||||
"message.code_style": "代码风格",
|
||||
"message.delete.content": "确定要删除此消息吗?",
|
||||
@@ -503,22 +473,22 @@
|
||||
"restore.success": "恢复成功",
|
||||
"save.success.title": "保存成功",
|
||||
"searching": "正在联网搜索...",
|
||||
"success.joplin.export": "成功导出到 Joplin",
|
||||
"success.markdown.export.preconf": "成功导出 Markdown 文件到预先设定的路径",
|
||||
"success.markdown.export.specified": "成功导出 Markdown 文件",
|
||||
"success.notion.export": "成功导出到 Notion",
|
||||
"success.yuque.export": "成功导出到语雀",
|
||||
"success.joplin.export": "成功导出到 Joplin",
|
||||
"switch.disabled": "请等待当前回复完成后操作",
|
||||
"tools": {
|
||||
"completed": "已完成",
|
||||
"invoking": "调用中"
|
||||
},
|
||||
"topic.added": "话题添加成功",
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.title": "升级成功",
|
||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试",
|
||||
"tools": {
|
||||
"invoking": "调用中",
|
||||
"completed": "已完成"
|
||||
}
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "添加到侧边栏",
|
||||
@@ -557,7 +527,9 @@
|
||||
"embedding": "嵌入",
|
||||
"embedding_model": "嵌入模型",
|
||||
"embedding_model_tooltip": "在设置->模型服务中点击管理按钮添加",
|
||||
"function_calling": "函数调用",
|
||||
"rerank_model": "重排序模型",
|
||||
"rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加",
|
||||
"free": "免费",
|
||||
"no_matches": "无可用模型",
|
||||
"parameter_name": "参数名称",
|
||||
"parameter_type": {
|
||||
@@ -567,22 +539,20 @@
|
||||
"string": "文本"
|
||||
},
|
||||
"pinned": "已固定",
|
||||
"rerank_model": "重排模型",
|
||||
"rerank_model_support_provider": "目前重排序模型仅支持部分服务商 ({{provider}})",
|
||||
"rerank_model_tooltip": "在设置->模型服务中点击管理按钮添加",
|
||||
"reasoning": "推理",
|
||||
"search": "搜索模型...",
|
||||
"stream_output": "流式输出",
|
||||
"function_calling": "函数调用",
|
||||
"type": {
|
||||
"embedding": "嵌入",
|
||||
"free": "免费",
|
||||
"function_calling": "工具",
|
||||
"reasoning": "推理",
|
||||
"rerank": "重排",
|
||||
"select": "选择模型类型",
|
||||
"text": "文本",
|
||||
"vision": "视觉",
|
||||
"websearch": "联网"
|
||||
}
|
||||
"vision": "图像",
|
||||
"function_calling": "函数调用"
|
||||
},
|
||||
"vision": "视觉",
|
||||
"websearch": "联网"
|
||||
},
|
||||
"navbar": {
|
||||
"expand": "伸缩对话框",
|
||||
@@ -635,12 +605,10 @@
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"baidu-cloud": "百度云千帆",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里云百炼",
|
||||
"deepseek": "深度求索",
|
||||
"dmxapi": "DMXAPI",
|
||||
@@ -649,7 +617,6 @@
|
||||
"gemini": "Gemini",
|
||||
"gitee-ai": "Gitee AI",
|
||||
"github": "GitHub Models",
|
||||
"gpustack": "GPUStack",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
@@ -679,7 +646,9 @@
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI",
|
||||
"voyageai":"Voyage AI"
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "确定要恢复数据吗?",
|
||||
@@ -695,6 +664,12 @@
|
||||
},
|
||||
"title": "数据恢复"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在内存中保持的时间(默认:5分钟)",
|
||||
"keep_alive_time.placeholder": "分钟",
|
||||
"keep_alive_time.title": "保持活跃时间",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"settings": {
|
||||
"about": "关于我们",
|
||||
"about.checkingUpdate": "正在检查更新...",
|
||||
@@ -741,30 +716,15 @@
|
||||
"data.title": "数据目录",
|
||||
"hour_interval_one": "{{count}} 小时",
|
||||
"hour_interval_other": "{{count}} 小时",
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "检查",
|
||||
"empty_token": "请先输入 Joplin 授权令牌",
|
||||
"empty_url": "请先输入 Joplin 剪裁服务监听 URL",
|
||||
"fail": "Joplin 连接验证失败",
|
||||
"success": "Joplin 连接验证成功"
|
||||
},
|
||||
"help": "在 Joplin 选项中,启用网页剪裁服务(无需安装浏览器插件),确认端口号,并复制授权令牌",
|
||||
"title": "Joplin 配置",
|
||||
"token": "Joplin 授权令牌",
|
||||
"token_placeholder": "请输入 Joplin 授权令牌",
|
||||
"url": "Joplin 剪裁服务监听 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。",
|
||||
"markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式",
|
||||
"markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框",
|
||||
"minute_interval_one": "{{count}} 分钟",
|
||||
"minute_interval_other": "{{count}} 分钟",
|
||||
"markdown_export.title": "Markdown 导出",
|
||||
"markdown_export.path": "默认导出路径",
|
||||
"markdown_export.path_placeholder": "导出路径",
|
||||
"markdown_export.select": "选择",
|
||||
"markdown_export.title": "Markdown 导出",
|
||||
"minute_interval_one": "{{count}} 分钟",
|
||||
"minute_interval_other": "{{count}} 分钟",
|
||||
"markdown_export.help": "若填入,则每次导出时将自动保存到该路径;否则,将弹出保存对话框",
|
||||
"markdown_export.force_dollar_math.title": "强制使用$$来标记LaTeX公式",
|
||||
"markdown_export.force_dollar_math.help": "开启后,导出Markdown时会将强制使用$$来标记LaTeX公式。注意:该项也会影响所有通过Markdown导出的方式,如Notion、语雀等。",
|
||||
"notion.api_key": "Notion 密钥",
|
||||
"notion.api_key_placeholder": "请输入Notion 密钥",
|
||||
"notion.auto_split": "导出对话时自动分页",
|
||||
@@ -786,22 +746,11 @@
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.title": "Notion 配置",
|
||||
"obsidian": {
|
||||
"folder": "文件夹",
|
||||
"folder_placeholder": "请输入文件夹名称",
|
||||
"tags": "全局标签",
|
||||
"tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔",
|
||||
"title": "Obsidian 配置",
|
||||
"vault": "保管库",
|
||||
"vault_placeholder": "请输入保管库名称"
|
||||
},
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
"autoSync.off": "关闭",
|
||||
"backup.button": "备份到 WebDAV",
|
||||
"backup.modal.filename.placeholder": "请输入备份文件名",
|
||||
"backup.modal.title": "备份到 WebDAV",
|
||||
"host": "WebDAV 地址",
|
||||
"host.placeholder": "http://localhost:8080",
|
||||
"hour_interval_one": "{{count}} 小时",
|
||||
@@ -813,12 +762,14 @@
|
||||
"password": "WebDAV 密码",
|
||||
"path": "WebDAV 路径",
|
||||
"path.placeholder": "/backup",
|
||||
"restore.button": "从 WebDAV 恢复",
|
||||
"restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?",
|
||||
"restore.confirm.title": "确认恢复",
|
||||
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
||||
"restore.modal.select.placeholder": "请选择要恢复的备份文件",
|
||||
"backup.modal.title": "备份到 WebDAV",
|
||||
"backup.modal.filename.placeholder": "请输入备份文件名",
|
||||
"restore.modal.title": "从 WebDAV 恢复",
|
||||
"restore.modal.select.placeholder": "请选择要恢复的备份文件",
|
||||
"restore.confirm.title": "确认恢复",
|
||||
"restore.confirm.content": "从 WebDAV 恢复将会覆盖当前数据,是否继续?",
|
||||
"restore.button": "从 WebDAV 恢复",
|
||||
"restore.content": "从 WebDAV 恢复将覆盖当前数据,是否继续?",
|
||||
"restore.title": "从 WebDAV 恢复",
|
||||
"syncError": "备份错误",
|
||||
"syncStatus": "备份状态",
|
||||
@@ -839,6 +790,36 @@
|
||||
"title": "语雀配置",
|
||||
"token": "语雀 Token",
|
||||
"token_placeholder": "请输入语雀Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"check": {
|
||||
"button": "检查",
|
||||
"empty_url": "请先输入 Obsidian REST API URL",
|
||||
"empty_api_key": "请先输入 Obsidian API Key",
|
||||
"fail": "Obsidian 连接验证失败",
|
||||
"success": "Obsidian 连接验证成功"
|
||||
},
|
||||
"help": "先安装 Obsidian 插件 Local REST API,然后获取 Obsidian API Key",
|
||||
"url": "Obsidian 知识库 URL",
|
||||
"url_placeholder": "http://127.0.0.1:27123/",
|
||||
"title": "Obsidian 配置",
|
||||
"api_key": "Obsidian API Key",
|
||||
"api_key_placeholder": "请输入 Obsidian API Key"
|
||||
},
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "检查",
|
||||
"empty_url": "请先输入 Joplin 剪裁服务监听 URL",
|
||||
"empty_token": "请先输入 Joplin 授权令牌",
|
||||
"fail": "Joplin 连接验证失败",
|
||||
"success": "Joplin 连接验证成功"
|
||||
},
|
||||
"title": "Joplin 配置",
|
||||
"help": "在 Joplin 选项中,启用网页剪裁服务(无需安装浏览器插件),确认端口号,并复制授权令牌",
|
||||
"url": "Joplin 剪裁服务监听 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"token": "Joplin 授权令牌",
|
||||
"token_placeholder": "请输入 Joplin 授权令牌"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "助手设置",
|
||||
@@ -884,15 +865,12 @@
|
||||
"input.target_language.english": "英文",
|
||||
"input.target_language.japanese": "日文",
|
||||
"input.target_language.russian": "俄文",
|
||||
"launch.onboot": "开机自动启动",
|
||||
"launch.title": "启动",
|
||||
"launch.totray": "启动时最小化到托盘",
|
||||
"mcp": {
|
||||
"actions": "操作",
|
||||
"active": "启用",
|
||||
"addError": "添加服务器失败",
|
||||
"addServer": "添加服务器",
|
||||
"addSuccess": "服务器添加成功",
|
||||
"addError": "添加服务器失败",
|
||||
"args": "参数",
|
||||
"argsTooltip": "每个参数占一行",
|
||||
"baseUrlTooltip": "远程 URL 地址",
|
||||
@@ -903,49 +881,49 @@
|
||||
"confirmDeleteMessage": "您确定要删除该服务器吗?",
|
||||
"deleteError": "删除服务器失败",
|
||||
"deleteSuccess": "服务器删除成功",
|
||||
"dependenciesInstall": "安装依赖项",
|
||||
"dependenciesInstalling": "正在安装依赖项...",
|
||||
"description": "描述",
|
||||
"duplicateName": "已存在同名服务器",
|
||||
"editJson": "编辑JSON",
|
||||
"editServer": "编辑服务器",
|
||||
"env": "环境变量",
|
||||
"envTooltip": "格式:KEY=value,每行一个",
|
||||
"findMore": "更多 MCP 服务器",
|
||||
"install": "安装",
|
||||
"installError": "安装依赖项失败",
|
||||
"installSuccess": "依赖项安装成功",
|
||||
"jsonFormatError": "JSON格式化错误",
|
||||
"jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。",
|
||||
"jsonSaveError": "保存JSON配置失败",
|
||||
"jsonSaveSuccess": "JSON配置已保存",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"name": "名称",
|
||||
"nameRequired": "请输入服务器名称",
|
||||
"noServers": "未配置服务器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
"package_name": "包名称",
|
||||
"scope_placeholder": "输入 npm 作用域 (例如 @your-org)",
|
||||
"scope_required": "请输入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失败",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"serverPlural": "服务器",
|
||||
"serverSingular": "服务器",
|
||||
"title": "MCP 服务器",
|
||||
"toggleError": "切换失败",
|
||||
"type": "类型",
|
||||
"updateError": "更新服务器失败",
|
||||
"updateSuccess": "服务器更新成功",
|
||||
"url": "URL"
|
||||
"updateError": "更新服务器失败",
|
||||
"url": "URL",
|
||||
"toggleError": "切换失败",
|
||||
"dependenciesInstalling": "正在安装依赖项...",
|
||||
"dependenciesInstall": "安装依赖项",
|
||||
"installSuccess": "依赖项安装成功",
|
||||
"installError": "安装依赖项失败",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"install": "安装",
|
||||
"npx_list": {
|
||||
"title": "NPX 包列表",
|
||||
"desc": "搜索并添加 npm 包作为 MCP 服务",
|
||||
"scope_placeholder": "输入 npm 作用域 (例如 @your-org)",
|
||||
"search": "搜索",
|
||||
"package_name": "包名称",
|
||||
"description": "描述",
|
||||
"usage": "用法",
|
||||
"npm": "NPM",
|
||||
"version": "版本",
|
||||
"actions": "操作",
|
||||
"scope_required": "请输入 npm 作用域",
|
||||
"no_packages": "未找到包",
|
||||
"search_error": "搜索失败"
|
||||
},
|
||||
"editJson": "编辑JSON",
|
||||
"jsonModeHint": "编辑MCP服务器配置的JSON表示。保存前请确保格式正确。",
|
||||
"jsonFormatError": "JSON格式化错误",
|
||||
"jsonSaveSuccess": "JSON配置已保存",
|
||||
"jsonSaveError": "保存JSON配置失败"
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
@@ -961,10 +939,6 @@
|
||||
"messages.math_engine": "数学公式引擎",
|
||||
"messages.metrics": "首字时延 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型设置",
|
||||
"messages.navigation": "对话导航按钮",
|
||||
"messages.navigation.anchor": "对话锚点",
|
||||
"messages.navigation.buttons": "上下按钮",
|
||||
"messages.navigation.none": "不显示",
|
||||
"messages.title": "消息设置",
|
||||
"messages.use_serif_font": "使用衬线字体",
|
||||
"model": "默认模型",
|
||||
@@ -985,7 +959,7 @@
|
||||
"models.check.enabled": "开启",
|
||||
"models.check.failed": "失败",
|
||||
"models.check.keys_status_count": "通过:{{count_passed}}个密钥,失败:{{count_failed}}个密钥",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 个模型完成健康检查(其中 {{count_partial}} 个模型用某些密钥无法访问),{{count_failed}} 个模型完全无法访问。",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}}个模型通过所有密钥,{{count_failed}}个模型未通过任何密钥,{{count_partial}}个模型未通过某些密钥",
|
||||
"models.check.no_api_keys": "未找到API密钥,请先添加API密钥。",
|
||||
"models.check.passed": "通过",
|
||||
"models.check.select_api_key": "选择要使用的API密钥:",
|
||||
@@ -1027,43 +1001,43 @@
|
||||
"check": "检查",
|
||||
"check_all_keys": "检查所有密钥",
|
||||
"check_multiple_keys": "检查多个 API 密钥",
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot 认证失败",
|
||||
"auth_success": "Github Copilot 认证成功",
|
||||
"auth_success_title": "认证成功",
|
||||
"code_failed": "获取 Device Code 失败,请重试",
|
||||
"code_generated_desc": "请将 Device Code 复制到下面的浏览器链接中",
|
||||
"code_generated_title": "获取 Device Code",
|
||||
"confirm_login": "过度使用可能会导致您的 Github 账号遭到封号,请谨慎使用!!!!",
|
||||
"confirm_title": "风险警告",
|
||||
"connect": "连接 Github",
|
||||
"custom_headers": "自定义请求头",
|
||||
"description": "您的 Github 账号需要订阅 Copilot",
|
||||
"expand": "展开",
|
||||
"headers_description": "自定义请求头(json格式)",
|
||||
"invalid_json": "JSON 格式错误",
|
||||
"login": "登录 Github",
|
||||
"logout": "退出 Github",
|
||||
"logout_failed": "退出失败,请重试",
|
||||
"logout_success": "已成功退出",
|
||||
"model_setting": "模型设置",
|
||||
"open_verification_first": "请先点击上方链接访问验证页面",
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登录 Github"
|
||||
},
|
||||
"delete.content": "确定要删除此模型提供商吗?",
|
||||
"delete.title": "删除提供商",
|
||||
"docs_check": "查看",
|
||||
"docs_more_details": "获取更多详情",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"is_not_support_array_content": "开启兼容模式",
|
||||
"no_models": "请先添加模型再检查 API 连接",
|
||||
"not_checked": "未检查",
|
||||
"remove_duplicate_keys": "移除重复密钥",
|
||||
"remove_invalid_keys": "删除无效密钥",
|
||||
"search": "搜索模型平台...",
|
||||
"search_placeholder": "搜索模型 ID 或名称",
|
||||
"title": "模型服务"
|
||||
"title": "模型服务",
|
||||
"is_not_support_array_content": "开启兼容模式",
|
||||
"copilot": {
|
||||
"tooltip": "使用 Github Copilot 需要先登录 Github",
|
||||
"description": "您的 Github 账号需要订阅 Copilot",
|
||||
"login": "登录 Github",
|
||||
"connect": "连接 Github",
|
||||
"logout": "退出 Github",
|
||||
"auth_success_title": "认证成功",
|
||||
"code_generated_title": "获取 Device Code",
|
||||
"code_generated_desc": "请将 Device Code 复制到下面的浏览器链接中",
|
||||
"code_failed": "获取 Device Code 失败,请重试",
|
||||
"auth_success": "Github Copilot 认证成功",
|
||||
"auth_failed": "Github Copilot 认证失败",
|
||||
"logout_success": "已成功退出",
|
||||
"logout_failed": "退出失败,请重试",
|
||||
"confirm_title": "风险警告",
|
||||
"confirm_login": "过度使用可能会导致您的 Github 账号遭到封号,请谨慎使用!!!!",
|
||||
"rate_limit": "速率限制",
|
||||
"custom_headers": "自定义请求头",
|
||||
"headers_description": "自定义请求头(json格式)",
|
||||
"expand": "展开",
|
||||
"model_setting": "模型设置",
|
||||
"invalid_json": "JSON 格式错误",
|
||||
"open_verification_first": "请先点击上方链接访问验证页面"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
@@ -1078,9 +1052,9 @@
|
||||
"quickAssistant": {
|
||||
"click_tray_to_show": "点击托盘图标启动",
|
||||
"enable_quick_assistant": "启用快捷助手",
|
||||
"read_clipboard_at_startup": "启动时读取剪贴板",
|
||||
"title": "快捷助手",
|
||||
"use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动"
|
||||
"use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动",
|
||||
"read_clipboard_at_startup": "启动时读取剪贴板"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
@@ -1117,18 +1091,14 @@
|
||||
"topic.position.left": "左侧",
|
||||
"topic.position.right": "右侧",
|
||||
"topic.show.time": "显示话题时间",
|
||||
"tray.onclose": "关闭时最小化到托盘",
|
||||
"tray.show": "显示托盘图标",
|
||||
"tray.title": "托盘",
|
||||
"tray.title": "启用系统托盘图标",
|
||||
"websearch": {
|
||||
"blacklist": "黑名单",
|
||||
"blacklist_description": "在搜索结果中不会出现以下网站的结果",
|
||||
"blacklist_tooltip": "请使用以下格式(换行分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "检查",
|
||||
"check_failed": "验证失败",
|
||||
"check_success": "验证成功",
|
||||
"enhance_mode": "搜索增强模式",
|
||||
"enhance_mode_tooltip": "使用默认模型提取关键词后搜索",
|
||||
"check_failed": "验证失败",
|
||||
"get_api_key": "点击这里获取密钥",
|
||||
"no_provider_selected": "请选择搜索服务商后再检查",
|
||||
"search_max_result": "搜索结果个数",
|
||||
@@ -1136,6 +1106,8 @@
|
||||
"search_provider_placeholder": "选择一个搜索服务商",
|
||||
"search_result_default": "默认",
|
||||
"search_with_time": "搜索包含日期",
|
||||
"enhance_mode": "搜索增强模式",
|
||||
"enhance_mode_tooltip": "使用默认模型提取关键词后搜索",
|
||||
"tavily": {
|
||||
"api_key": "Tavily API 密钥",
|
||||
"api_key.placeholder": "请输入 Tavily API 密钥",
|
||||
@@ -1143,6 +1115,53 @@
|
||||
"title": "Tavily"
|
||||
},
|
||||
"title": "网络搜索"
|
||||
},
|
||||
"nodeRequired": "需要 Node.js",
|
||||
"nodeSettings": {
|
||||
"title": "Node.js 环境设置",
|
||||
"description": "管理 Cherry Studio 内置的 Node.js 环境。您可以选择要安装的 Node.js 版本,以确保最佳的兼容性。",
|
||||
"status": "状态",
|
||||
"checking": "检查中...",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装",
|
||||
"refresh": "刷新",
|
||||
"version": "Node.js 版本",
|
||||
"versionHelp": "选择要安装的 Node.js 版本",
|
||||
"customVersion": "自定义版本",
|
||||
"customVersionHelp": "如果您需要特定版本,请在此输入版本号(如 18.16.1)",
|
||||
"install": "安装 Node.js",
|
||||
"reinstall": "重新安装 Node.js",
|
||||
"installSuccess": "Node.js v{{version}} 安装成功",
|
||||
"installFailed": "Node.js 安装失败"
|
||||
},
|
||||
"nodeSettingsTab": "Node.js 环境",
|
||||
"appsManagerTab": "应用管理",
|
||||
"packageDeployerTab": "部署代码包",
|
||||
"packageDeployer": {
|
||||
"advancedOptions": "高级选项",
|
||||
"deploy": "部署",
|
||||
"deployFailed": "部署包失败",
|
||||
"deploySuccess": "{{name}} 已成功部署在端口 {{port}} 上",
|
||||
"description": "上传包含 Node.js 应用程序代码的 ZIP 文件。该包将被自动解压和安装。",
|
||||
"fileSelectError": "选择文件时出错",
|
||||
"installNode": "安装 Node.js",
|
||||
"installNodePrompt": "部署应用程序需要 Node.js。您要现在安装吗?",
|
||||
"namePlaceholder": "为您部署的应用输入名称",
|
||||
"nodeInstallFailed": "安装 Node.js 失败",
|
||||
"nodeInstallSuccess": "Node.js 安装成功",
|
||||
"nodeNeeded": "运行应用程序需要内置 Node.js。",
|
||||
"nodeNotAvailable": "Node.js 不可用",
|
||||
"noFileSelected": "请选择要部署的 ZIP 文件",
|
||||
"open": "在浏览器中打开",
|
||||
"selectZip": "点击选择 ZIP 文件",
|
||||
"title": "部署代码包",
|
||||
"moduleTypeError": "模块类型错误",
|
||||
"esModuleError": "发现 ES 模块语法。请在 package.json 中设置 \"type\": \"module\" 或使用 .mjs 扩展名。",
|
||||
"convertToCommonJS": "转换为 CommonJS 语法",
|
||||
"nextJsDetected": "检测到 Next.js 应用",
|
||||
"buildStepAdded": "已自动添加构建步骤:将在启动应用前执行 'npm run build'。",
|
||||
"nextJsInfo": "Next.js 应用注意事项",
|
||||
"nextJsDescription": "Next.js 应用需要先构建后才能启动。请确保勾选\"这是一个 Next.js 应用\"或手动添加\"npm run build\"作为构建命令。"
|
||||
}
|
||||
},
|
||||
"translate": {
|
||||
@@ -1180,6 +1199,32 @@
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"nodeapp": {
|
||||
"add": "添加",
|
||||
"addApp": "添加应用",
|
||||
"appName": "应用名称",
|
||||
"appsManager": {
|
||||
"confirmDelete": "确定要删除此应用吗?",
|
||||
"confirmStop": "确定要停止此应用吗?",
|
||||
"description": "管理应用",
|
||||
"install": "安装",
|
||||
"noApps": "暂无应用,请添加新应用或从代码部署",
|
||||
"port": "端口",
|
||||
"repository": "仓库",
|
||||
"start": "启动",
|
||||
"status": "状态",
|
||||
"stop": "停止",
|
||||
"title": "应用管理",
|
||||
"uninstall": "卸载",
|
||||
"update": "更新",
|
||||
"updateProgress": "更新进度",
|
||||
"updateSuccess": "{{name}} 更新成功",
|
||||
"version": "版本",
|
||||
"viewRepository": "查看仓库"
|
||||
},
|
||||
"nextJsInfo": "Next.js 应用注意事项",
|
||||
"nextJsDescription": "Next.js 应用需要先构建后才能启动。请确保勾选\"这是一个 Next.js 应用\"或手动添加\"npm run build\"作为构建命令。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,16 +96,6 @@
|
||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||
"default.name": "預設助手",
|
||||
"default.topic.name": "預設話題",
|
||||
"history": {
|
||||
"assistant_node": "助手",
|
||||
"click_to_navigate": "點擊跳轉到對應訊息",
|
||||
"coming_soon": "聊天工作流圖表即將上線",
|
||||
"no_messages": "沒有找到訊息",
|
||||
"start_conversation": "開始對話以查看聊天流程圖",
|
||||
"title": "聊天歷史",
|
||||
"user_node": "用戶",
|
||||
"view_full_content": "查看完整內容"
|
||||
},
|
||||
"input.auto_resize": "自動調整高度",
|
||||
"input.clear": "清除 {{Command}}",
|
||||
"input.clear.content": "您想要清除目前話題的所有訊息嗎?",
|
||||
@@ -115,8 +105,6 @@
|
||||
"input.estimated_tokens.tip": "預估 Token 數",
|
||||
"input.expand": "展開",
|
||||
"input.file_not_supported": "模型不支援此檔案類型",
|
||||
"input.generate_image": "生成圖片",
|
||||
"input.generate_image_not_supported": "模型不支援生成圖片",
|
||||
"input.knowledge_base": "知識庫",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new_topic": "新話題 {{Command}}",
|
||||
@@ -138,13 +126,6 @@
|
||||
"message.quote": "引用",
|
||||
"message.regenerate.model": "切換模型",
|
||||
"message.useful": "有用",
|
||||
"navigation": {
|
||||
"first": "已經是第一條訊息",
|
||||
"history": "聊天歷史",
|
||||
"last": "已經是最後一條訊息",
|
||||
"next": "下一條訊息",
|
||||
"prev": "上一條訊息"
|
||||
},
|
||||
"resend": "重新傳送",
|
||||
"save": "儲存",
|
||||
"settings.code_collapsible": "程式碼區塊可折疊",
|
||||
@@ -176,42 +157,37 @@
|
||||
"topics.edit.placeholder": "輸入新名稱",
|
||||
"topics.edit.title": "編輯名稱",
|
||||
"topics.export.image": "匯出為圖片",
|
||||
"topics.export.joplin": "匯出到 Joplin",
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.notion": "匯出到 Notion",
|
||||
"topics.export.obsidian": "匯出到 Obsidian",
|
||||
"topics.export.obsidian_atributes": "配置筆記屬性",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "建立時間",
|
||||
"topics.export.obsidian_created_placeholder": "請選擇建立時間",
|
||||
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||
"topics.export.obsidian_export_success": "匯出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_operate": "處理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)",
|
||||
"topics.export.obsidian_operate_placeholder": "請選擇處理方式",
|
||||
"topics.export.obsidian_operate_prepend": "前置",
|
||||
"topics.export.obsidian_source": "來源",
|
||||
"topics.export.obsidian_source_placeholder": "請輸入來源",
|
||||
"topics.export.obsidian_tags": "標籤",
|
||||
"topics.export.obsidian_tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔",
|
||||
"topics.export.obsidian_title": "標題",
|
||||
"topics.export.obsidian_title_placeholder": "請輸入標題",
|
||||
"topics.export.obsidian_title_required": "標題不能為空",
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.word": "匯出為 Word",
|
||||
"topics.export.yuque": "匯出到語雀",
|
||||
"topics.export.obsidian": "匯出到 Obsidian",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_fetch_failed": "獲取 Obsidian 文件夾結構失敗",
|
||||
"topics.export.obsidian_select_folder": "選擇 Obsidian 文件夾",
|
||||
"topics.export.obsidian_select_folder.btn": "確定",
|
||||
"topics.export.obsidian_export_success": "匯出成功",
|
||||
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||
"topics.export.obsidian_show_md_files": "顯示md文件",
|
||||
"topics.export.obsidian_selected_path": "已選擇路徑",
|
||||
"topics.export.joplin": "匯出到 Joplin",
|
||||
"topics.list": "話題列表",
|
||||
"topics.move_to": "移動到",
|
||||
"topics.new": "開始新對話",
|
||||
"topics.pinned": "固定話題",
|
||||
"topics.prompt": "話題提示詞",
|
||||
"topics.prompt.edit.title": "編輯話題提示詞",
|
||||
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
||||
"topics.title": "話題",
|
||||
"topics.unpinned": "取消固定",
|
||||
"translate": "翻譯"
|
||||
"topics.new": "開始新對話",
|
||||
"translate": "翻譯",
|
||||
"navigation": {
|
||||
"prev": "上一條訊息",
|
||||
"next": "下一條訊息",
|
||||
"first": "已經是第一條訊息",
|
||||
"last": "已經是最後一條訊息"
|
||||
}
|
||||
},
|
||||
"code_block": {
|
||||
"collapse": "折疊",
|
||||
@@ -221,7 +197,6 @@
|
||||
},
|
||||
"common": {
|
||||
"add": "新增",
|
||||
"advanced_settings": "進階設定",
|
||||
"and": "與",
|
||||
"assistant": "智慧代理人",
|
||||
"avatar": "頭像",
|
||||
@@ -230,8 +205,6 @@
|
||||
"chat": "聊天",
|
||||
"clear": "清除",
|
||||
"close": "關閉",
|
||||
"confirm": "確認",
|
||||
"copied": "已複製",
|
||||
"copy": "複製",
|
||||
"cut": "剪下",
|
||||
"default": "預設",
|
||||
@@ -241,7 +214,6 @@
|
||||
"download": "下載",
|
||||
"duplicate": "複製",
|
||||
"edit": "編輯",
|
||||
"expand": "展開",
|
||||
"footnote": "引用內容",
|
||||
"footnotes": "引用",
|
||||
"fullscreen": "已進入全螢幕模式,按 F11 結束",
|
||||
@@ -249,7 +221,6 @@
|
||||
"language": "語言",
|
||||
"model": "模型",
|
||||
"models": "模型",
|
||||
"more": "更多",
|
||||
"name": "名稱",
|
||||
"paste": "貼上",
|
||||
"prompt": "提示詞",
|
||||
@@ -262,7 +233,12 @@
|
||||
"select": "選擇",
|
||||
"topics": "話題",
|
||||
"warning": "警告",
|
||||
"you": "您"
|
||||
"you": "您",
|
||||
"copied": "已複製",
|
||||
"confirm": "確認",
|
||||
"more": "更多",
|
||||
"advanced_settings": "進階設定",
|
||||
"expand": "展開"
|
||||
},
|
||||
"docs": {
|
||||
"title": "說明文件"
|
||||
@@ -320,12 +296,6 @@
|
||||
"title": "檔案",
|
||||
"type": "類型"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||
"keep_alive_time.placeholder": "分鐘",
|
||||
"keep_alive_time.title": "保持活躍時間",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "繼續聊天",
|
||||
"locate.message": "定位到訊息",
|
||||
@@ -395,13 +365,13 @@
|
||||
"threshold_too_large_or_small": "閾值不能大於 1 或小於 0",
|
||||
"threshold_tooltip": "用於衡量使用者問題與知識庫內容之間的相關性(0-1)",
|
||||
"title": "知識庫",
|
||||
"topN": "返回結果數量",
|
||||
"topN__too_large_or_small": "返回結果數量不能大於100或小於1",
|
||||
"topN_placeholder": "未設定",
|
||||
"topN_tooltip": "返回的匹配結果數量,數值越大,匹配結果越多,但消耗的 Token 也越多",
|
||||
"url_added": "網址已新增",
|
||||
"url_placeholder": "請輸入網址,多個網址用換行符號分隔",
|
||||
"urls": "網址"
|
||||
"urls": "網址",
|
||||
"topN": "返回結果數量",
|
||||
"topN_placeholder": "未設定",
|
||||
"topN__too_large_or_small": "返回結果數量不能大於100或小於1",
|
||||
"topN_tooltip": "返回的匹配結果數量,數值越大,匹配結果越多,但消耗的 Token 也越多"
|
||||
},
|
||||
"languages": {
|
||||
"arabic": "阿拉伯文",
|
||||
@@ -439,22 +409,22 @@
|
||||
"title": "Mermaid 圖表"
|
||||
},
|
||||
"message": {
|
||||
"attachments": {
|
||||
"pasted_text": "剪切板文件",
|
||||
"pasted_image": "剪切板圖片"
|
||||
},
|
||||
"api.check.model.title": "請選擇要偵測的模型",
|
||||
"api.connection.failed": "連接失敗",
|
||||
"api.connection.success": "連接成功",
|
||||
"assistant.added.content": "智慧代理人新增成功",
|
||||
"attachments": {
|
||||
"pasted_image": "剪切板圖片",
|
||||
"pasted_text": "剪切板文件"
|
||||
},
|
||||
"backup.failed": "備份失敗",
|
||||
"backup.start.success": "開始備份",
|
||||
"backup.success": "備份成功",
|
||||
"chat.completion.paused": "聊天完成已暫停",
|
||||
"citations": "參考文獻",
|
||||
"copied": "已複製!",
|
||||
"copy.failed": "複製失敗",
|
||||
"copy.success": "已複製!",
|
||||
"copy.failed": "複製失敗",
|
||||
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
|
||||
"error.dimension_too_large": "內容尺寸過大",
|
||||
"error.enter.api.host": "請先輸入您的 API 主機地址",
|
||||
@@ -467,20 +437,20 @@
|
||||
"error.invalid.enter.model": "請選擇一個模型",
|
||||
"error.invalid.proxy.url": "無效的代理伺服器 URL",
|
||||
"error.invalid.webdav": "無效的 WebDAV 設定",
|
||||
"error.joplin.export": "匯出 Joplin 失敗,請保持 Joplin 已運行並檢查連接狀態或檢查設定",
|
||||
"error.joplin.no_config": "未設定 Joplin 授權Token 或 URL",
|
||||
"error.markdown.export.preconf": "導出 Markdown 文件到預先設定的路徑失敗",
|
||||
"error.markdown.export.specified": "導出 Markdown 文件失敗",
|
||||
"error.notion.export": "匯出 Notion 錯誤,請檢查連接狀態並對照文件檢查設定",
|
||||
"error.notion.no_api_key": "未設定 Notion API Key 或 Notion Database ID",
|
||||
"error.yuque.export": "匯出語雀錯誤,請檢查連接狀態並對照文件檢查設定",
|
||||
"error.yuque.no_config": "未設定語雀 Token 或知識庫 Url",
|
||||
"error.joplin.no_config": "未設定 Joplin 授權Token 或 URL",
|
||||
"error.joplin.export": "匯出 Joplin 失敗,請保持 Joplin 已運行並檢查連接狀態或檢查設定",
|
||||
"group.delete.content": "刪除分組訊息會刪除使用者提問和所有助手的回答",
|
||||
"group.delete.title": "刪除分組訊息",
|
||||
"ignore.knowledge.base": "網路模式開啟,忽略知識庫",
|
||||
"info.notion.block_reach_limit": "對話過長,自動分頁匯出到 Notion",
|
||||
"loading.notion.exporting_progress": "正在匯出到 Notion ({{current}}/{{total}})...",
|
||||
"loading.notion.preparing": "正在準備匯出到 Notion...",
|
||||
"loading.notion.exporting_progress": "正在匯出到 Notion ({{current}}/{{total}})...",
|
||||
"mention.title": "切換模型回答",
|
||||
"message.code_style": "程式碼風格",
|
||||
"message.delete.content": "確定要刪除此訊息嗎?",
|
||||
@@ -503,22 +473,22 @@
|
||||
"restore.success": "恢復成功",
|
||||
"save.success.title": "儲存成功",
|
||||
"searching": "正在網路上搜尋...",
|
||||
"success.joplin.export": "成功匯出到 Joplin",
|
||||
"success.markdown.export.preconf": "成功導出 Markdown 文件到預先設定的路徑",
|
||||
"success.markdown.export.specified": "成功導出 Markdown 文件",
|
||||
"success.notion.export": "成功匯出到 Notion",
|
||||
"success.yuque.export": "成功匯出到語雀",
|
||||
"success.joplin.export": "成功匯出到 Joplin",
|
||||
"switch.disabled": "請等待當前回覆完成",
|
||||
"tools": {
|
||||
"completed": "已完成",
|
||||
"invoking": "調用中"
|
||||
},
|
||||
"topic.added": "新話題已新增",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||
"upgrade.success.title": "升級成功",
|
||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試",
|
||||
"tools": {
|
||||
"invoking": "調用中",
|
||||
"completed": "已完成"
|
||||
}
|
||||
},
|
||||
"minapp": {
|
||||
"sidebar.add.title": "新增到側邊欄",
|
||||
@@ -557,7 +527,7 @@
|
||||
"embedding": "嵌入",
|
||||
"embedding_model": "嵌入模型",
|
||||
"embedding_model_tooltip": "在設定->模型服務中點選管理按鈕新增",
|
||||
"function_calling": "函數調用",
|
||||
"free": "免費",
|
||||
"no_matches": "無可用模型",
|
||||
"parameter_name": "參數名稱",
|
||||
"parameter_type": {
|
||||
@@ -567,22 +537,22 @@
|
||||
"string": "文字"
|
||||
},
|
||||
"pinned": "已固定",
|
||||
"rerank_model": "重排模型",
|
||||
"rerank_model_support_provider": "目前重排序模型僅支持部分服務商 ({{provider}})",
|
||||
"rerank_model_tooltip": "在設定->模型服務中點擊管理按鈕添加",
|
||||
"reasoning": "推理",
|
||||
"search": "搜尋模型...",
|
||||
"stream_output": "串流輸出",
|
||||
"function_calling": "函數調用",
|
||||
"type": {
|
||||
"embedding": "嵌入",
|
||||
"free": "免費",
|
||||
"function_calling": "工具",
|
||||
"reasoning": "推理",
|
||||
"rerank": "重排",
|
||||
"select": "選擇模型類型",
|
||||
"text": "文字",
|
||||
"vision": "視覺",
|
||||
"websearch": "網路搜尋"
|
||||
}
|
||||
"vision": "影像",
|
||||
"function_calling": "函數調用"
|
||||
},
|
||||
"vision": "視覺",
|
||||
"websearch": "網路搜尋",
|
||||
"rerank_model": "重排序模型",
|
||||
"rerank_model_tooltip": "在設定->模型服務中點擊管理按鈕添加"
|
||||
},
|
||||
"navbar": {
|
||||
"expand": "伸縮對話框",
|
||||
@@ -635,12 +605,10 @@
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
"alayanew": "Alaya NeW",
|
||||
"anthropic": "Anthropic",
|
||||
"azure-openai": "Azure OpenAI",
|
||||
"baichuan": "百川",
|
||||
"baidu-cloud": "百度雲千帆",
|
||||
"copilot": "GitHub Copilot",
|
||||
"dashscope": "阿里雲百鍊",
|
||||
"deepseek": "深度求索",
|
||||
"dmxapi": "DMXAPI",
|
||||
@@ -649,7 +617,6 @@
|
||||
"gemini": "Gemini",
|
||||
"gitee-ai": "Gitee AI",
|
||||
"github": "GitHub Models",
|
||||
"gpustack": "GPUStack",
|
||||
"graphrag-kylin-mountain": "GraphRAG",
|
||||
"grok": "Grok",
|
||||
"groq": "Groq",
|
||||
@@ -679,7 +646,9 @@
|
||||
"yi": "零一萬物",
|
||||
"zhinao": "360 智腦",
|
||||
"zhipu": "智譜 AI",
|
||||
"voyageai": "Voyage AI"
|
||||
"copilot": "GitHub Copilot",
|
||||
"gpustack": "GPUStack",
|
||||
"alayanew": "Alaya NeW"
|
||||
},
|
||||
"restore": {
|
||||
"confirm": "確定要復原資料嗎?",
|
||||
@@ -695,6 +664,12 @@
|
||||
},
|
||||
"title": "資料復原"
|
||||
},
|
||||
"gpustack": {
|
||||
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||
"keep_alive_time.placeholder": "分鐘",
|
||||
"keep_alive_time.title": "保持活躍時間",
|
||||
"title": "GPUStack"
|
||||
},
|
||||
"settings": {
|
||||
"about": "關於與回饋",
|
||||
"about.checkingUpdate": "正在檢查更新...",
|
||||
@@ -741,30 +716,15 @@
|
||||
"data.title": "資料目錄",
|
||||
"hour_interval_one": "{{count}} 小時",
|
||||
"hour_interval_other": "{{count}} 小時",
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "檢查",
|
||||
"empty_token": "請先輸入 Joplin 授權Token",
|
||||
"empty_url": "請先輸入 Joplin 剪輯服務 URL",
|
||||
"fail": "Joplin 連接驗證失敗",
|
||||
"success": "Joplin 連接驗證成功"
|
||||
},
|
||||
"help": "在 Joplin 選項中,啟用剪輯服務(無需安裝瀏覽器外掛),確認埠編號,並複製授權Token",
|
||||
"title": "Joplin 設定",
|
||||
"token": "Joplin 授權Token",
|
||||
"token_placeholder": "請輸入 Joplin 授權Token",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/"
|
||||
},
|
||||
"markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$",
|
||||
"markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框。",
|
||||
"minute_interval_one": "{{count}} 分鐘",
|
||||
"minute_interval_other": "{{count}} 分鐘",
|
||||
"markdown_export.title": "Markdown 匯出",
|
||||
"markdown_export.path": "預設匯出路徑",
|
||||
"markdown_export.path_placeholder": "匯出路徑",
|
||||
"markdown_export.select": "選擇",
|
||||
"markdown_export.title": "Markdown 匯出",
|
||||
"minute_interval_one": "{{count}} 分鐘",
|
||||
"minute_interval_other": "{{count}} 分鐘",
|
||||
"markdown_export.help": "若填入,每次匯出時將自動儲存至該路徑;否則,將彈出儲存對話框。",
|
||||
"markdown_export.force_dollar_math.title": "LaTeX公式強制使用$$",
|
||||
"markdown_export.force_dollar_math.help": "開啟後,匯出Markdown時會強制使用$$來標記LaTeX公式。注意:該項也會影響所有透過Markdown匯出的方式,如Notion、語雀等。",
|
||||
"notion.api_key": "Notion 金鑰",
|
||||
"notion.api_key_placeholder": "請輸入 Notion 金鑰",
|
||||
"notion.auto_split": "匯出對話時自動分頁",
|
||||
@@ -786,22 +746,11 @@
|
||||
"notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "資料夾",
|
||||
"folder_placeholder": "請輸入資料夾名稱",
|
||||
"tags": "全域標籤",
|
||||
"tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。",
|
||||
"title": "Obsidian 設定",
|
||||
"vault": "保險庫",
|
||||
"vault_placeholder": "請輸入保險庫名稱"
|
||||
},
|
||||
"title": "資料設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
"autoSync.off": "關閉",
|
||||
"backup.button": "備份到 WebDAV",
|
||||
"backup.modal.filename.placeholder": "請輸入備份文件名",
|
||||
"backup.modal.title": "備份到 WebDAV",
|
||||
"host": "WebDAV 主機位址",
|
||||
"host.placeholder": "http://localhost:8080",
|
||||
"hour_interval_one": "{{count}} 小時",
|
||||
@@ -814,16 +763,18 @@
|
||||
"path": "WebDAV 路徑",
|
||||
"path.placeholder": "/backup",
|
||||
"restore.button": "從 WebDAV 恢復",
|
||||
"restore.confirm.content": "從 WebDAV 恢復將覆蓋目前資料,是否繼續?",
|
||||
"restore.confirm.title": "復元確認",
|
||||
"restore.content": "從 WebDAV 恢復將覆蓋目前資料,是否繼續?",
|
||||
"restore.modal.select.placeholder": "請選擇要恢復的備份文件",
|
||||
"restore.modal.title": "從 WebDAV 恢復",
|
||||
"restore.title": "從 WebDAV 恢復",
|
||||
"syncError": "備份錯誤",
|
||||
"syncStatus": "備份狀態",
|
||||
"title": "WebDAV",
|
||||
"user": "WebDAV 使用者名稱"
|
||||
"user": "WebDAV 使用者名稱",
|
||||
"backup.modal.title": "備份到 WebDAV",
|
||||
"backup.modal.filename.placeholder": "請輸入備份文件名",
|
||||
"restore.modal.title": "從 WebDAV 恢復",
|
||||
"restore.modal.select.placeholder": "請選擇要恢復的備份文件",
|
||||
"restore.confirm.title": "復元確認",
|
||||
"restore.confirm.content": "從 WebDAV 恢復將覆蓋目前資料,是否繼續?"
|
||||
},
|
||||
"yuque": {
|
||||
"check": {
|
||||
@@ -839,6 +790,36 @@
|
||||
"title": "語雀設定",
|
||||
"token": "語雀 Token",
|
||||
"token_placeholder": "請輸入語雀 Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"check": {
|
||||
"button": "檢查",
|
||||
"empty_url": "請先輸入 Obsidian REST API URL",
|
||||
"empty_api_key": "請先輸入 Obsidian API Key",
|
||||
"fail": "Obsidian 連接驗證失敗",
|
||||
"success": "Obsidian 連接驗證成功"
|
||||
},
|
||||
"help": "先安裝 Obsidian 插件 Local REST API,然後獲取 Obsidian API Key",
|
||||
"url": "Obsidian 知識庫 URL",
|
||||
"url_placeholder": "http://127.0.0.1:27123/",
|
||||
"title": "Obsidian 設定",
|
||||
"api_key": "Obsidian API Key",
|
||||
"api_key_placeholder": "請輸入 Obsidian API Key"
|
||||
},
|
||||
"joplin": {
|
||||
"check": {
|
||||
"button": "檢查",
|
||||
"empty_url": "請先輸入 Joplin 剪輯服務 URL",
|
||||
"empty_token": "請先輸入 Joplin 授權Token",
|
||||
"fail": "Joplin 連接驗證失敗",
|
||||
"success": "Joplin 連接驗證成功"
|
||||
},
|
||||
"title": "Joplin 設定",
|
||||
"help": "在 Joplin 選項中,啟用剪輯服務(無需安裝瀏覽器外掛),確認埠編號,並複製授權Token",
|
||||
"url": "Joplin 剪輯服務 URL",
|
||||
"url_placeholder": "http://127.0.0.1:41184/",
|
||||
"token": "Joplin 授權Token",
|
||||
"token_placeholder": "請輸入 Joplin 授權Token"
|
||||
}
|
||||
},
|
||||
"display.assistant.title": "助手設定",
|
||||
@@ -884,15 +865,12 @@
|
||||
"input.target_language.english": "英文",
|
||||
"input.target_language.japanese": "日文",
|
||||
"input.target_language.russian": "俄文",
|
||||
"launch.onboot": "開機自動啟動",
|
||||
"launch.title": "啟動",
|
||||
"launch.totray": "啟動時最小化到系统匣",
|
||||
"mcp": {
|
||||
"actions": "操作",
|
||||
"active": "啟用",
|
||||
"addError": "添加伺服器失敗",
|
||||
"addServer": "新增伺服器",
|
||||
"addSuccess": "伺服器新增成功",
|
||||
"addError": "添加伺服器失敗",
|
||||
"args": "參數",
|
||||
"argsTooltip": "每個參數佔一行",
|
||||
"baseUrlTooltip": "遠端 URL 地址",
|
||||
@@ -903,49 +881,49 @@
|
||||
"confirmDeleteMessage": "您確定要刪除該伺服器嗎?",
|
||||
"deleteError": "刪除伺服器失敗",
|
||||
"deleteSuccess": "伺服器刪除成功",
|
||||
"dependenciesInstall": "安裝相依套件",
|
||||
"dependenciesInstalling": "正在安裝相依套件...",
|
||||
"description": "描述",
|
||||
"duplicateName": "已存在相同名稱的伺服器",
|
||||
"editJson": "編輯JSON",
|
||||
"editServer": "編輯伺服器",
|
||||
"env": "環境變數",
|
||||
"envTooltip": "格式:KEY=value,每行一個",
|
||||
"findMore": "更多 MCP 伺服器",
|
||||
"install": "安裝",
|
||||
"installError": "安裝相依套件失敗",
|
||||
"installSuccess": "相依套件安裝成功",
|
||||
"jsonFormatError": "JSON格式錯誤",
|
||||
"jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確。",
|
||||
"jsonSaveError": "保存JSON配置失敗",
|
||||
"jsonSaveSuccess": "JSON配置已儲存",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"name": "名稱",
|
||||
"nameRequired": "請輸入伺服器名稱",
|
||||
"noServers": "未設定伺服器",
|
||||
"npx_list": {
|
||||
"actions": "操作",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
"description": "描述",
|
||||
"no_packages": "未找到包",
|
||||
"npm": "NPM",
|
||||
"package_name": "包名稱",
|
||||
"scope_placeholder": "輸入 npm 作用域 (例如 @your-org)",
|
||||
"scope_required": "請輸入 npm 作用域",
|
||||
"search": "搜索",
|
||||
"search_error": "搜索失敗",
|
||||
"title": "NPX 包列表",
|
||||
"usage": "用法",
|
||||
"version": "版本"
|
||||
},
|
||||
"serverPlural": "伺服器",
|
||||
"serverSingular": "伺服器",
|
||||
"title": "MCP 伺服器",
|
||||
"toggleError": "切換失敗",
|
||||
"type": "類型",
|
||||
"updateError": "更新伺服器失敗",
|
||||
"updateSuccess": "伺服器更新成功",
|
||||
"url": "URL"
|
||||
"updateError": "更新伺服器失敗",
|
||||
"url": "URL",
|
||||
"toggleError": "切換失敗",
|
||||
"dependenciesInstalling": "正在安裝相依套件...",
|
||||
"dependenciesInstall": "安裝相依套件",
|
||||
"installSuccess": "相依套件安裝成功",
|
||||
"installError": "安裝相依套件失敗",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"install": "安裝",
|
||||
"npx_list": {
|
||||
"title": "NPX 包列表",
|
||||
"desc": "搜索並添加 npm 包作為 MCP 服務",
|
||||
"scope_placeholder": "輸入 npm 作用域 (例如 @your-org)",
|
||||
"search": "搜索",
|
||||
"package_name": "包名稱",
|
||||
"description": "描述",
|
||||
"usage": "用法",
|
||||
"npm": "NPM",
|
||||
"version": "版本",
|
||||
"actions": "操作",
|
||||
"scope_required": "請輸入 npm 作用域",
|
||||
"no_packages": "未找到包",
|
||||
"search_error": "搜索失敗"
|
||||
},
|
||||
"editJson": "編輯JSON",
|
||||
"jsonModeHint": "編輯MCP伺服器配置的JSON表示。保存前請確保格式正確。",
|
||||
"jsonFormatError": "JSON格式錯誤",
|
||||
"jsonSaveSuccess": "JSON配置已儲存",
|
||||
"jsonSaveError": "保存JSON配置失敗"
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
@@ -961,10 +939,6 @@
|
||||
"messages.math_engine": "Markdown 渲染輸入訊息",
|
||||
"messages.metrics": "首字延遲 {{time_first_token_millsec}}ms | 每秒 {{token_speed}} tokens",
|
||||
"messages.model.title": "模型設定",
|
||||
"messages.navigation": "訊息導航",
|
||||
"messages.navigation.anchor": "對話錨點",
|
||||
"messages.navigation.buttons": "上下按鈕",
|
||||
"messages.navigation.none": "不顯示",
|
||||
"messages.title": "訊息設定",
|
||||
"messages.use_serif_font": "使用襯線字型",
|
||||
"model": "預設模型",
|
||||
@@ -985,7 +959,7 @@
|
||||
"models.check.enabled": "開啟",
|
||||
"models.check.failed": "失敗",
|
||||
"models.check.keys_status_count": "通過:{{count_passed}}個密鑰,失敗:{{count_failed}}個密鑰",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}} 個模型完成健康檢查(其中 {{count_partial}} 個模型用某些密鑰無法訪問),{{count_failed}} 個模型完全無法訪問。",
|
||||
"models.check.model_status_summary": "{{provider}}: {{count_passed}}個模型通過所有密鑰,{{count_failed}}個模型未通過所有密鑰,{{count_partial}}個模型未通過某些密鑰",
|
||||
"models.check.no_api_keys": "未找到API密鑰,請先添加API密鑰。",
|
||||
"models.check.passed": "通過",
|
||||
"models.check.select_api_key": "選擇要使用的API密鑰:",
|
||||
@@ -1027,43 +1001,43 @@
|
||||
"check": "檢查",
|
||||
"check_all_keys": "檢查所有金鑰",
|
||||
"check_multiple_keys": "檢查多個 API 金鑰",
|
||||
"copilot": {
|
||||
"auth_failed": "Github Copilot認證失敗",
|
||||
"auth_success": "Github Copilot 認證成功",
|
||||
"auth_success_title": "認證成功",
|
||||
"code_failed": "獲取 Device Code失敗,請重試",
|
||||
"code_generated_desc": "請將設備代碼複製到下面的瀏覽器連結中",
|
||||
"code_generated_title": "獲取設備代碼",
|
||||
"confirm_login": "過度使用可能會導致您的 Github 帳號遭到封號,請謹慎使用!!!!",
|
||||
"confirm_title": "風險警告",
|
||||
"connect": "連接 Github",
|
||||
"custom_headers": "自訂請求標頭",
|
||||
"description": "您的 Github 帳號需要訂閱 Copilot",
|
||||
"expand": "展開",
|
||||
"headers_description": "自訂請求標頭(json格式)",
|
||||
"invalid_json": "JSON 格式錯誤",
|
||||
"login": "登入 Github",
|
||||
"logout": "退出 Github",
|
||||
"logout_failed": "退出失敗,請重試",
|
||||
"logout_success": "已成功登出",
|
||||
"model_setting": "模型設定",
|
||||
"open_verification_first": "請先點擊上方連結訪問驗證頁面",
|
||||
"rate_limit": "速率限制",
|
||||
"tooltip": "使用 Github Copilot 需要先登入 Github"
|
||||
},
|
||||
"delete.content": "確定要刪除此提供者嗎?",
|
||||
"delete.title": "刪除提供者",
|
||||
"docs_check": "檢查",
|
||||
"docs_more_details": "檢視更多細節",
|
||||
"get_api_key": "點選這裡取得金鑰",
|
||||
"is_not_support_array_content": "開啟相容模式",
|
||||
"no_models": "請先新增模型再檢查 API 連接",
|
||||
"not_checked": "未檢查",
|
||||
"remove_duplicate_keys": "移除重複金鑰",
|
||||
"remove_invalid_keys": "刪除無效金鑰",
|
||||
"search": "搜尋模型平臺...",
|
||||
"search_placeholder": "搜尋模型 ID 或名稱",
|
||||
"title": "模型提供者"
|
||||
"title": "模型提供者",
|
||||
"is_not_support_array_content": "開啟相容模式",
|
||||
"copilot": {
|
||||
"tooltip": "使用 Github Copilot 需要先登入 Github",
|
||||
"description": "您的 Github 帳號需要訂閱 Copilot",
|
||||
"login": "登入 Github",
|
||||
"connect": "連接 Github",
|
||||
"logout": "退出 Github",
|
||||
"auth_success_title": "認證成功",
|
||||
"code_generated_title": "獲取設備代碼",
|
||||
"code_generated_desc": "請將設備代碼複製到下面的瀏覽器連結中",
|
||||
"code_failed": "獲取 Device Code失敗,請重試",
|
||||
"auth_success": "Github Copilot 認證成功",
|
||||
"auth_failed": "Github Copilot認證失敗",
|
||||
"logout_success": "已成功登出",
|
||||
"logout_failed": "退出失敗,請重試",
|
||||
"confirm_title": "風險警告",
|
||||
"confirm_login": "過度使用可能會導致您的 Github 帳號遭到封號,請謹慎使用!!!!",
|
||||
"rate_limit": "速率限制",
|
||||
"custom_headers": "自訂請求標頭",
|
||||
"headers_description": "自訂請求標頭(json格式)",
|
||||
"expand": "展開",
|
||||
"model_setting": "模型設定",
|
||||
"invalid_json": "JSON 格式錯誤",
|
||||
"open_verification_first": "請先點擊上方連結訪問驗證頁面"
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"mode": {
|
||||
@@ -1078,9 +1052,9 @@
|
||||
"quickAssistant": {
|
||||
"click_tray_to_show": "點選工具列圖示啟動",
|
||||
"enable_quick_assistant": "啟用快捷助手",
|
||||
"read_clipboard_at_startup": "啟動時讀取剪貼簿",
|
||||
"title": "快捷助手",
|
||||
"use_shortcut_to_show": "右鍵點選工具列圖示或使用快捷鍵啟動"
|
||||
"use_shortcut_to_show": "右鍵點選工具列圖示或使用快捷鍵啟動",
|
||||
"read_clipboard_at_startup": "啟動時讀取剪貼簿"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action": "操作",
|
||||
@@ -1117,18 +1091,14 @@
|
||||
"topic.position.left": "左側",
|
||||
"topic.position.right": "右側",
|
||||
"topic.show.time": "顯示話題時間",
|
||||
"tray.onclose": "關閉時最小化到系统匣",
|
||||
"tray.show": "顯示系统匣圖示",
|
||||
"tray.title": "系统匣",
|
||||
"tray.title": "啟用系統工具列圖示",
|
||||
"websearch": {
|
||||
"blacklist": "黑名單",
|
||||
"blacklist_description": "以下網站不會出現在搜尋結果中",
|
||||
"blacklist_tooltip": "請使用以下格式 (換行符號分隔)\nexample.com\nhttps://www.example.com\nhttps://example.com\n*://*.example.com",
|
||||
"check": "檢查",
|
||||
"check_failed": "驗證失敗",
|
||||
"check_success": "驗證成功",
|
||||
"enhance_mode": "搜索增強模式",
|
||||
"enhance_mode_tooltip": "使用預設模型提取關鍵詞後搜索",
|
||||
"check_failed": "驗證失敗",
|
||||
"get_api_key": "點選這裡取得金鑰",
|
||||
"no_provider_selected": "請選擇搜尋服務商後再檢查",
|
||||
"search_max_result": "搜尋結果個數",
|
||||
@@ -1136,6 +1106,8 @@
|
||||
"search_provider_placeholder": "選擇一個搜尋服務商",
|
||||
"search_result_default": "預設",
|
||||
"search_with_time": "搜尋包含日期",
|
||||
"enhance_mode": "搜索增強模式",
|
||||
"enhance_mode_tooltip": "使用預設模型提取關鍵詞後搜索",
|
||||
"tavily": {
|
||||
"api_key": "Tavily API 金鑰",
|
||||
"api_key.placeholder": "請輸入 Tavily API 金鑰",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
本目录文件使用机器翻译,请勿编辑
|
||||
This directory file is machine translated, please do not edit
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,390 +1,250 @@
|
||||
export type GroupTranslations = {
|
||||
[key: string]: {
|
||||
'el-GR': string
|
||||
'en-US': string
|
||||
'es-ES': string
|
||||
'fr-FR': string
|
||||
'zh-CN': string
|
||||
'zh-TW': string
|
||||
'ru-RU': string
|
||||
'ja-JP': string
|
||||
'pt-PT': string
|
||||
}
|
||||
}
|
||||
|
||||
export const groupTranslations: GroupTranslations = {
|
||||
我的: {
|
||||
'el-GR': 'Τα δικά μου',
|
||||
'en-US': 'My Agents',
|
||||
'es-ES': 'Mis agentes',
|
||||
'fr-FR': 'Mes agents',
|
||||
'zh-CN': '我的',
|
||||
'zh-TW': '我的',
|
||||
'ru-RU': 'Мои агенты',
|
||||
'ja-JP': '私のエージェント',
|
||||
'pt-PT': 'Meus Agentes'
|
||||
'ja-JP': '私のエージェント'
|
||||
},
|
||||
职业: {
|
||||
'el-GR': 'Επαγγελμα',
|
||||
'en-US': 'Career',
|
||||
'es-ES': 'Profesional',
|
||||
'fr-FR': 'Professionnel',
|
||||
'zh-CN': '职业',
|
||||
'zh-TW': '職業',
|
||||
'ru-RU': 'Карьера',
|
||||
'ja-JP': 'キャリア',
|
||||
'pt-PT': 'Profissional'
|
||||
'ja-JP': 'キャリア'
|
||||
},
|
||||
商业: {
|
||||
'el-GR': 'Εμπορικός',
|
||||
'en-US': 'Business',
|
||||
'es-ES': 'Negocio',
|
||||
'fr-FR': 'Commercial',
|
||||
'zh-CN': '商业',
|
||||
'zh-TW': '商業',
|
||||
'ru-RU': 'Бизнес',
|
||||
'ja-JP': 'ビジネス',
|
||||
'pt-PT': 'Negócio'
|
||||
'ja-JP': 'ビジネス'
|
||||
},
|
||||
工具: {
|
||||
'el-GR': 'Εργαλεία',
|
||||
'en-US': 'Tools',
|
||||
'es-ES': 'Herramientas',
|
||||
'fr-FR': 'Outils',
|
||||
'zh-CN': '工具',
|
||||
'zh-TW': '工具',
|
||||
'ru-RU': 'Инструменты',
|
||||
'ja-JP': 'ツール',
|
||||
'pt-PT': 'Ferramentas'
|
||||
'ja-JP': 'ツール'
|
||||
},
|
||||
语言: {
|
||||
'el-GR': 'Γλώσσα',
|
||||
'en-US': 'Language',
|
||||
'es-ES': 'Idioma',
|
||||
'fr-FR': 'Langue',
|
||||
'zh-CN': '语言',
|
||||
'zh-TW': '語言',
|
||||
'ru-RU': 'Язык',
|
||||
'ja-JP': '言語',
|
||||
'pt-PT': 'Idioma'
|
||||
'ja-JP': '言語'
|
||||
},
|
||||
办公: {
|
||||
'el-GR': 'Γραφείο',
|
||||
'en-US': 'Office',
|
||||
'es-ES': 'Oficina',
|
||||
'fr-FR': 'Bureau',
|
||||
'zh-CN': '办公',
|
||||
'zh-TW': '辦公',
|
||||
'ru-RU': 'Офис',
|
||||
'ja-JP': 'オフィス',
|
||||
'pt-PT': 'Escritório'
|
||||
'ja-JP': 'オフィス'
|
||||
},
|
||||
通用: {
|
||||
'el-GR': 'Γενικά',
|
||||
'en-US': 'General',
|
||||
'es-ES': 'General',
|
||||
'fr-FR': 'Général',
|
||||
'zh-CN': '通用',
|
||||
'zh-TW': '通用',
|
||||
'ru-RU': 'Общее',
|
||||
'ja-JP': '一般',
|
||||
'pt-PT': 'Geral'
|
||||
'ja-JP': '一般'
|
||||
},
|
||||
写作: {
|
||||
'el-GR': 'Γράφημα',
|
||||
'en-US': 'Writing',
|
||||
'es-ES': 'Escritura',
|
||||
'fr-FR': 'Écriture',
|
||||
'zh-CN': '写作',
|
||||
'zh-TW': '寫作',
|
||||
'ru-RU': 'Письмо',
|
||||
'ja-JP': '書き込み',
|
||||
'pt-PT': 'Escrita'
|
||||
'ja-JP': '書き込み'
|
||||
},
|
||||
精选: {
|
||||
'el-GR': 'Επιλεγμένο',
|
||||
'en-US': 'Featured',
|
||||
'es-ES': 'Destacado',
|
||||
'fr-FR': 'Sélection',
|
||||
'zh-CN': '精选',
|
||||
'zh-TW': '精選',
|
||||
'ru-RU': 'Избранное',
|
||||
'ja-JP': '特集',
|
||||
'pt-PT': 'Destaque'
|
||||
'ja-JP': '特集'
|
||||
},
|
||||
编程: {
|
||||
'el-GR': 'Προγραμματισμός',
|
||||
'en-US': 'Programming',
|
||||
'es-ES': 'Programación',
|
||||
'fr-FR': 'Programmation',
|
||||
'zh-CN': '编程',
|
||||
'zh-TW': '編程',
|
||||
'ru-RU': 'Программирование',
|
||||
'ja-JP': 'プログラミング',
|
||||
'pt-PT': 'Programação'
|
||||
'ja-JP': 'プログラミング'
|
||||
},
|
||||
情感: {
|
||||
'el-GR': 'Αίσθημα',
|
||||
'en-US': 'Emotion',
|
||||
'es-ES': 'Emoción',
|
||||
'fr-FR': 'Émotion',
|
||||
'zh-CN': '情感',
|
||||
'zh-TW': '情感',
|
||||
'ru-RU': 'Эмоции',
|
||||
'ja-JP': '感情',
|
||||
'pt-PT': 'Emoção'
|
||||
'ja-JP': '感情'
|
||||
},
|
||||
教育: {
|
||||
'el-GR': 'Εκπαίδευση',
|
||||
'en-US': 'Education',
|
||||
'es-ES': 'Educación',
|
||||
'fr-FR': 'Éducation',
|
||||
'zh-CN': '教育',
|
||||
'zh-TW': '教育',
|
||||
'ru-RU': 'Образование',
|
||||
'ja-JP': '教育',
|
||||
'pt-PT': 'Educação'
|
||||
'ja-JP': '教育'
|
||||
},
|
||||
创意: {
|
||||
'el-GR': 'Κreativiteit',
|
||||
'en-US': 'Creative',
|
||||
'es-ES': 'Creativo',
|
||||
'fr-FR': 'Créatif',
|
||||
'zh-CN': '创意',
|
||||
'zh-TW': '創意',
|
||||
'ru-RU': 'Креатив',
|
||||
'ja-JP': 'クリエイティブ',
|
||||
'pt-PT': 'Criativo'
|
||||
'ja-JP': 'クリエイティブ'
|
||||
},
|
||||
学术: {
|
||||
'el-GR': 'Ακαδημικός',
|
||||
'en-US': 'Academic',
|
||||
'es-ES': 'Académico',
|
||||
'fr-FR': 'Académique',
|
||||
'zh-CN': '学术',
|
||||
'zh-TW': '學術',
|
||||
'ru-RU': 'Академический',
|
||||
'ja-JP': 'アカデミック',
|
||||
'pt-PT': 'Académico'
|
||||
'ja-JP': 'アカデミック'
|
||||
},
|
||||
设计: {
|
||||
'el-GR': 'Δημιουργικό',
|
||||
'en-US': 'Design',
|
||||
'es-ES': 'Diseño',
|
||||
'fr-FR': 'Design',
|
||||
'zh-CN': '设计',
|
||||
'zh-TW': '設計',
|
||||
'ru-RU': 'Дизайн',
|
||||
'ja-JP': 'デザイン',
|
||||
'pt-PT': 'Design'
|
||||
'ja-JP': 'デザイン'
|
||||
},
|
||||
艺术: {
|
||||
'el-GR': 'Τέχνη',
|
||||
'en-US': 'Art',
|
||||
'es-ES': 'Arte',
|
||||
'fr-FR': 'Art',
|
||||
'zh-CN': '艺术',
|
||||
'zh-TW': '藝術',
|
||||
'ru-RU': 'Искусство',
|
||||
'ja-JP': 'アート',
|
||||
'pt-PT': 'Arte'
|
||||
'ja-JP': 'アート'
|
||||
},
|
||||
娱乐: {
|
||||
'el-GR': 'Αναψυχή',
|
||||
'en-US': 'Entertainment',
|
||||
'es-ES': 'Entretenimiento',
|
||||
'fr-FR': 'Divertissement',
|
||||
'zh-CN': '娱乐',
|
||||
'zh-TW': '娛樂',
|
||||
'ru-RU': 'Развлечения',
|
||||
'ja-JP': 'エンターテイメント',
|
||||
'pt-PT': 'Entretenimento'
|
||||
'ja-JP': 'エンターテイメント'
|
||||
},
|
||||
生活: {
|
||||
'el-GR': 'Ζωή',
|
||||
'en-US': 'Life',
|
||||
'es-ES': 'Vida',
|
||||
'fr-FR': 'Vie',
|
||||
'zh-CN': '生活',
|
||||
'zh-TW': '生活',
|
||||
'ru-RU': 'Жизнь',
|
||||
'ja-JP': '生活',
|
||||
'pt-PT': 'Vida'
|
||||
'ja-JP': '生活'
|
||||
},
|
||||
医疗: {
|
||||
'el-GR': 'Υγεία',
|
||||
'en-US': 'Medical',
|
||||
'es-ES': 'Médico',
|
||||
'fr-FR': 'Médical',
|
||||
'zh-CN': '医疗',
|
||||
'zh-TW': '醫療',
|
||||
'ru-RU': 'Медицина',
|
||||
'ja-JP': '医療',
|
||||
'pt-PT': 'Saúde'
|
||||
'ja-JP': '医療'
|
||||
},
|
||||
游戏: {
|
||||
'el-GR': 'Παιχνίδια',
|
||||
'en-US': 'Games',
|
||||
'es-ES': 'Juegos',
|
||||
'fr-FR': 'Jeux',
|
||||
'zh-CN': '游戏',
|
||||
'zh-TW': '遊戲',
|
||||
'ru-RU': 'Игры',
|
||||
'ja-JP': 'ゲーム',
|
||||
'pt-PT': 'Jogos'
|
||||
'ja-JP': 'ゲーム'
|
||||
},
|
||||
翻译: {
|
||||
'el-GR': 'Γραφήματα',
|
||||
'en-US': 'Translation',
|
||||
'es-ES': 'Traducción',
|
||||
'fr-FR': 'Traduction',
|
||||
'zh-CN': '翻译',
|
||||
'zh-TW': '翻譯',
|
||||
'ru-RU': 'Перевод',
|
||||
'ja-JP': '翻訳',
|
||||
'pt-PT': 'Tradução'
|
||||
'ja-JP': '翻訳'
|
||||
},
|
||||
音乐: {
|
||||
'el-GR': 'Μουσική',
|
||||
'en-US': 'Music',
|
||||
'es-ES': 'Música',
|
||||
'fr-FR': 'Musique',
|
||||
'zh-CN': '音乐',
|
||||
'zh-TW': '音樂',
|
||||
'ru-RU': 'Музыка',
|
||||
'ja-JP': '音楽',
|
||||
'pt-PT': 'Música'
|
||||
'ja-JP': '音楽'
|
||||
},
|
||||
点评: {
|
||||
'el-GR': 'Αξιολόγηση',
|
||||
'en-US': 'Review',
|
||||
'es-ES': 'Revisión',
|
||||
'fr-FR': 'Avis',
|
||||
'zh-CN': '点评',
|
||||
'zh-TW': '點評',
|
||||
'ru-RU': 'Обзор',
|
||||
'ja-JP': 'レビュー',
|
||||
'pt-PT': 'Revisão'
|
||||
'ja-JP': 'レビュー'
|
||||
},
|
||||
文案: {
|
||||
'el-GR': 'Γραφήματα',
|
||||
'en-US': 'Copywriting',
|
||||
'es-ES': 'Redacción',
|
||||
'fr-FR': 'Rédaction',
|
||||
'zh-CN': '文案',
|
||||
'zh-TW': '文案',
|
||||
'ru-RU': 'Копирайтинг',
|
||||
'ja-JP': 'コピーライティング',
|
||||
'pt-PT': 'Escrita'
|
||||
'ja-JP': 'コピーライティング'
|
||||
},
|
||||
百科: {
|
||||
'el-GR': 'Εγκυκλοπαίδεια',
|
||||
'en-US': 'Encyclopedia',
|
||||
'es-ES': 'Enciclopedia',
|
||||
'fr-FR': 'Encyclopédie',
|
||||
'zh-CN': '百科',
|
||||
'zh-TW': '百科',
|
||||
'ru-RU': 'Энциклопедия',
|
||||
'ja-JP': '百科事典',
|
||||
'pt-PT': 'Enciclopédia'
|
||||
'ja-JP': '百科事典'
|
||||
},
|
||||
健康: {
|
||||
'el-GR': 'Υγεία',
|
||||
'en-US': 'Health',
|
||||
'es-ES': 'Salud',
|
||||
'fr-FR': 'Santé',
|
||||
'zh-CN': '健康',
|
||||
'zh-TW': '健康',
|
||||
'ru-RU': 'Здоровье',
|
||||
'ja-JP': '健康',
|
||||
'pt-PT': 'Saúde'
|
||||
'ja-JP': '健康'
|
||||
},
|
||||
营销: {
|
||||
'el-GR': 'Μάρκετινγκ',
|
||||
'en-US': 'Marketing',
|
||||
'es-ES': 'Marketing',
|
||||
'fr-FR': 'Marketing',
|
||||
'zh-CN': '营销',
|
||||
'zh-TW': '營銷',
|
||||
'ru-RU': 'Маркетинг',
|
||||
'ja-JP': 'マーケティング',
|
||||
'pt-PT': 'Marketing'
|
||||
'ja-JP': 'マーケティング'
|
||||
},
|
||||
科学: {
|
||||
'el-GR': 'Επιστήμη',
|
||||
'en-US': 'Science',
|
||||
'es-ES': 'Ciencia',
|
||||
'fr-FR': 'Science',
|
||||
'zh-CN': '科学',
|
||||
'zh-TW': '科學',
|
||||
'ru-RU': 'Наука',
|
||||
'ja-JP': '科学',
|
||||
'pt-PT': 'Ciência'
|
||||
'ja-JP': '科学'
|
||||
},
|
||||
分析: {
|
||||
'el-GR': 'Ανάλυση',
|
||||
'en-US': 'Analysis',
|
||||
'es-ES': 'Análisis',
|
||||
'fr-FR': 'Analyse',
|
||||
'zh-CN': '分析',
|
||||
'zh-TW': '分析',
|
||||
'ru-RU': 'Анализ',
|
||||
'ja-JP': '分析',
|
||||
'pt-PT': 'Análise'
|
||||
'ja-JP': '分析'
|
||||
},
|
||||
法律: {
|
||||
'el-GR': 'Νόμος',
|
||||
'en-US': 'Legal',
|
||||
'es-ES': 'Legal',
|
||||
'fr-FR': 'Légal',
|
||||
'zh-CN': '法律',
|
||||
'zh-TW': '法律',
|
||||
'ru-RU': 'Право',
|
||||
'ja-JP': '法律',
|
||||
'pt-PT': 'Legal'
|
||||
'ja-JP': '法律'
|
||||
},
|
||||
咨询: {
|
||||
'el-GR': 'Συμβουλή',
|
||||
'en-US': 'Consulting',
|
||||
'es-ES': 'Consultoría',
|
||||
'fr-FR': 'Consultation',
|
||||
'zh-CN': '咨询',
|
||||
'zh-TW': '諮詢',
|
||||
'ru-RU': 'Консалтинг',
|
||||
'ja-JP': 'コンサルティング',
|
||||
'pt-PT': 'Consultoria'
|
||||
'ja-JP': 'コンサルティング'
|
||||
},
|
||||
金融: {
|
||||
'el-GR': 'Φορολογία',
|
||||
'en-US': 'Finance',
|
||||
'es-ES': 'Finanzas',
|
||||
'fr-FR': 'Finance',
|
||||
'zh-CN': '金融',
|
||||
'zh-TW': '金融',
|
||||
'ru-RU': 'Финансы',
|
||||
'ja-JP': '金融',
|
||||
'pt-PT': 'Finanças'
|
||||
'ja-JP': '金融'
|
||||
},
|
||||
旅游: {
|
||||
'el-GR': 'Τουρισμός',
|
||||
'en-US': 'Travel',
|
||||
'es-ES': 'Viajes',
|
||||
'fr-FR': 'Voyages',
|
||||
'zh-CN': '旅游',
|
||||
'zh-TW': '旅遊',
|
||||
'ru-RU': 'Путешествия',
|
||||
'ja-JP': '旅行',
|
||||
'pt-PT': 'Viagens'
|
||||
'ja-JP': '旅行'
|
||||
},
|
||||
管理: {
|
||||
'el-GR': 'Διοίκηση',
|
||||
'en-US': 'Management',
|
||||
'es-ES': 'Gestión',
|
||||
'fr-FR': 'Gestion',
|
||||
'zh-CN': '管理',
|
||||
'zh-TW': '管理',
|
||||
'ru-RU': 'Управление',
|
||||
'ja-JP': '管理',
|
||||
'pt-PT': 'Gestão'
|
||||
'ja-JP': '管理'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { PictureOutlined } from '@ant-design/icons'
|
||||
import { isGenerateImageModel } from '@renderer/config/models'
|
||||
import { Assistant, Model } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
model: Model
|
||||
ToolbarButton: any
|
||||
onEnableGenerateImage: () => void
|
||||
}
|
||||
|
||||
const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEnableGenerateImage }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isGenerateImageModel(model)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={
|
||||
isGenerateImageModel(model) ? t('chat.input.generate_image') : t('chat.input.generate_image_not_supported')
|
||||
}
|
||||
arrow>
|
||||
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
|
||||
<PictureOutlined style={{ color: assistant.enableGenerateImage ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default GenerateImageButton
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isFunctionCallingModel, isGenerateImageModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { isFunctionCallingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
@@ -44,7 +44,6 @@ import styled from 'styled-components'
|
||||
import NarrowLayout from '../Messages/NarrowLayout'
|
||||
import AttachmentButton from './AttachmentButton'
|
||||
import AttachmentPreview from './AttachmentPreview'
|
||||
import GenerateImageButton from './GenerateImageButton'
|
||||
import KnowledgeBaseButton from './KnowledgeBaseButton'
|
||||
import MCPToolsButton from './MCPToolsButton'
|
||||
import MentionModelsButton from './MentionModelsButton'
|
||||
@@ -627,6 +626,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
}
|
||||
|
||||
const onEnableWebSearch = () => {
|
||||
console.log(assistant)
|
||||
if (!isWebSearchModel(model)) {
|
||||
if (!WebSearchService.isWebSearchEnabled()) {
|
||||
window.modal.confirm({
|
||||
@@ -645,17 +645,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })
|
||||
}
|
||||
|
||||
const onEnableGenerateImage = () => {
|
||||
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWebSearchModel(model) && !WebSearchService.isWebSearchEnabled() && assistant.enableWebSearch) {
|
||||
updateAssistant({ ...assistant, enableWebSearch: false })
|
||||
}
|
||||
if (!isGenerateImageModel(model) && assistant.enableGenerateImage) {
|
||||
updateAssistant({ ...assistant, enableGenerateImage: false })
|
||||
}
|
||||
}, [assistant, model, updateAssistant])
|
||||
|
||||
const resetHeight = () => {
|
||||
@@ -723,6 +716,11 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||
<MentionModelsButton
|
||||
mentionModels={mentionModels}
|
||||
onMentionModel={(model) => onMentionModel(model, mentionFromKeyboard)}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||
<ToolbarButton type="text" onClick={onEnableWebSearch}>
|
||||
<GlobalOutlined
|
||||
@@ -745,17 +743,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
)}
|
||||
<GenerateImageButton
|
||||
model={model}
|
||||
assistant={assistant}
|
||||
onEnableGenerateImage={onEnableGenerateImage}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
<MentionModelsButton
|
||||
mentionModels={mentionModels}
|
||||
onMentionModel={(model) => onMentionModel(model, mentionFromKeyboard)}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} arrow>
|
||||
<Popconfirm
|
||||
title={t('chat.input.clear.content')}
|
||||
|
||||
@@ -1,614 +0,0 @@
|
||||
import '@xyflow/react/dist/style.css'
|
||||
|
||||
import { RobotOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { selectTopicMessages } from '@renderer/store/messages'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Controls, Handle, MiniMap, ReactFlow, ReactFlowProvider } from '@xyflow/react'
|
||||
import { Edge, Node, NodeTypes, Position, useEdgesState, useNodesState } from '@xyflow/react'
|
||||
import { Avatar, Spin, Tooltip } from 'antd'
|
||||
import { isEqual } from 'lodash'
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
// 定义Tooltip相关样式组件
|
||||
const TooltipContent = styled.div`
|
||||
max-width: 300px;
|
||||
`
|
||||
|
||||
const TooltipTitle = styled.div`
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding-bottom: 4px;
|
||||
`
|
||||
|
||||
const TooltipBody = styled.div`
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
|
||||
const TooltipFooter = styled.div`
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
// 自定义节点组件
|
||||
const CustomNode: FC<{ data: any }> = ({ data }) => {
|
||||
const { t } = useTranslation()
|
||||
const nodeType = data.type
|
||||
let borderColor = 'var(--color-border)'
|
||||
let title = ''
|
||||
let backgroundColor = 'var(--bg-color)'
|
||||
let gradientColor = 'rgba(0, 0, 0, 0.03)'
|
||||
let avatar: JSX.Element | null = null
|
||||
|
||||
// 根据消息类型设置不同的样式和图标
|
||||
if (nodeType === 'user') {
|
||||
borderColor = 'var(--color-icon)'
|
||||
backgroundColor = 'rgba(var(--color-info-rgb), 0.03)'
|
||||
gradientColor = 'rgba(var(--color-info-rgb), 0.08)'
|
||||
title = data.userName || t('chat.history.user_node')
|
||||
|
||||
// 用户头像
|
||||
if (data.userAvatar) {
|
||||
avatar = <Avatar src={data.userAvatar} alt={title} />
|
||||
} else {
|
||||
avatar = <Avatar icon={<UserOutlined />} style={{ backgroundColor: 'var(--color-info)' }} />
|
||||
}
|
||||
} else if (nodeType === 'assistant') {
|
||||
borderColor = 'var(--color-primary)'
|
||||
backgroundColor = 'rgba(var(--color-primary-rgb), 0.03)'
|
||||
gradientColor = 'rgba(var(--color-primary-rgb), 0.08)'
|
||||
title = `${data.model || t('chat.history.assistant_node')}`
|
||||
|
||||
// 模型头像
|
||||
if (data.modelInfo) {
|
||||
avatar = <ModelAvatar model={data.modelInfo} size={32} />
|
||||
} else if (data.modelId) {
|
||||
const modelLogo = getModelLogo(data.modelId)
|
||||
avatar = (
|
||||
<Avatar
|
||||
src={modelLogo}
|
||||
icon={!modelLogo ? <RobotOutlined /> : undefined}
|
||||
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
avatar = <Avatar icon={<RobotOutlined />} style={{ backgroundColor: 'var(--color-primary)' }} />
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点点击事件,滚动到对应消息
|
||||
const handleNodeClick = () => {
|
||||
if (data.messageId) {
|
||||
// 创建一个自定义事件来定位消息并切换标签
|
||||
const customEvent = new CustomEvent('flow-navigate-to-message', {
|
||||
detail: {
|
||||
messageId: data.messageId,
|
||||
modelId: data.modelId,
|
||||
modelName: data.model,
|
||||
nodeType: nodeType
|
||||
},
|
||||
bubbles: true
|
||||
})
|
||||
|
||||
// 让监听器处理标签切换
|
||||
document.dispatchEvent(customEvent)
|
||||
|
||||
setTimeout(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + data.messageId)
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏连接点的通用样式
|
||||
const handleStyle = {
|
||||
opacity: 0,
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: 'transparent',
|
||||
border: 'none'
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<TooltipContent>
|
||||
<TooltipTitle>{title}</TooltipTitle>
|
||||
<TooltipBody>{data.content}</TooltipBody>
|
||||
<TooltipFooter>{t('chat.history.click_to_navigate')}</TooltipFooter>
|
||||
</TooltipContent>
|
||||
}
|
||||
placement="top"
|
||||
color="rgba(0, 0, 0, 0.85)"
|
||||
mouseEnterDelay={0.3}
|
||||
mouseLeaveDelay={0.1}
|
||||
destroyTooltipOnHide>
|
||||
<CustomNodeContainer
|
||||
style={{
|
||||
borderColor,
|
||||
background: `linear-gradient(135deg, ${backgroundColor} 0%, ${gradientColor} 100%)`,
|
||||
boxShadow: `0 4px 10px rgba(0, 0, 0, 0.1), 0 0 0 2px ${borderColor}40`
|
||||
}}
|
||||
onClick={handleNodeClick}>
|
||||
<Handle type="target" position={Position.Top} style={handleStyle} isConnectable={false} />
|
||||
<Handle type="target" position={Position.Left} style={handleStyle} isConnectable={false} />
|
||||
|
||||
<NodeHeader>
|
||||
<NodeAvatar>{avatar}</NodeAvatar>
|
||||
<NodeTitle>{title}</NodeTitle>
|
||||
</NodeHeader>
|
||||
<NodeContent title={data.content}>{data.content}</NodeContent>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} style={handleStyle} isConnectable={false} />
|
||||
<Handle type="source" position={Position.Right} style={handleStyle} isConnectable={false} />
|
||||
</CustomNodeContainer>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// 创建自定义节点类型
|
||||
const nodeTypes: NodeTypes = { custom: CustomNode }
|
||||
|
||||
interface ChatFlowHistoryProps {
|
||||
conversationId?: string
|
||||
}
|
||||
|
||||
// 定义节点和边的类型
|
||||
type FlowNode = Node<any, string>
|
||||
type FlowEdge = Edge<any>
|
||||
|
||||
// 统一的边样式
|
||||
const commonEdgeStyle = {
|
||||
stroke: 'var(--color-border)',
|
||||
strokeDasharray: '4,4',
|
||||
strokeWidth: 2
|
||||
}
|
||||
|
||||
// 统一的边配置
|
||||
const defaultEdgeOptions = {
|
||||
animated: true,
|
||||
style: commonEdgeStyle,
|
||||
type: 'step',
|
||||
markerEnd: undefined,
|
||||
zIndex: 5
|
||||
}
|
||||
|
||||
const ChatFlowHistory: FC<ChatFlowHistoryProps> = ({ conversationId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<any>([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<any>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { userName } = useSettings()
|
||||
|
||||
const topicId = conversationId
|
||||
|
||||
// 只在消息实际内容变化时更新,而不是属性变化(如foldSelected)
|
||||
const messages = useSelector(
|
||||
(state: RootState) => selectTopicMessages(state, topicId || ''),
|
||||
(prev, next) => {
|
||||
// 只比较消息的关键属性,忽略展示相关的属性(如foldSelected)
|
||||
if (prev.length !== next.length) return false
|
||||
|
||||
// 比较每条消息的内容和关键属性,忽略UI状态相关属性
|
||||
return prev.every((prevMsg, index) => {
|
||||
const nextMsg = next[index]
|
||||
return (
|
||||
prevMsg.id === nextMsg.id &&
|
||||
prevMsg.content === nextMsg.content &&
|
||||
prevMsg.role === nextMsg.role &&
|
||||
prevMsg.createdAt === nextMsg.createdAt &&
|
||||
prevMsg.askId === nextMsg.askId &&
|
||||
isEqual(prevMsg.model, nextMsg.model)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// 获取用户头像
|
||||
const userAvatar = useSelector((state: RootState) => state.runtime.avatar)
|
||||
|
||||
// 消息过滤
|
||||
const { userMessages, assistantMessages } = useMemo(() => {
|
||||
const userMsgs = messages.filter((msg) => msg.role === 'user')
|
||||
const assistantMsgs = messages.filter((msg) => msg.role === 'assistant')
|
||||
return { userMessages: userMsgs, assistantMessages: assistantMsgs }
|
||||
}, [messages])
|
||||
|
||||
const buildConversationFlowData = useCallback(() => {
|
||||
if (!topicId || !messages.length) return { nodes: [], edges: [] }
|
||||
|
||||
// 创建节点和边
|
||||
const flowNodes: FlowNode[] = []
|
||||
const flowEdges: FlowEdge[] = []
|
||||
|
||||
// 布局参数
|
||||
const verticalGap = 200
|
||||
const horizontalGap = 350
|
||||
const baseX = 150
|
||||
|
||||
// 如果没有任何消息可以显示,返回空结果
|
||||
if (userMessages.length === 0 && assistantMessages.length === 0) {
|
||||
return { nodes: [], edges: [] }
|
||||
}
|
||||
|
||||
// 为所有用户消息创建节点
|
||||
userMessages.forEach((message, index) => {
|
||||
const nodeId = `user-${message.id}`
|
||||
const yPosition = index * verticalGap * 2
|
||||
|
||||
// 获取用户名
|
||||
const userNameValue = userName || t('chat.history.user_node')
|
||||
|
||||
// 获取用户头像
|
||||
const msgUserAvatar = userAvatar || null
|
||||
|
||||
flowNodes.push({
|
||||
id: nodeId,
|
||||
type: 'custom',
|
||||
data: {
|
||||
userName: userNameValue,
|
||||
content: message.content,
|
||||
type: 'user',
|
||||
messageId: message.id,
|
||||
userAvatar: msgUserAvatar
|
||||
},
|
||||
position: { x: baseX, y: yPosition },
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top
|
||||
})
|
||||
|
||||
// 找到用户消息之后的助手回复
|
||||
const userMsgTime = new Date(message.createdAt).getTime()
|
||||
const relatedAssistantMsgs = assistantMessages.filter((aMsg) => {
|
||||
const aMsgTime = new Date(aMsg.createdAt).getTime()
|
||||
return (
|
||||
aMsgTime > userMsgTime &&
|
||||
(index === userMessages.length - 1 || aMsgTime < new Date(userMessages[index + 1].createdAt).getTime())
|
||||
)
|
||||
})
|
||||
|
||||
// 为相关的助手消息创建节点
|
||||
relatedAssistantMsgs.forEach((aMsg, aIndex) => {
|
||||
const assistantNodeId = `assistant-${aMsg.id}`
|
||||
const isMultipleResponses = relatedAssistantMsgs.length > 1
|
||||
const assistantX = baseX + (isMultipleResponses ? horizontalGap * aIndex : 0)
|
||||
const assistantY = yPosition + verticalGap
|
||||
|
||||
// 根据位置确定连接点位置
|
||||
let sourcePos = Position.Bottom // 默认向下输出
|
||||
let targetPos = Position.Top // 默认从上方输入
|
||||
|
||||
// 横向排列多个助手消息时调整连接点
|
||||
// 注意:现在所有助手节点都直接连接到用户节点,而不是相互连接
|
||||
if (isMultipleResponses) {
|
||||
// 所有助手节点都使用顶部作为输入点(从用户节点)
|
||||
targetPos = Position.Top
|
||||
|
||||
// 所有助手节点都使用底部作为输出点(到下一个用户节点)
|
||||
sourcePos = Position.Bottom
|
||||
}
|
||||
|
||||
const aMsgAny = aMsg as any
|
||||
|
||||
// 获取模型名称
|
||||
const modelName = (aMsgAny.model && aMsgAny.model.name) || t('chat.history.assistant_node')
|
||||
|
||||
// 获取模型ID
|
||||
const modelId = (aMsgAny.model && aMsgAny.model.id) || ''
|
||||
|
||||
// 完整的模型信息
|
||||
const modelInfo = aMsgAny.model as Model | undefined
|
||||
|
||||
flowNodes.push({
|
||||
id: assistantNodeId,
|
||||
type: 'custom',
|
||||
data: {
|
||||
model: modelName,
|
||||
content: aMsg.content,
|
||||
type: 'assistant',
|
||||
messageId: aMsg.id,
|
||||
modelId: modelId,
|
||||
modelInfo
|
||||
},
|
||||
position: { x: assistantX, y: assistantY },
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos
|
||||
})
|
||||
|
||||
// 连接消息 - 将每个助手节点直接连接到用户节点
|
||||
if (aIndex === 0) {
|
||||
// 连接用户消息到第一个助手回复
|
||||
flowEdges.push({
|
||||
id: `edge-${nodeId}-to-${assistantNodeId}`,
|
||||
source: nodeId,
|
||||
target: assistantNodeId
|
||||
})
|
||||
} else {
|
||||
// 直接连接用户消息到所有其他助手回复
|
||||
flowEdges.push({
|
||||
id: `edge-${nodeId}-to-${assistantNodeId}`,
|
||||
source: nodeId,
|
||||
target: assistantNodeId
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 连接相邻的用户消息
|
||||
if (index > 0) {
|
||||
const prevUserNodeId = `user-${userMessages[index - 1].id}`
|
||||
const prevUserTime = new Date(userMessages[index - 1].createdAt).getTime()
|
||||
|
||||
// 查找前一个用户消息的所有助手回复
|
||||
const prevAssistantMsgs = assistantMessages.filter((aMsg) => {
|
||||
const aMsgTime = new Date(aMsg.createdAt).getTime()
|
||||
return aMsgTime > prevUserTime && aMsgTime < userMsgTime
|
||||
})
|
||||
|
||||
if (prevAssistantMsgs.length > 0) {
|
||||
// 所有前一个用户的助手消息都连接到当前用户消息
|
||||
prevAssistantMsgs.forEach((aMsg) => {
|
||||
const assistantId = `assistant-${aMsg.id}`
|
||||
flowEdges.push({
|
||||
id: `edge-${assistantId}-to-${nodeId}`,
|
||||
source: assistantId,
|
||||
target: nodeId
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 如果没有助手消息,直接连接两个用户消息
|
||||
flowEdges.push({
|
||||
id: `edge-${prevUserNodeId}-to-${nodeId}`,
|
||||
source: prevUserNodeId,
|
||||
target: nodeId
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 处理孤立的助手消息(没有对应的用户消息)
|
||||
const orphanAssistantMsgs = assistantMessages.filter(
|
||||
(aMsg) => !flowNodes.some((node) => node.id === `assistant-${aMsg.id}`)
|
||||
)
|
||||
|
||||
if (orphanAssistantMsgs.length > 0) {
|
||||
// 在图表顶部添加这些孤立消息
|
||||
const startY = flowNodes.length > 0 ? Math.min(...flowNodes.map((node) => node.position.y)) - verticalGap * 2 : 0
|
||||
|
||||
orphanAssistantMsgs.forEach((aMsg, index) => {
|
||||
const assistantNodeId = `orphan-assistant-${aMsg.id}`
|
||||
|
||||
// 获取模型数据
|
||||
const aMsgAny = aMsg as any
|
||||
|
||||
// 获取模型名称
|
||||
const modelName = (aMsgAny.model && aMsgAny.model.name) || t('chat.history.assistant_node')
|
||||
|
||||
// 获取模型ID
|
||||
const modelId = (aMsgAny.model && aMsgAny.model.id) || ''
|
||||
|
||||
// 完整的模型信息
|
||||
const modelInfo = aMsgAny.model as Model | undefined
|
||||
|
||||
flowNodes.push({
|
||||
id: assistantNodeId,
|
||||
type: 'custom',
|
||||
data: {
|
||||
model: modelName,
|
||||
content: aMsg.content,
|
||||
type: 'assistant',
|
||||
messageId: aMsg.id,
|
||||
modelId: modelId,
|
||||
modelInfo
|
||||
},
|
||||
position: { x: baseX, y: startY - index * verticalGap },
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Top
|
||||
})
|
||||
|
||||
// 连接相邻的孤立消息
|
||||
if (index > 0) {
|
||||
const prevNodeId = `orphan-assistant-${orphanAssistantMsgs[index - 1].id}`
|
||||
flowEdges.push({
|
||||
id: `edge-${prevNodeId}-to-${assistantNodeId}`,
|
||||
source: prevNodeId,
|
||||
target: assistantNodeId
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { nodes: flowNodes, edges: flowEdges }
|
||||
}, [topicId, messages, userMessages, assistantMessages, t])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setTimeout(() => {
|
||||
const { nodes: flowNodes, edges: flowEdges } = buildConversationFlowData()
|
||||
setNodes([...flowNodes])
|
||||
setEdges([...flowEdges])
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
}, [buildConversationFlowData, setNodes, setEdges])
|
||||
|
||||
return (
|
||||
<FlowContainer>
|
||||
{loading ? (
|
||||
<LoadingContainer>
|
||||
<Spin size="large" />
|
||||
</LoadingContainer>
|
||||
) : nodes.length > 0 ? (
|
||||
<ReactFlowProvider>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
edgesFocusable={true}
|
||||
zoomOnDoubleClick={true}
|
||||
preventScrolling={true}
|
||||
elementsSelectable={true}
|
||||
selectNodesOnDrag={false}
|
||||
nodesFocusable={true}
|
||||
zoomOnScroll={true}
|
||||
panOnScroll={false}
|
||||
minZoom={0.4}
|
||||
maxZoom={1}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
fitView={true}
|
||||
fitViewOptions={{
|
||||
padding: 0.3,
|
||||
includeHiddenNodes: false,
|
||||
minZoom: 0.4,
|
||||
maxZoom: 1
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
className="react-flow-container">
|
||||
<Controls showInteractive={false} />
|
||||
<MiniMap
|
||||
nodeStrokeWidth={3}
|
||||
zoomable
|
||||
pannable
|
||||
nodeColor={(node) => (node.data.type === 'user' ? 'var(--color-info)' : 'var(--color-primary)')}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
) : (
|
||||
<EmptyContainer>
|
||||
<EmptyText>{t('chat.history.no_messages')}</EmptyText>
|
||||
</EmptyContainer>
|
||||
)}
|
||||
</FlowContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// 样式组件定义
|
||||
const FlowContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
`
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const EmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const EmptyText = styled.div`
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
const CustomNodeContainer = styled.div`
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid;
|
||||
width: 280px;
|
||||
height: 120px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 10px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 2px ${(props) => props.style?.borderColor || 'var(--color-border)'}80 !important;
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
/* 添加点击动画效果 */
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
`
|
||||
|
||||
const NodeHeader = styled.div`
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
||||
color: var(--color-text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
`
|
||||
|
||||
const NodeAvatar = styled.span`
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ant-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`
|
||||
|
||||
const NodeTitle = styled.span`
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const NodeContent = styled.div`
|
||||
margin: 2px 0;
|
||||
color: var(--color-text);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
padding: 3px;
|
||||
`
|
||||
|
||||
// 确保组件使用React.memo包装以减少不必要的重渲染
|
||||
export default memo(ChatFlowHistory, (prevProps, nextProps) => {
|
||||
return prevProps.conversationId === nextProps.conversationId
|
||||
})
|
||||
@@ -1,15 +1,10 @@
|
||||
import { DownOutlined, HistoryOutlined, UpOutlined } from '@ant-design/icons'
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { RootState } from '@renderer/store'
|
||||
import { selectCurrentTopicId } from '@renderer/store/messages'
|
||||
import { Button, Drawer, Tooltip } from 'antd'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ChatFlowHistory from './ChatFlowHistory'
|
||||
|
||||
interface ChatNavigationProps {
|
||||
containerId: string
|
||||
}
|
||||
@@ -19,8 +14,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [isNearButtons, setIsNearButtons] = useState(false)
|
||||
const [hideTimer, setHideTimer] = useState<NodeJS.Timeout | null>(null)
|
||||
const [showChatHistory, setShowChatHistory] = useState(false)
|
||||
const currentTopicId = useSelector((state: RootState) => selectCurrentTopicId(state))
|
||||
const lastMoveTime = useRef(0)
|
||||
const { topicPosition, showTopics } = useSettings()
|
||||
const showRightTopics = topicPosition === 'right' && showTopics
|
||||
@@ -66,15 +59,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
setHideTimer(timer)
|
||||
}, [])
|
||||
|
||||
const handleChatHistoryClick = () => {
|
||||
setShowChatHistory(true)
|
||||
resetHideTimer()
|
||||
}
|
||||
|
||||
const handleDrawerClose = () => {
|
||||
setShowChatHistory(false)
|
||||
}
|
||||
|
||||
const findUserMessages = () => {
|
||||
const container = document.getElementById(containerId)
|
||||
if (!container) return []
|
||||
@@ -275,58 +259,31 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationContainer
|
||||
$isVisible={isVisible}
|
||||
$right={right}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t('chat.navigation.prev')} placement="left">
|
||||
<NavigationButton
|
||||
type="text"
|
||||
icon={<UpOutlined />}
|
||||
onClick={handlePrevMessage}
|
||||
aria-label={t('chat.navigation.prev')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip title={t('chat.navigation.next')} placement="left">
|
||||
<NavigationButton
|
||||
type="text"
|
||||
icon={<DownOutlined />}
|
||||
onClick={handleNextMessage}
|
||||
aria-label={t('chat.navigation.next')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip title={t('chat.navigation.history')} placement="left">
|
||||
<NavigationButton
|
||||
type="text"
|
||||
icon={<HistoryOutlined />}
|
||||
onClick={handleChatHistoryClick}
|
||||
aria-label={t('chat.navigation.history')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</NavigationContainer>
|
||||
|
||||
<Drawer
|
||||
title={t('chat.history.title')}
|
||||
placement="right"
|
||||
onClose={handleDrawerClose}
|
||||
open={showChatHistory}
|
||||
width={680}
|
||||
destroyOnClose
|
||||
styles={{
|
||||
body: {
|
||||
padding: 0,
|
||||
height: 'calc(100% - 55px)'
|
||||
}
|
||||
}}>
|
||||
<ChatFlowHistory conversationId={currentTopicId || undefined} />
|
||||
</Drawer>
|
||||
</>
|
||||
<NavigationContainer
|
||||
$isVisible={isVisible}
|
||||
$right={right}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<ButtonGroup>
|
||||
<Tooltip title={t('chat.navigation.prev')} placement="left">
|
||||
<NavigationButton
|
||||
type="text"
|
||||
icon={<UpOutlined />}
|
||||
onClick={handlePrevMessage}
|
||||
aria-label={t('chat.navigation.prev')}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider />
|
||||
<Tooltip title={t('chat.navigation.next')} placement="left">
|
||||
<NavigationButton
|
||||
type="text"
|
||||
icon={<DownOutlined />}
|
||||
onClick={handleNextMessage}
|
||||
aria-label={t('chat.navigation.next')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelName } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateMessage } from '@renderer/store/messages'
|
||||
import { Message } from '@renderer/types'
|
||||
import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar } from 'antd'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
interface MessageLineProps {
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
if (isLocalAi) return AppLogo
|
||||
return modelId ? getModelLogo(modelId) : undefined
|
||||
}
|
||||
|
||||
const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
|
||||
const { t } = useTranslation()
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { userName } = useSettings()
|
||||
const messagesListRef = useRef<HTMLDivElement>(null)
|
||||
const messageItemsRef = useRef<Map<string, HTMLDivElement>>(new Map())
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [mouseY, setMouseY] = useState<number | null>(null)
|
||||
const { topicPosition, showTopics } = useSettings()
|
||||
const showRightTopics = topicPosition === 'right' && showTopics
|
||||
const right = showRightTopics ? 'calc(var(--topic-list-width) + 15px)' : '15px'
|
||||
|
||||
const [listOffsetY, setListOffsetY] = useState(0)
|
||||
const [containerHeight, setContainerHeight] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeight = () => {
|
||||
if (containerRef.current) {
|
||||
const parentElement = containerRef.current.parentElement
|
||||
if (parentElement) {
|
||||
setContainerHeight(parentElement.clientHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHeight()
|
||||
window.addEventListener('resize', updateHeight)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateHeight)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// 函数用于计算根据距离的变化值
|
||||
const calculateValueByDistance = useCallback(
|
||||
(itemId: string, maxValue: number) => {
|
||||
if (mouseY === null) return 0
|
||||
|
||||
const element = messageItemsRef.current.get(itemId)
|
||||
if (!element) return 0
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const centerY = rect.top + rect.height / 2
|
||||
const distance = Math.abs(centerY - (messagesListRef.current?.getBoundingClientRect().top || 0) - mouseY)
|
||||
const maxDistance = 100
|
||||
|
||||
return Math.max(0, maxValue * (1 - distance / maxDistance))
|
||||
},
|
||||
[mouseY]
|
||||
)
|
||||
|
||||
const getUserName = useCallback(
|
||||
(message: Message) => {
|
||||
if (isLocalAi && message.role !== 'user') {
|
||||
return APP_NAME
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
if (message.model) {
|
||||
return getModelName(message.model) || message.model.name || message.modelId || ''
|
||||
}
|
||||
|
||||
const modelId = getMessageModelId(message)
|
||||
return modelId || ''
|
||||
}
|
||||
|
||||
return userName || t('common.you')
|
||||
},
|
||||
[userName, t]
|
||||
)
|
||||
|
||||
const setSelectedMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const groupMessages = messages.filter((m) => m.askId === message.askId)
|
||||
if (groupMessages.length > 1) {
|
||||
groupMessages.forEach((m) => {
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: m.topicId,
|
||||
messageId: m.id,
|
||||
updates: { foldSelected: m.id === message.id }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
[dispatch, messages]
|
||||
)
|
||||
|
||||
const scrollToMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
|
||||
if (!messageElement) return
|
||||
|
||||
const display = messageElement ? window.getComputedStyle(messageElement).display : null
|
||||
if (display === 'none') {
|
||||
setSelectedMessage(message)
|
||||
return
|
||||
}
|
||||
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
},
|
||||
[setSelectedMessage]
|
||||
)
|
||||
|
||||
if (messages.length === 0) return null
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (messagesListRef.current) {
|
||||
const containerRect = e.currentTarget.getBoundingClientRect()
|
||||
const listRect = messagesListRef.current.getBoundingClientRect()
|
||||
setMouseY(e.clientY - listRect.top)
|
||||
|
||||
if (listRect.height > containerRect.height) {
|
||||
const mousePositionRatio = (e.clientY - containerRect.top) / containerRect.height
|
||||
const maxOffset = (containerRect.height - listRect.height) / 2 - 20
|
||||
setListOffsetY(-maxOffset + mousePositionRatio * (maxOffset * 2))
|
||||
} else {
|
||||
setListOffsetY(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseY(null)
|
||||
setListOffsetY(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageLineContainer
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
$right={right}
|
||||
$height={containerHeight}>
|
||||
<MessagesList ref={messagesListRef} style={{ transform: `translateY(${listOffsetY}px)` }}>
|
||||
{messages.map((message, index) => {
|
||||
const opacity = 0.5 + calculateValueByDistance(message.id, 1)
|
||||
const scale = 1 + calculateValueByDistance(message.id, 1)
|
||||
const size = 10 + calculateValueByDistance(message.id, 20)
|
||||
const avatarSource = getAvatarSource(isLocalAi, getMessageModelId(message))
|
||||
const username = removeLeadingEmoji(getUserName(message))
|
||||
|
||||
return (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
ref={(el) => {
|
||||
if (el) messageItemsRef.current.set(message.id, el)
|
||||
else messageItemsRef.current.delete(message.id)
|
||||
}}
|
||||
style={{
|
||||
opacity: mouseY ? opacity : Math.max(0, 0.6 - (0.3 * Math.abs(index - messages.length / 2)) / 5)
|
||||
}}
|
||||
onClick={() => scrollToMessage(message)}>
|
||||
<MessageItemContainer style={{ transform: ` scale(${scale})` }}>
|
||||
<MessageItemTitle>{username}</MessageItemTitle>
|
||||
<MessageItemContent>{message.content.substring(0, 50)}</MessageItemContent>
|
||||
</MessageItemContainer>
|
||||
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar
|
||||
src={avatarSource}
|
||||
size={size}
|
||||
style={{
|
||||
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
||||
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
||||
}}>
|
||||
A
|
||||
</Avatar>
|
||||
) : (
|
||||
<>
|
||||
{isEmoji(avatar) ? (
|
||||
<EmojiAvatar size={size}>{avatar}</EmojiAvatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={size} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MessageItem>
|
||||
)
|
||||
})}
|
||||
</MessagesList>
|
||||
</MessageLineContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageItemContainer = styled.div`
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
text-align: right;
|
||||
gap: 4px;
|
||||
text-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
opacity: 0;
|
||||
transform-origin: right center;
|
||||
`
|
||||
|
||||
const MessageLineContainer = styled.div<{ $right: string; $height: number | null }>`
|
||||
width: 14px;
|
||||
position: fixed;
|
||||
top: ${(props) => (props.$height ? `calc(${props.$height / 2}px + var(--status-bar-height))` : '50%')};
|
||||
right: ${(props) => props.$right};
|
||||
max-height: ${(props) => (props.$height ? `${props.$height}px` : 'calc(100% - var(--status-bar-height) * 2)')};
|
||||
transform: translateY(-50%);
|
||||
z-index: 0;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 5px;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
width: 440px;
|
||||
overflow-x: visible;
|
||||
overflow-y: hidden;
|
||||
${MessageItemContainer} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MessagesList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
will-change: transform;
|
||||
`
|
||||
|
||||
const MessageItem = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transform-origin: right center;
|
||||
padding: 2px 0;
|
||||
will-change: opacity;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.1s linear;
|
||||
`
|
||||
|
||||
const MessageItemTitle = styled.div`
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
`
|
||||
const MessageItemContent = styled.div`
|
||||
color: var(--color-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
`
|
||||
|
||||
const EmojiAvatar = styled.div<{ size: number }>`
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 20%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${(props) => props.size * 0.6}px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
export default MessageAnchorLine
|
||||
@@ -16,7 +16,6 @@ import styled from 'styled-components'
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
import MessageAttachments from './MessageAttachments'
|
||||
import MessageError from './MessageError'
|
||||
import MessageImage from './MessageImage'
|
||||
import MessageSearchResults from './MessageSearchResults'
|
||||
import MessageThought from './MessageThought'
|
||||
import MessageTools from './MessageTools'
|
||||
@@ -151,7 +150,6 @@ const MessageContent: React.FC<Props> = ({ message: _message, model }) => {
|
||||
<MessageThought message={message} />
|
||||
<MessageTools message={message} />
|
||||
<Markdown message={{ ...message, content: processedContent }} citationsData={citationsData} />
|
||||
{message.metadata?.generateImage && <MessageImage message={message} />}
|
||||
{message.translatedContent && (
|
||||
<Fragment>
|
||||
<Divider style={{ margin: 0, marginBottom: 10 }}>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import type { Message, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
@@ -19,12 +17,10 @@ interface Props {
|
||||
}
|
||||
|
||||
const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
const { editMessage } = useMessageOperations(topic)
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
||||
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
||||
)
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
|
||||
|
||||
const messageLength = messages.length
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
@@ -37,90 +33,6 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
}, [messageLength])
|
||||
|
||||
// 添加对流程图节点点击事件的监听
|
||||
useEffect(() => {
|
||||
// 只在组件挂载和消息数组变化时添加监听器
|
||||
if (!isGrouped || messageLength <= 1) return
|
||||
|
||||
const handleFlowNavigate = (event: CustomEvent) => {
|
||||
const { messageId } = event.detail
|
||||
|
||||
// 查找对应的消息在当前消息组中的索引
|
||||
const targetIndex = messages.findIndex((msg) => msg.id === messageId)
|
||||
|
||||
// 如果找到消息且不是当前选中的索引,则切换标签
|
||||
if (targetIndex !== -1 && targetIndex !== selectedIndex) {
|
||||
setSelectedIndex(targetIndex)
|
||||
|
||||
// 使用setSelectedMessage函数来切换标签,这是处理foldSelected的关键
|
||||
const targetMessage = messages[targetIndex]
|
||||
if (targetMessage) {
|
||||
setSelectedMessage(targetMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加事件监听器
|
||||
document.addEventListener('flow-navigate-to-message', handleFlowNavigate as EventListener)
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
document.removeEventListener('flow-navigate-to-message', handleFlowNavigate as EventListener)
|
||||
}
|
||||
}, [messages, selectedIndex, isGrouped, messageLength])
|
||||
|
||||
const setSelectedMessage = useCallback(
|
||||
(message: Message) => {
|
||||
messages.forEach(async (m) => {
|
||||
await editMessage(m.id, { foldSelected: m.id === message.id })
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
[editMessage, messages]
|
||||
)
|
||||
|
||||
// 添加对LOCATE_MESSAGE事件的监听
|
||||
useEffect(() => {
|
||||
// 为每个消息注册一个定位事件监听器
|
||||
const eventHandlers: { [key: string]: () => void } = {}
|
||||
|
||||
messages.forEach((message) => {
|
||||
const eventName = EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id
|
||||
const handler = () => {
|
||||
// 检查消息是否处于可见状态
|
||||
const element = document.getElementById(`message-${message.id}`)
|
||||
if (element) {
|
||||
const display = window.getComputedStyle(element).display
|
||||
|
||||
if (display === 'none') {
|
||||
// 如果消息隐藏,先切换标签
|
||||
setSelectedMessage(message)
|
||||
} else {
|
||||
// 直接滚动
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventHandlers[eventName] = handler
|
||||
EventEmitter.on(eventName, handler)
|
||||
})
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 移除所有事件监听器
|
||||
Object.entries(eventHandlers).forEach(([eventName, handler]) => {
|
||||
EventEmitter.off(eventName, handler)
|
||||
})
|
||||
}
|
||||
}, [messages, setSelectedMessage])
|
||||
|
||||
const renderMessage = useCallback(
|
||||
(message: Message & { index: number }, index: number) => {
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
@@ -137,16 +49,11 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
|
||||
const messageWrapper = (
|
||||
<MessageWrapper
|
||||
id={`message-${message.id}`}
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={classNames({
|
||||
'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped,
|
||||
[multiModelMessageStyle]: true,
|
||||
selected: 'foldSelected' in message ? message.foldSelected : index === 0
|
||||
})}>
|
||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||
<MessageStream {...messageProps} />
|
||||
</MessageWrapper>
|
||||
)
|
||||
@@ -188,7 +95,6 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
|
||||
return (
|
||||
<GroupContainer
|
||||
id={`message-group-${messages[0].askId}`}
|
||||
$isGrouped={isGrouped}
|
||||
$layout={multiModelMessageStyle}
|
||||
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||
@@ -202,14 +108,10 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
{isGrouped && (
|
||||
<MessageGroupMenuBar
|
||||
multiModelMessageStyle={multiModelMessageStyle}
|
||||
setMultiModelMessageStyle={(style) => {
|
||||
setMultiModelMessageStyle(style)
|
||||
messages.forEach((message) => {
|
||||
editMessage(message.id, { multiModelMessageStyle: style })
|
||||
})
|
||||
}}
|
||||
setMultiModelMessageStyle={setMultiModelMessageStyle}
|
||||
messages={messages}
|
||||
setSelectedMessage={setSelectedMessage}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
topic={topic}
|
||||
/>
|
||||
)}
|
||||
@@ -271,18 +173,15 @@ interface MessageWrapperProps {
|
||||
|
||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
width: 100%;
|
||||
&.horizontal {
|
||||
display: inline-block;
|
||||
}
|
||||
&.grid {
|
||||
display: inline-block;
|
||||
}
|
||||
&.fold {
|
||||
display: none;
|
||||
&.selected {
|
||||
display: inline-block;
|
||||
display: ${(props) => {
|
||||
if (props.$layout === 'fold') {
|
||||
return props.$selected ? 'block' : 'none'
|
||||
}
|
||||
}
|
||||
if (props.$layout === 'horizontal') {
|
||||
return 'inline-block'
|
||||
}
|
||||
return 'block'
|
||||
}};
|
||||
|
||||
${({ $layout, $isGrouped }) => {
|
||||
if ($layout === 'horizontal' && $isGrouped) {
|
||||
|
||||
@@ -21,7 +21,8 @@ interface Props {
|
||||
multiModelMessageStyle: MultiModelMessageStyle
|
||||
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
||||
messages: Message[]
|
||||
setSelectedMessage: (message: Message) => void
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
@@ -29,7 +30,8 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
multiModelMessageStyle,
|
||||
setMultiModelMessageStyle,
|
||||
messages,
|
||||
setSelectedMessage,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
topic
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -75,7 +77,11 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
))}
|
||||
</LayoutContainer>
|
||||
{multiModelMessageStyle === 'fold' && (
|
||||
<MessageGroupModelList messages={messages} setSelectedMessage={setSelectedMessage} />
|
||||
<MessageGroupModelList
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
/>
|
||||
)}
|
||||
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||
</HStack>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
@@ -9,12 +10,13 @@ import styled from 'styled-components'
|
||||
|
||||
interface MessageGroupModelListProps {
|
||||
messages: Message[]
|
||||
setSelectedMessage: (message: Message) => void
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
}
|
||||
|
||||
type DisplayMode = 'compact' | 'expanded'
|
||||
|
||||
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSelectedMessage }) => {
|
||||
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectedIndex, setSelectedIndex }) => {
|
||||
const { t } = useTranslation()
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>('expanded')
|
||||
const isCompact = displayMode === 'compact'
|
||||
@@ -41,9 +43,10 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSe
|
||||
<Tooltip key={index} title={message.model?.name} placement="top" mouseEnterDelay={0.2}>
|
||||
<AvatarWrapper
|
||||
className="avatar-wrapper"
|
||||
isSelected={'foldSelected' in message ? message.foldSelected! : index === 0}
|
||||
isSelected={selectedIndex === index}
|
||||
onClick={() => {
|
||||
setSelectedMessage(message)
|
||||
setSelectedIndex(index)
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[index].id, false)
|
||||
}}>
|
||||
<ModelAvatar model={message.model as Model} size={28} />
|
||||
</AvatarWrapper>
|
||||
@@ -53,19 +56,19 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSe
|
||||
) : (
|
||||
/* Expanded style display */
|
||||
<Segmented
|
||||
value={messages.find((message) => message.foldSelected)?.id || messages[0].id}
|
||||
value={selectedIndex.toString()}
|
||||
onChange={(value) => {
|
||||
const message = messages.find((message) => message.id === value) as Message
|
||||
setSelectedMessage(message)
|
||||
setSelectedIndex(Number(value))
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
||||
}}
|
||||
options={messages.map((message) => ({
|
||||
options={messages.map((message, index) => ({
|
||||
label: (
|
||||
<SegmentedLabel>
|
||||
<ModelAvatar model={message.model as Model} size={20} />
|
||||
<ModelName>{message.model?.name}</ModelName>
|
||||
</SegmentedLabel>
|
||||
),
|
||||
value: message.id
|
||||
value: index.toString()
|
||||
}))}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Message } from '@renderer/types'
|
||||
import { Image as AntdImage } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageImage: FC<Props> = ({ message }) => {
|
||||
return (
|
||||
<Container style={{ marginBottom: 8 }}>
|
||||
{message.metadata?.generateImage!.images.map((image, index) => (
|
||||
<Image src={image} key={`image-${index}`} width="33%" />
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
const Image = styled(AntdImage)`
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
export default MessageImage
|
||||
@@ -15,7 +15,6 @@ import { UploadOutlined } from '@ant-design/icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { isReasoningModel } from '@renderer/config/models'
|
||||
import { TranslateLanguageOptions } from '@renderer/config/translate'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@@ -31,14 +30,11 @@ import {
|
||||
exportMessageAsMarkdown,
|
||||
messageToMarkdown
|
||||
} from '@renderer/utils/export'
|
||||
import { withMessageThought } from '@renderer/utils/formats'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { clone } from 'lodash'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
assistant: Assistant
|
||||
@@ -58,8 +54,6 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
|
||||
const assistantModel = assistant?.model
|
||||
const {
|
||||
loading,
|
||||
@@ -76,21 +70,12 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onCopy = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
// 只处理助手消息和来自推理模型的消息
|
||||
if (message.role === 'assistant' && message.model && isReasoningModel(message.model)) {
|
||||
const processedMessage = withMessageThought(clone(message))
|
||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(processedMessage.content.trimStart()))
|
||||
} else {
|
||||
// 其他情况直接复制原始内容
|
||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content.trimStart()))
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content.trimStart()))
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
},
|
||||
[message, t]
|
||||
[message.content, t]
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
@@ -111,15 +96,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onEdit = useCallback(async () => {
|
||||
let resendMessage = false
|
||||
|
||||
let textToEdit = message.content
|
||||
|
||||
if (message.role === 'assistant' && message.model && isReasoningModel(message.model)) {
|
||||
const processedMessage = withMessageThought(clone(message))
|
||||
textToEdit = processedMessage.content
|
||||
}
|
||||
|
||||
const editedText = await TextEditPopup.show({
|
||||
text: textToEdit,
|
||||
text: message.content,
|
||||
children: (props) => {
|
||||
const onPress = () => {
|
||||
props.onOk?.()
|
||||
@@ -135,7 +113,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
}
|
||||
})
|
||||
|
||||
if (editedText && editedText !== textToEdit) {
|
||||
if (editedText && editedText !== message.content) {
|
||||
await editMessage(message.id, { content: editedText })
|
||||
resendMessage && handleResendUserMessage({ ...message, content: editedText })
|
||||
}
|
||||
@@ -242,8 +220,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
key: 'obsidian',
|
||||
onClick: async () => {
|
||||
const markdown = messageToMarkdown(message)
|
||||
const title = topic.name?.replace(/\//g, '_') || 'Untitled'
|
||||
await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' })
|
||||
const title = getMessageTitle(message)
|
||||
await ObsidianExportPopup.show({ title, markdown })
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -258,7 +236,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
]
|
||||
}
|
||||
],
|
||||
[message, messageContainerRef, onEdit, onNewBranch, t, topic.name]
|
||||
[message, messageContainerRef, onEdit, onNewBranch, t]
|
||||
)
|
||||
|
||||
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||
@@ -312,14 +290,10 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
destroyTooltipOnHide
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={onRegenerate}
|
||||
onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
|
||||
<Tooltip
|
||||
title={t('common.regenerate')}
|
||||
mouseEnterDelay={0.8}
|
||||
open={showRegenerateTooltip}
|
||||
onOpenChange={setShowRegenerateTooltip}>
|
||||
onConfirm={onRegenerate}>
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button">
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
@@ -371,14 +345,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
onConfirm={() => deleteMessage(message)}>
|
||||
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
|
||||
<Tooltip
|
||||
title={t('common.delete')}
|
||||
mouseEnterDelay={1}
|
||||
open={showDeleteTooltip}
|
||||
onOpenChange={setShowDeleteTooltip}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<DeleteOutlined />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
|
||||
@@ -26,7 +26,6 @@ import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ChatNavigation from './ChatNavigation'
|
||||
import MessageAnchorLine from './MessageAnchorLine'
|
||||
import MessageGroup from './MessageGroup'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import NewTopicButton from './NewTopicButton'
|
||||
@@ -40,7 +39,7 @@ interface MessagesProps {
|
||||
|
||||
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||
const { showTopics, topicPosition, showAssistants } = useSettings()
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const dispatch = useAppDispatch()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -139,13 +138,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
|
||||
const newTopic = getDefaultTopic(assistant.id)
|
||||
newTopic.name = topic.name
|
||||
const currentMessages = messagesRef.current
|
||||
|
||||
// 复制消息并且更新 topicId
|
||||
const branchMessages = take(currentMessages, currentMessages.length - index).map((msg) => ({
|
||||
...msg,
|
||||
topicId: newTopic.id
|
||||
}))
|
||||
const branchMessages = take(messages, messages.length - index)
|
||||
|
||||
// 将分支的消息放入数据库
|
||||
await db.topics.add({ id: newTopic.id, messages: branchMessages })
|
||||
@@ -232,10 +225,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} topic={topic} />
|
||||
</NarrowLayout>
|
||||
|
||||
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
||||
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
<ChatNavigation containerId="messages" />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
setFontSize,
|
||||
setMathEngine,
|
||||
setMessageFont,
|
||||
setMessageNavigation,
|
||||
setMessageStyle,
|
||||
setMultiModelMessageStyle,
|
||||
setPasteLongTextAsFile,
|
||||
@@ -77,8 +76,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
autoTranslateWithSpace,
|
||||
pasteLongTextThreshold,
|
||||
multiModelMessageStyle,
|
||||
thoughtAutoCollapse,
|
||||
messageNavigation
|
||||
thoughtAutoCollapse
|
||||
} = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||
@@ -363,19 +361,6 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</StyledSelect>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
|
||||
<StyledSelect
|
||||
size="small"
|
||||
value={messageNavigation}
|
||||
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
|
||||
style={{ width: 135 }}>
|
||||
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
|
||||
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
|
||||
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
|
||||
</StyledSelect>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||
<StyledSelect
|
||||
|
||||
@@ -262,7 +262,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
key: 'obsidian',
|
||||
onClick: async () => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' })
|
||||
await ObsidianExportPopup.show({ title: topic.name, markdown })
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -61,7 +61,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
} = useKnowledge(selectedBase.id || '')
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const rerankModelProviderName = getProviderName(base?.rerankModel?.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
@@ -446,34 +445,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
</ContentSection>
|
||||
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
|
||||
<ModelInfo>
|
||||
<div className="model-header">
|
||||
<label>{t('knowledge.model_info')}</label>
|
||||
<Button icon={<SettingOutlined />} onClick={() => KnowledgeSettingsPopup.show({ base })} size="small" />
|
||||
</div>
|
||||
|
||||
<div className="model-row">
|
||||
<div className="label-column">
|
||||
<label>{t('models.embedding_model')}</label>
|
||||
</div>
|
||||
<div className="tag-column">
|
||||
{providerName && <Tag color="purple">{providerName}</Tag>}
|
||||
<Tag color="blue">{base.model.name}</Tag>
|
||||
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{base.rerankModel && (
|
||||
<div className="model-row">
|
||||
<div className="label-column">
|
||||
<label>{t('models.rerank_model')}</label>
|
||||
</div>
|
||||
<div className="tag-column">
|
||||
{rerankModelProviderName && <Tag color="purple">{rerankModelProviderName}</Tag>}
|
||||
<Tag color="blue">{base.rerankModel?.name}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<label htmlFor="model-info">{t('knowledge.model_info')}</label>
|
||||
<Tag color="blue">{base.model.name}</Tag>
|
||||
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
|
||||
{providerName && <Tag color="purple">{providerName}</Tag>}
|
||||
<Button icon={<SettingOutlined />} onClick={() => KnowledgeSettingsPopup.show({ base })} size="small" />
|
||||
</ModelInfo>
|
||||
|
||||
<IndexSection>
|
||||
@@ -569,39 +547,15 @@ const IndexSection = styled.div`
|
||||
|
||||
const ModelInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
.model-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.model-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.label-column {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-column {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 8px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`
|
||||
|
||||
const FlexColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { SUPPORTED_REANK_PROVIDERS } from '@renderer/config/providers'
|
||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { SettingHelpText } from '@renderer/pages/settings'
|
||||
import AiProvider from '@renderer/providers/AiProvider'
|
||||
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@@ -36,17 +34,14 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
const { addKnowledgeBase } = useKnowledgeBases()
|
||||
|
||||
const allModels = providers
|
||||
.map((p) => p.models)
|
||||
.flat()
|
||||
.filter((model) => isEmbeddingModel(model) && !isRerankModel(model))
|
||||
|
||||
.filter((model) => isEmbeddingModel(model))
|
||||
const rerankModels = providers
|
||||
.map((p) => p.models)
|
||||
.flat()
|
||||
.filter((model) => isRerankModel(model))
|
||||
|
||||
const nameInputRef = useRef<any>(null)
|
||||
|
||||
const selectOptions = providers
|
||||
@@ -55,7 +50,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
options: sortBy(p.models, 'name')
|
||||
.filter((model) => isEmbeddingModel(model) && !isRerankModel(model))
|
||||
.filter((model) => isEmbeddingModel(model))
|
||||
.map((m) => ({
|
||||
label: m.name,
|
||||
value: getModelUniqId(m)
|
||||
@@ -65,7 +60,6 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
|
||||
const rerankSelectOptions = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.filter((p) => SUPPORTED_REANK_PROVIDERS.includes(p.id))
|
||||
.map((p) => ({
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
@@ -82,7 +76,6 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const selectedModel = find(allModels, JSON.parse(values.model)) as Model
|
||||
|
||||
const selectedRerankModel = values.rerankModel
|
||||
? (find(rerankModels, JSON.parse(values.rerankModel)) as Model)
|
||||
: undefined
|
||||
@@ -172,11 +165,6 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
rules={[{ required: false, message: t('message.error.enter.model') }]}>
|
||||
<Select style={{ width: '100%' }} options={rerankSelectOptions} placeholder={t('settings.models.empty')} />
|
||||
</Form.Item>
|
||||
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
|
||||
{t('models.rerank_model_support_provider', {
|
||||
provider: SUPPORTED_REANK_PROVIDERS.map((id) => t(`provider.${id}`))
|
||||
})}
|
||||
</SettingHelpText>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -3,10 +3,8 @@ import { TopView } from '@renderer/components/TopView'
|
||||
import { DEFAULT_KNOWLEDGE_DOCUMENT_COUNT } from '@renderer/config/constant'
|
||||
import { getEmbeddingMaxContext } from '@renderer/config/embedings'
|
||||
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
|
||||
import { SUPPORTED_REANK_PROVIDERS } from '@renderer/config/providers'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { SettingHelpText } from '@renderer/pages/settings'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { Alert, Form, Input, InputNumber, Modal, Select, Slider } from 'antd'
|
||||
@@ -57,7 +55,7 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
options: sortBy(p.models, 'name')
|
||||
.filter((model) => isEmbeddingModel(model) && !isRerankModel(model))
|
||||
.filter((model) => isEmbeddingModel(model))
|
||||
.map((m) => ({
|
||||
label: m.name,
|
||||
value: getModelUniqId(m)
|
||||
@@ -67,7 +65,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
|
||||
const rerankSelectOptions = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.filter((p) => SUPPORTED_REANK_PROVIDERS.includes(p.id))
|
||||
.map((p) => ({
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
@@ -97,7 +94,7 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
}
|
||||
updateKnowledgeBase(newBase)
|
||||
setOpen(false)
|
||||
setTimeout(() => resolve(newBase), 350)
|
||||
resolve(newBase)
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error)
|
||||
}
|
||||
@@ -154,11 +151,6 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
|
||||
{t('models.rerank_model_support_provider', {
|
||||
provider: SUPPORTED_REANK_PROVIDERS.map((id) => t(`provider.${id}`))
|
||||
})}
|
||||
</SettingHelpText>
|
||||
|
||||
<Form.Item
|
||||
name="documentCount"
|
||||
|
||||
301
src/renderer/src/pages/nodeapps/NodeApp.tsx
Normal file
301
src/renderer/src/pages/nodeapps/NodeApp.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
import { DownloadOutlined, GithubOutlined, LoadingOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons'
|
||||
import { useNodeApps } from '@renderer/hooks/useNodeApps'
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Avatar, Button, Card, Dropdown, Menu, Space, Tag, Tooltip, Typography, notification } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
|
||||
interface Props {
|
||||
app: NodeAppType
|
||||
}
|
||||
|
||||
const NodeApp: FC<Props> = ({ app }) => {
|
||||
const { t } = useTranslation()
|
||||
const { installApp, updateApp, startApp, stopApp, uninstallApp } = useNodeApps()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionType, setActionType] = useState<string>('')
|
||||
|
||||
// Handle installation
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('install')
|
||||
await installApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.installSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle update
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('update')
|
||||
await updateApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.updateSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start
|
||||
const handleStart = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('start')
|
||||
const result = await startApp(app.id as string)
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.startSuccess', { name: app.name, port: result.port })
|
||||
})
|
||||
window.api.openWebsite(result.url)
|
||||
}
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('stop')
|
||||
await stopApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.stopSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uninstall
|
||||
const handleUninstall = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setActionType('uninstall')
|
||||
await uninstallApp(app.id as string)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.uninstallSuccess', { name: app.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setActionType('')
|
||||
}
|
||||
}
|
||||
|
||||
// Open GitHub repository
|
||||
const openRepository = () => {
|
||||
if (app.repositoryUrl) {
|
||||
window.api.openWebsite(app.repositoryUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Open app homepage
|
||||
const openHomepage = () => {
|
||||
if (app.homepage) {
|
||||
window.api.openWebsite(app.homepage)
|
||||
}
|
||||
}
|
||||
|
||||
// Render app status tag
|
||||
const renderStatusTag = () => {
|
||||
if (app.isRunning) {
|
||||
return <Tag color="green">{t('nodeapp.running')}</Tag>
|
||||
}
|
||||
if (app.isInstalled) {
|
||||
return <Tag color="blue">{t('nodeapp.installed')}</Tag>
|
||||
}
|
||||
return <Tag color="default">{t('nodeapp.notInstalled')}</Tag>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
cover={
|
||||
<CardCoverContainer>
|
||||
{app.logo ? (
|
||||
<img alt={app.name} src={app.logo} style={{ width: '100%', height: '140px', objectFit: 'cover' }} />
|
||||
) : (
|
||||
<AppLogo>
|
||||
<Avatar size={64} style={{ backgroundColor: '#1890ff' }}>
|
||||
{app.name.substring(0, 2).toUpperCase()}
|
||||
</Avatar>
|
||||
</AppLogo>
|
||||
)}
|
||||
{renderStatusTag()}
|
||||
</CardCoverContainer>
|
||||
}
|
||||
actions={[
|
||||
// Show different actions based on app status
|
||||
app.isInstalled ? (
|
||||
app.isRunning ? (
|
||||
<Tooltip title={t('nodeapp.stop')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'stop' ? <LoadingOutlined /> : <StopOutlined />}
|
||||
onClick={handleStop}
|
||||
loading={loading && actionType === 'stop'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t('nodeapp.start')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'start' ? <LoadingOutlined /> : <PlayCircleOutlined />}
|
||||
onClick={handleStart}
|
||||
loading={loading && actionType === 'start'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
) : (
|
||||
<Tooltip title={t('nodeapp.install')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'install' ? <LoadingOutlined /> : <DownloadOutlined />}
|
||||
onClick={handleInstall}
|
||||
loading={loading && actionType === 'install'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
app.isInstalled && (
|
||||
<Tooltip title={t('nodeapp.update')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={loading && actionType === 'update' ? <LoadingOutlined /> : <ReloadOutlined />}
|
||||
onClick={handleUpdate}
|
||||
loading={loading && actionType === 'update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
app.repositoryUrl && (
|
||||
<Tooltip title={t('nodeapp.viewRepository')}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<GithubOutlined />}
|
||||
onClick={openRepository}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<Card.Meta
|
||||
title={<Title level={5}>{app.name}</Title>}
|
||||
description={
|
||||
<div style={{ minHeight: '100px' }}>
|
||||
<Paragraph ellipsis={{ rows: 3 }}>{app.description}</Paragraph>
|
||||
|
||||
{app.author && (
|
||||
<Space style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">{t('nodeapp.author')}:</Text>
|
||||
<Text>{app.author}</Text>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{app.version && (
|
||||
<Space style={{ marginTop: '4px' }}>
|
||||
<Text type="secondary">{t('nodeapp.version')}:</Text>
|
||||
<Text>{app.version}</Text>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{app.isInstalled && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'uninstall',
|
||||
danger: true,
|
||||
label: t('nodeapp.uninstall'),
|
||||
onClick: handleUninstall
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
{t('nodeapp.more')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const CardCoverContainer = styled.div`
|
||||
position: relative;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.ant-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const AppLogo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
`
|
||||
|
||||
export default NodeApp
|
||||
118
src/renderer/src/pages/nodeapps/NodeAppForm.tsx
Normal file
118
src/renderer/src/pages/nodeapps/NodeAppForm.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Button, Form, Input, Space } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
onSubmit: (values: NodeAppType) => void
|
||||
onCancel: () => void
|
||||
loading: boolean
|
||||
initialValues?: Partial<NodeAppType>
|
||||
}
|
||||
|
||||
const NodeAppForm: FC<Props> = ({ onSubmit, onCancel, loading, initialValues }) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
onSubmit({
|
||||
...values,
|
||||
type: 'node',
|
||||
isInstalled: false,
|
||||
isRunning: false
|
||||
} as NodeAppType)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={initialValues}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('nodeapp.form.name')}
|
||||
rules={[{ required: true, message: t('nodeapp.form.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="repositoryUrl"
|
||||
label={t('nodeapp.form.repositoryUrl')}
|
||||
rules={[
|
||||
{ required: true, message: t('nodeapp.form.repositoryUrlRequired') },
|
||||
{
|
||||
pattern: /^https?:\/\/github\.com\/[\w-]+\/[\w.-]+\/?$/,
|
||||
message: t('nodeapp.form.repositoryUrlInvalid')
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.repositoryUrlPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label={t('nodeapp.form.description')}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={t('nodeapp.form.descriptionPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="author"
|
||||
label={t('nodeapp.form.author')}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.authorPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="homepage"
|
||||
label={t('nodeapp.form.homepage')}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.form.homepagePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="startCommand"
|
||||
label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('nodeapp.form.port')}
|
||||
help={t('nodeapp.form.portHelp')}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ float: 'right' }}>
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeAppForm
|
||||
171
src/renderer/src/pages/nodeapps/NodeAppsPage.tsx
Normal file
171
src/renderer/src/pages/nodeapps/NodeAppsPage.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { useNodeApps } from '@renderer/hooks/useNodeApps'
|
||||
import { NodeAppType } from '@renderer/types'
|
||||
import { Button, Col, Empty, Input, Modal, Row, Spin, Tabs, Typography, notification } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import NodeApp from './NodeApp'
|
||||
import NodeAppForm from './NodeAppForm'
|
||||
import PackageDeployer from './PackageDeployer'
|
||||
|
||||
const { Title } = Typography
|
||||
const { TabPane } = Tabs
|
||||
|
||||
const NodeAppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const { apps, loading, addApp, refresh } = useNodeApps()
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
const [formLoading, setFormLoading] = useState(false)
|
||||
|
||||
// Filter apps based on search
|
||||
const filteredApps = search
|
||||
? apps.filter(
|
||||
(app) =>
|
||||
app.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.description?.toLowerCase().includes(search.toLowerCase()) ||
|
||||
app.author?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: apps
|
||||
|
||||
// Handle adding a new app
|
||||
const handleAddApp = useCallback(async (values: NodeAppType) => {
|
||||
try {
|
||||
setFormLoading(true)
|
||||
await addApp({
|
||||
...values,
|
||||
type: 'node'
|
||||
})
|
||||
setIsModalVisible(false)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.addSuccess', { name: values.name })
|
||||
})
|
||||
} catch (err) {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
} finally {
|
||||
setFormLoading(false)
|
||||
}
|
||||
}, [addApp, t])
|
||||
|
||||
// Disable right-click menu in blank area
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Handle successful package deployment
|
||||
const handleDeployed = useCallback(() => {
|
||||
refresh()
|
||||
}, [refresh])
|
||||
|
||||
return (
|
||||
<Container onContextMenu={handleContextMenu}>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('nodeapp.title')}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
className="nodrag"
|
||||
style={{ width: '250px', height: 28 }}
|
||||
size="small"
|
||||
variant="filled"
|
||||
suffix={<SearchOutlined />}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
{t('nodeapp.add')}
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ width: 80 }} />
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
|
||||
<ContentContainer id="content-container">
|
||||
<Tabs defaultActiveKey="apps" style={{ height: '100%' }}>
|
||||
<TabPane tab={t('nodeapp.marketplaceTab')} key="apps">
|
||||
{loading ? (
|
||||
<Center>
|
||||
<Spin size="large" />
|
||||
</Center>
|
||||
) : isEmpty(filteredApps) ? (
|
||||
<Center>
|
||||
<Empty
|
||||
description={
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<p>{t('nodeapp.empty')}</p>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsModalVisible(true)}
|
||||
>
|
||||
{t('nodeapp.add')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Center>
|
||||
) : (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Title level={5} style={{ marginBottom: '16px' }}>{t('nodeapp.featured')}</Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
{filteredApps.map((app) => (
|
||||
<Col key={app.id} xs={24} sm={12} md={8} lg={6}>
|
||||
<NodeApp app={app} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane tab={t('nodeapp.packageDeployerTab')} key="packageDeployer">
|
||||
<PackageDeployer onDeployed={handleDeployed} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</ContentContainer>
|
||||
|
||||
<Modal
|
||||
title={t('nodeapp.addNew')}
|
||||
open={isModalVisible}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<NodeAppForm
|
||||
onSubmit={handleAddApp}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
loading={formLoading}
|
||||
/>
|
||||
</Modal>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 20px;
|
||||
`
|
||||
|
||||
export default NodeAppsPage
|
||||
198
src/renderer/src/pages/nodeapps/NodeSettings.tsx
Normal file
198
src/renderer/src/pages/nodeapps/NodeSettings.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { Button, Card, Form, Input, Select, Typography, notification, Space } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
const NodeSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checkingVersion, setCheckingVersion] = useState(false)
|
||||
const [nodeInstalled, setNodeInstalled] = useState(false)
|
||||
const [currentVersion, setCurrentVersion] = useState('')
|
||||
|
||||
// 默认提供的Node.js版本选项
|
||||
const nodeVersions = [
|
||||
{ value: '20.11.1', label: 'v20.11.1 (LTS)' },
|
||||
{ value: '18.18.0', label: 'v18.18.0 (LTS)' },
|
||||
{ value: '16.20.2', label: 'v16.20.2 (LTS)' },
|
||||
{ value: '14.21.3', label: 'v14.21.3 (LTS)' }
|
||||
]
|
||||
|
||||
// 检查Node.js是否已安装
|
||||
const checkNodeStatus = async () => {
|
||||
try {
|
||||
setCheckingVersion(true)
|
||||
const isNodeInstalled = await window.api.nodeapp.checkNode()
|
||||
setNodeInstalled(isNodeInstalled)
|
||||
|
||||
if (isNodeInstalled) {
|
||||
try {
|
||||
// 获取当前安装的Node.js版本
|
||||
// 使用ipc调用获取Node.js版本
|
||||
const versionFromConfig = await window.api.config.get('NODE_VERSION')
|
||||
if (versionFromConfig) {
|
||||
setCurrentVersion(versionFromConfig)
|
||||
} else {
|
||||
setCurrentVersion('Unknown')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting Node.js version:', error)
|
||||
setCurrentVersion('Unknown')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking Node.js status:', error)
|
||||
} finally {
|
||||
setCheckingVersion(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件加载时检查状态
|
||||
useEffect(() => {
|
||||
checkNodeStatus()
|
||||
}, [])
|
||||
|
||||
// 安装Node.js
|
||||
const handleInstall = async (values: any) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 设置环境变量来指定要安装的Node.js版本
|
||||
if (values.nodeVersion) {
|
||||
await window.api.config.set('NODE_VERSION', values.nodeVersion)
|
||||
}
|
||||
|
||||
const success = await window.api.nodeapp.installNode()
|
||||
|
||||
if (success) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.nodeSettings.installSuccess', {
|
||||
version: values.nodeVersion
|
||||
})
|
||||
})
|
||||
|
||||
// 重新检查状态
|
||||
await checkNodeStatus()
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.nodeSettings.installFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error installing Node.js:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Card title={<Title level={4}>{t('nodeapp.nodeSettings.title')}</Title>}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>{t('nodeapp.nodeSettings.description')}</Text>
|
||||
|
||||
<StatusSection>
|
||||
<Text strong>{t('nodeapp.nodeSettings.status')}: </Text>
|
||||
{checkingVersion ? (
|
||||
<Text type="secondary">
|
||||
<LoadingOutlined style={{ marginRight: 8 }} />
|
||||
{t('nodeapp.nodeSettings.checking')}
|
||||
</Text>
|
||||
) : nodeInstalled ? (
|
||||
<Text type="success">
|
||||
{t('nodeapp.nodeSettings.installed')}
|
||||
{currentVersion && ` (${currentVersion})`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="warning">{t('nodeapp.nodeSettings.notInstalled')}</Text>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={checkNodeStatus}
|
||||
loading={checkingVersion}
|
||||
>
|
||||
{t('nodeapp.nodeSettings.refresh')}
|
||||
</Button>
|
||||
</StatusSection>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleInstall}
|
||||
initialValues={{
|
||||
nodeVersion: '18.18.0'
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="nodeVersion"
|
||||
label={t('nodeapp.nodeSettings.version')}
|
||||
help={t('nodeapp.nodeSettings.versionHelp')}
|
||||
>
|
||||
<Select>
|
||||
{nodeVersions.map((version) => (
|
||||
<Option key={version.value} value={version.value}>
|
||||
{version.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="customVersion"
|
||||
label={t('nodeapp.nodeSettings.customVersion')}
|
||||
help={t('nodeapp.nodeSettings.customVersionHelp')}
|
||||
>
|
||||
<Input
|
||||
placeholder="20.12.1"
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
form.setFieldsValue({ nodeVersion: e.target.value })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
>
|
||||
{nodeInstalled ? t('nodeapp.nodeSettings.reinstall') : t('nodeapp.nodeSettings.install')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 16px;
|
||||
`
|
||||
|
||||
const StatusSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export default NodeSettings
|
||||
580
src/renderer/src/pages/nodeapps/PackageDeployer.tsx
Normal file
580
src/renderer/src/pages/nodeapps/PackageDeployer.tsx
Normal file
@@ -0,0 +1,580 @@
|
||||
import { CloudUploadOutlined, GithubOutlined, LoadingOutlined, SettingOutlined, InfoCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { FileType } from '@renderer/types'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Spin,
|
||||
Tabs,
|
||||
Typography,
|
||||
Upload,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
Checkbox
|
||||
} from 'antd'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Panel } = Collapse
|
||||
const { TabPane } = Tabs
|
||||
const { Option } = Select
|
||||
|
||||
interface Props {
|
||||
onDeployed?: (result: { port: number; url: string }) => void
|
||||
}
|
||||
|
||||
const PackageDeployer: FC<Props> = ({ onDeployed }) => {
|
||||
const { t } = useTranslation()
|
||||
const [form] = Form.useForm()
|
||||
const [gitForm] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [gitLoading, setGitLoading] = useState(false)
|
||||
const [file, setFile] = useState<FileType | null>(null)
|
||||
const [advancedVisible, setAdvancedVisible] = useState(false)
|
||||
const [gitAdvancedVisible, setGitAdvancedVisible] = useState(false)
|
||||
const [isNodeAvailable, setIsNodeAvailable] = useState<boolean | null>(null)
|
||||
const [isInstallingNode, setIsInstallingNode] = useState(false)
|
||||
const [uploadUrl, setUploadUrl] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('zip')
|
||||
|
||||
// Check if Node.js is available
|
||||
useEffect(() => {
|
||||
const checkNodeAvailability = async () => {
|
||||
try {
|
||||
const isAvailable = await window.api.nodeapp.checkNode()
|
||||
setIsNodeAvailable(isAvailable)
|
||||
} catch (error) {
|
||||
console.error('Error checking Node.js availability:', error)
|
||||
setIsNodeAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
checkNodeAvailability()
|
||||
}, [])
|
||||
|
||||
// Handle Node.js installation
|
||||
const handleInstallNode = async () => {
|
||||
try {
|
||||
setIsInstallingNode(true)
|
||||
const success = await window.api.nodeapp.installNode()
|
||||
|
||||
if (success) {
|
||||
setIsNodeAvailable(true)
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.nodeInstallSuccess')
|
||||
})
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.nodeInstallFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error installing Node.js:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setIsInstallingNode(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = async () => {
|
||||
try {
|
||||
const files = await window.api.file.select({
|
||||
filters: [
|
||||
{ name: 'ZIP Files', extensions: ['zip'] }
|
||||
],
|
||||
properties: ['openFile']
|
||||
})
|
||||
|
||||
if (files && files.length > 0) {
|
||||
setFile(files[0])
|
||||
form.setFieldsValue({
|
||||
name: files[0].name.replace(/\.zip$/, ''),
|
||||
file: files[0]
|
||||
})
|
||||
setUploadUrl(files[0].path)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error selecting file:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeploy = async (values: any) => {
|
||||
// First check if Node.js is available
|
||||
if (isNodeAvailable === false) {
|
||||
Modal.confirm({
|
||||
title: t('nodeapp.packageDeployer.nodeRequired'),
|
||||
content: t('nodeapp.packageDeployer.installNodePrompt'),
|
||||
okText: t('nodeapp.packageDeployer.installNode'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: handleInstallNode
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
notification.warning({
|
||||
message: t('common.warning'),
|
||||
description: t('nodeapp.packageDeployer.noFileSelected')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
// 检测是否为Next.js应用,如果文件名包含next或文件是从next.js项目导出的
|
||||
const isNextJs = file.name.toLowerCase().includes('next') ||
|
||||
(values.isNextJs === true);
|
||||
|
||||
// 如果是Next.js应用,自动设置构建步骤
|
||||
if (isNextJs && !values.buildCommand) {
|
||||
values.buildCommand = 'npm run build';
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.nextJsDetected'),
|
||||
description: t('nodeapp.packageDeployer.buildStepAdded'),
|
||||
duration: 5
|
||||
});
|
||||
}
|
||||
|
||||
// Display note about ES modules compatibility
|
||||
if (file.name.includes('react') || file.name.includes('next') || file.name.includes('vue')) {
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.moduleTypeError'),
|
||||
description: t('nodeapp.packageDeployer.esModuleError'),
|
||||
duration: 8
|
||||
})
|
||||
}
|
||||
|
||||
// Deploy the ZIP package
|
||||
const result = await window.api.nodeapp.deployZip(file.path, {
|
||||
name: values.name,
|
||||
port: values.port ? parseInt(values.port) : undefined,
|
||||
startCommand: values.startCommand,
|
||||
installCommand: values.installCommand,
|
||||
buildCommand: values.buildCommand
|
||||
})
|
||||
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.deploySuccess', {
|
||||
name: values.name,
|
||||
port: result.port
|
||||
}),
|
||||
btn: (
|
||||
<Button type="primary" size="small" onClick={() => window.api.openWebsite(result.url)}>
|
||||
{t('nodeapp.packageDeployer.open')}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
// Reset form and state
|
||||
form.resetFields()
|
||||
setFile(null)
|
||||
setAdvancedVisible(false)
|
||||
setUploadUrl('')
|
||||
|
||||
// Notify parent
|
||||
if (onDeployed) {
|
||||
onDeployed(result)
|
||||
}
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.deployFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deploying ZIP:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeployGit = async (values: any) => {
|
||||
// First check if Node.js is available
|
||||
if (isNodeAvailable === false) {
|
||||
Modal.confirm({
|
||||
title: t('nodeapp.packageDeployer.nodeRequired'),
|
||||
content: t('nodeapp.packageDeployer.installNodePrompt'),
|
||||
okText: t('nodeapp.packageDeployer.installNode'),
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: handleInstallNode
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!values.repoUrl) {
|
||||
notification.warning({
|
||||
message: t('common.warning'),
|
||||
description: t('nodeapp.packageDeployer.noRepoUrlProvided')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setGitLoading(true)
|
||||
|
||||
// 检测是否为Next.js应用
|
||||
const isNextJs = values.repoUrl.toLowerCase().includes('next') ||
|
||||
(values.isNextJs === true);
|
||||
|
||||
// 如果是Next.js应用,自动设置构建步骤
|
||||
if (isNextJs && !values.buildCommand) {
|
||||
values.buildCommand = 'npm run build';
|
||||
notification.info({
|
||||
message: t('nodeapp.packageDeployer.nextJsDetected'),
|
||||
description: t('nodeapp.packageDeployer.buildStepAdded'),
|
||||
duration: 5
|
||||
});
|
||||
}
|
||||
|
||||
// Deploy from Git repository
|
||||
const result = await window.api.nodeapp.deployGit(values.repoUrl, {
|
||||
name: values.name,
|
||||
port: values.port ? parseInt(values.port) : undefined,
|
||||
startCommand: values.startCommand,
|
||||
installCommand: values.installCommand,
|
||||
buildCommand: values.buildCommand
|
||||
})
|
||||
|
||||
if (result) {
|
||||
notification.success({
|
||||
message: t('common.success'),
|
||||
description: t('nodeapp.packageDeployer.deploySuccess', {
|
||||
name: values.name || 'Git App',
|
||||
port: result.port
|
||||
}),
|
||||
btn: (
|
||||
<Button type="primary" size="small" onClick={() => window.api.openWebsite(result.url)}>
|
||||
{t('nodeapp.packageDeployer.open')}
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
||||
// Reset form and state
|
||||
gitForm.resetFields()
|
||||
setGitAdvancedVisible(false)
|
||||
|
||||
// Notify parent
|
||||
if (onDeployed) {
|
||||
onDeployed(result)
|
||||
}
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: t('nodeapp.packageDeployer.deployFailed')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deploying from Git:', error)
|
||||
notification.error({
|
||||
message: t('common.error'),
|
||||
description: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
} finally {
|
||||
setGitLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Card title={t('nodeapp.packageDeployer.deployPackage')}>
|
||||
{isNodeAvailable === false && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t('nodeapp.packageDeployer.nodeNotAvailable')}
|
||||
description={
|
||||
<Space>
|
||||
<Text>{t('nodeapp.packageDeployer.nodeNeeded')}</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleInstallNode}
|
||||
loading={isInstallingNode}
|
||||
>
|
||||
{t('nodeapp.packageDeployer.installNode')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<CloudUploadOutlined /> {t('nodeapp.packageDeployer.deployFromZip')}
|
||||
</span>
|
||||
}
|
||||
key="zip"
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleDeploy}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Text>{t('nodeapp.packageDeployer.description')}</Text>
|
||||
|
||||
<UploadContainer>
|
||||
{file ? (
|
||||
<FileInfo>
|
||||
<div>
|
||||
<CloudUploadOutlined style={{ fontSize: 24, marginRight: 8 }} />
|
||||
<Text strong>{file.name}</Text>
|
||||
</div>
|
||||
<Button size="small" onClick={() => {
|
||||
setFile(null);
|
||||
form.setFieldsValue({ file: null });
|
||||
setUploadUrl('');
|
||||
}}>
|
||||
{t('common.remove')}
|
||||
</Button>
|
||||
</FileInfo>
|
||||
) : (
|
||||
<UploadButton onClick={handleFileSelect}>
|
||||
<CloudUploadOutlined style={{ fontSize: 24, marginBottom: 8 }} />
|
||||
<div>{t('nodeapp.packageDeployer.selectZip')}</div>
|
||||
</UploadButton>
|
||||
)}
|
||||
</UploadContainer>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('nodeapp.form.name')}
|
||||
rules={[{ required: true, message: t('nodeapp.form.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('nodeapp.packageDeployer.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Collapse
|
||||
ghost
|
||||
activeKey={advancedVisible ? ['1'] : []}
|
||||
onChange={() => setAdvancedVisible(!advancedVisible)}
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SettingOutlined style={{ marginRight: 8 }} />
|
||||
{t('nodeapp.packageDeployer.advancedOptions')}
|
||||
</div>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={t('nodeapp.form.port')}
|
||||
help={t('nodeapp.form.portHelp')}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="buildCommand"
|
||||
label={t('nodeapp.form.buildCommand')}
|
||||
help={t('nodeapp.form.buildCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm run build" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="startCommand"
|
||||
label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isNextJs"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
form.setFieldsValue({
|
||||
buildCommand: 'npm run build',
|
||||
startCommand: 'npm run start',
|
||||
installCommand: 'npm install --legacy-peer-deps'
|
||||
});
|
||||
}
|
||||
}}>{t('nodeapp.form.isNextJs')}</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message={t('nodeapp.packageDeployer.nextJsInfo')}
|
||||
description={t('nodeapp.packageDeployer.nextJsDescription')}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={!file}
|
||||
loading={loading}
|
||||
icon={loading ? <Spin size="small" /> : <CloudUploadOutlined />}
|
||||
>
|
||||
{t('nodeapp.packageDeployer.deploy')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</TabPane>
|
||||
|
||||
<TabPane
|
||||
tab={
|
||||
<span>
|
||||
<GithubOutlined /> {t('nodeapp.packageDeployer.deployFromGit')}
|
||||
</span>
|
||||
}
|
||||
key="git"
|
||||
>
|
||||
<Form form={gitForm} layout="vertical" onFinish={handleDeployGit}>
|
||||
<Form.Item
|
||||
name="repoUrl"
|
||||
label={t('nodeapp.packageDeployer.repoUrl')}
|
||||
rules={[{ required: true, message: t('nodeapp.packageDeployer.repoUrlRequired') }]}
|
||||
>
|
||||
<Input placeholder="https://github.com/username/repo" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="name" label={t('nodeapp.form.name')}>
|
||||
<Input placeholder={t('nodeapp.packageDeployer.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label={
|
||||
<span>
|
||||
{t('nodeapp.form.port')}
|
||||
<Popover
|
||||
content={t('nodeapp.form.portHelp')}
|
||||
title={t('common.tips')}
|
||||
>
|
||||
<QuestionCircleOutlined style={{ marginLeft: 8 }} />
|
||||
</Popover>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Input placeholder="3000" type="number" />
|
||||
</Form.Item>
|
||||
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setGitAdvancedVisible(!gitAdvancedVisible)}
|
||||
style={{ paddingLeft: 0, marginBottom: 16 }}
|
||||
>
|
||||
{gitAdvancedVisible
|
||||
? t('nodeapp.packageDeployer.hideAdvanced')
|
||||
: t('nodeapp.packageDeployer.showAdvanced')}
|
||||
</Button>
|
||||
|
||||
{gitAdvancedVisible && (
|
||||
<Collapse ghost>
|
||||
<Panel header={t('nodeapp.packageDeployer.advancedOptions')} key="1">
|
||||
<Form.Item
|
||||
name="installCommand"
|
||||
label={t('nodeapp.form.installCommand')}
|
||||
help={t('nodeapp.form.installCommandHelp')}
|
||||
>
|
||||
<Input placeholder="npm install" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="buildCommand" label={t('nodeapp.form.buildCommand')}
|
||||
help={t('nodeapp.form.buildCommandHelp')}>
|
||||
<Input placeholder="npm run build" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="startCommand" label={t('nodeapp.form.startCommand')}
|
||||
help={t('nodeapp.form.startCommandHelp')}>
|
||||
<Input placeholder="npm start" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="isNextJs" valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
gitForm.setFieldsValue({
|
||||
buildCommand: 'npm run build',
|
||||
startCommand: 'npm run start',
|
||||
installCommand: 'npm install --legacy-peer-deps'
|
||||
});
|
||||
}
|
||||
}}>{t('nodeapp.form.isNextJs')}</Checkbox>
|
||||
</Form.Item>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={gitLoading}>
|
||||
{t('nodeapp.packageDeployer.deploy')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 16px;
|
||||
`
|
||||
|
||||
const UploadContainer = styled.div`
|
||||
margin-bottom: 16px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
`
|
||||
|
||||
const UploadButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
cursor: pointer;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
const FileInfo = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
`
|
||||
|
||||
export default PackageDeployer
|
||||
39
src/renderer/src/pages/nodeapps/index.tsx
Normal file
39
src/renderer/src/pages/nodeapps/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Tabs } from 'antd'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import AppsManager from './AppsManager'
|
||||
import PackageDeployer from './PackageDeployer'
|
||||
import NodeSettings from './NodeSettings'
|
||||
|
||||
const NodeAppsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [activeTab, setActiveTab] = useState('apps')
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tabs activeKey={activeTab} onChange={handleTabChange}>
|
||||
<Tabs.TabPane tab={t('nodeapp.appsManagerTab')} key="apps">
|
||||
<AppsManager />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('nodeapp.packageDeployerTab')} key="deploy">
|
||||
<PackageDeployer />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab={t('nodeapp.nodeSettingsTab')} key="settings">
|
||||
<NodeSettings />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default NodeAppsPage
|
||||
@@ -1,7 +1,10 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setObsidianFolder, setObsidianTages, setObsidianValut } from '@renderer/store/settings'
|
||||
import { setObsidianApiKey, setObsidianUrl } from '@renderer/store/settings'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -14,35 +17,62 @@ const ObsidianSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
||||
// const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
||||
const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
||||
const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
||||
|
||||
const obsidianVault = useSelector((state: RootState) => state.settings.obsidianValut)
|
||||
const obsidianFolder = useSelector((state: RootState) => state.settings.obsidianFolder)
|
||||
const obsidianTags = useSelector((state: RootState) => state.settings.obsidianTages)
|
||||
|
||||
const handleObsidianVaultChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianValut(e.target.value))
|
||||
const handleObsidianApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianApiKey(e.target.value))
|
||||
}
|
||||
|
||||
const handleObsidianFolderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianFolder(e.target.value))
|
||||
const handleObsidianUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianUrl(e.target.value))
|
||||
}
|
||||
|
||||
const handleObsidianVaultBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianValut(e.target.value))
|
||||
const handleObsidianUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
let url = e.target.value
|
||||
// 确保URL以/结尾,但只在失去焦点时执行
|
||||
if (url && !url.endsWith('/')) {
|
||||
url = `${url}/`
|
||||
dispatch(setObsidianUrl(url))
|
||||
}
|
||||
}
|
||||
|
||||
const handleObsidianFolderBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianFolder(e.target.value))
|
||||
const handleObsidianConnectionCheck = async () => {
|
||||
try {
|
||||
if (!obsidianApiKey) {
|
||||
window.message.error(t('settings.data.obsidian.check.empty_api_key'))
|
||||
return
|
||||
}
|
||||
if (!obsidianUrl) {
|
||||
window.message.error(t('settings.data.obsidian.check.empty_url'))
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(`${obsidianUrl}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${obsidianApiKey}`
|
||||
}
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok || !data?.authenticated) {
|
||||
window.message.error(t('settings.data.obsidian.check.fail'))
|
||||
return
|
||||
}
|
||||
|
||||
window.message.success(t('settings.data.obsidian.check.success'))
|
||||
} catch (e) {
|
||||
window.message.error(t('settings.data.obsidian.check.fail'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleObsidianTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianTages(e.target.value))
|
||||
}
|
||||
|
||||
const handleObsidianTagsBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianTages(e.target.value))
|
||||
const handleObsidianHelpClick = () => {
|
||||
MinApp.start({
|
||||
id: 'obsidian-help',
|
||||
name: 'Obsidian Help',
|
||||
url: 'https://github.com/coddingtonbear/obsidian-local-rest-api'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,46 +80,38 @@ const ObsidianSettings: FC = () => {
|
||||
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.obsidian.vault')}</SettingRowTitle>
|
||||
<SettingRowTitle>{t('settings.data.obsidian.url')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={obsidianVault || ''}
|
||||
onChange={handleObsidianVaultChange}
|
||||
onBlur={handleObsidianVaultBlur}
|
||||
value={obsidianUrl || ''}
|
||||
onChange={handleObsidianUrlChange}
|
||||
onBlur={handleObsidianUrlBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.vault_placeholder')}
|
||||
placeholder={t('settings.data.obsidian.url_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.obsidian.folder')}</span>
|
||||
<span>{t('settings.data.obsidian.api_key')}</span>
|
||||
<Tooltip title={t('settings.data.obsidian.help')} placement="left">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer', marginLeft: 4 }}
|
||||
onClick={handleObsidianHelpClick}
|
||||
/>
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
value={obsidianFolder || ''}
|
||||
onChange={handleObsidianFolderChange}
|
||||
onBlur={handleObsidianFolderBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.folder_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.obsidian.tags')}</span>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
value={obsidianTags || ''}
|
||||
onChange={handleObsidianTagsChange}
|
||||
onBlur={handleObsidianTagsBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.tags_placeholder')}
|
||||
type="password"
|
||||
value={obsidianApiKey || ''}
|
||||
onChange={handleObsidianApiKeyChange}
|
||||
style={{ width: 250 }}
|
||||
placeholder={t('settings.data.obsidian.api_key_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleObsidianConnectionCheck}>{t('settings.data.obsidian.check.button')}</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
setWebdavSyncInterval as _setWebdavSyncInterval,
|
||||
setWebdavUser as _setWebdavUser
|
||||
} from '@renderer/store/settings'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, Input, Modal, Select, Spin, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useState } from 'react'
|
||||
@@ -171,7 +170,7 @@ const WebDavSettings: FC = () => {
|
||||
|
||||
const formatFileOption = (file: BackupFile) => {
|
||||
const date = dayjs(file.modifiedTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
const size = formatFileSize(file.size)
|
||||
const size = `${(file.size / 1024).toFixed(2)} KB`
|
||||
return {
|
||||
label: `${file.fileName} (${date}, ${size})`,
|
||||
value: file.fileName
|
||||
|
||||
@@ -13,47 +13,13 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const {
|
||||
language,
|
||||
proxyUrl: storeProxyUrl,
|
||||
theme,
|
||||
setLaunch,
|
||||
setTray,
|
||||
launchOnBoot,
|
||||
launchToTray,
|
||||
trayOnClose,
|
||||
tray,
|
||||
proxyMode: storeProxyMode
|
||||
} = useSettings()
|
||||
const { language, proxyUrl: storeProxyUrl, theme, setTray, tray, proxyMode: storeProxyMode } = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
const { theme: themeMode } = useTheme()
|
||||
|
||||
const updateTray = (isShowTray: boolean) => {
|
||||
setTray(isShowTray)
|
||||
//only set tray on close/launch to tray when tray is enabled
|
||||
if (!isShowTray) {
|
||||
updateTrayOnClose(false)
|
||||
updateLaunchToTray(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTrayOnClose = (isTrayOnClose: boolean) => {
|
||||
setTray(undefined, isTrayOnClose)
|
||||
//in case tray is not enabled, enable it
|
||||
if (isTrayOnClose && !tray) {
|
||||
updateTray(true)
|
||||
}
|
||||
}
|
||||
|
||||
const updateLaunchOnBoot = (isLaunchOnBoot: boolean) => {
|
||||
setLaunch(isLaunchOnBoot)
|
||||
}
|
||||
|
||||
const updateLaunchToTray = (isLaunchToTray: boolean) => {
|
||||
setLaunch(undefined, isLaunchToTray)
|
||||
if (isLaunchToTray && !tray) {
|
||||
updateTray(true)
|
||||
}
|
||||
const updateTray = (value: boolean) => {
|
||||
setTray(value)
|
||||
window.api.setTray(value)
|
||||
}
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -86,10 +52,8 @@ const GeneralSettings: FC = () => {
|
||||
dispatch(setProxyMode(mode))
|
||||
if (mode === 'system') {
|
||||
window.api.setProxy('system')
|
||||
dispatch(_setProxyUrl(undefined))
|
||||
} else if (mode === 'none') {
|
||||
window.api.setProxy(undefined)
|
||||
dispatch(_setProxyUrl(undefined))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +62,7 @@ const GeneralSettings: FC = () => {
|
||||
{ value: 'zh-TW', label: '中文(繁体)', flag: '🇭🇰' },
|
||||
{ value: 'en-US', label: 'English', flag: '🇺🇸' },
|
||||
{ value: 'ja-JP', label: '日本語', flag: '🇯🇵' },
|
||||
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' },
|
||||
{ value: 'el-GR', label: 'Ελληνικά', flag: '🇬🇷' },
|
||||
{ value: 'es-ES', label: 'Español', flag: '🇪🇸' },
|
||||
{ value: 'fr-FR', label: 'Français', flag: '🇫🇷' },
|
||||
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' }
|
||||
{ value: 'ru-RU', label: 'Русский', flag: '🇷🇺' }
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -151,32 +111,11 @@ const GeneralSettings: FC = () => {
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.launch.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.launch.onboot')}</SettingRowTitle>
|
||||
<Switch checked={launchOnBoot} onChange={(checked) => updateLaunchOnBoot(checked)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.launch.totray')}</SettingRowTitle>
|
||||
<Switch checked={launchToTray} onChange={(checked) => updateLaunchToTray(checked)} />
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.tray.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.tray.show')}</SettingRowTitle>
|
||||
<SettingRowTitle>{t('settings.tray.title')}</SettingRowTitle>
|
||||
<Switch checked={tray} onChange={(checked) => updateTray(checked)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.tray.onclose')}</SettingRowTitle>
|
||||
<Switch checked={trayOnClose} onChange={(checked) => updateTrayOnClose(checked)} disabled={!tray} />
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import type { MCPServer } from '@renderer/types'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { Button, Input, Space, Spin, Table, Typography } from 'antd'
|
||||
import { npxFinder } from 'npx-scope-finder'
|
||||
import { type FC, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingTitle } from '..'
|
||||
@@ -21,7 +21,7 @@ interface SearchResult {
|
||||
const NpxSearch: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const { Paragraph, Text, Link } = Typography
|
||||
const { Paragraph, Text } = Typography
|
||||
|
||||
// Add new state variables for npm scope search
|
||||
const [npmScope, setNpmScope] = useState('@modelcontextprotocol')
|
||||
@@ -59,12 +59,8 @@ const NpxSearch: FC = () => {
|
||||
if (formattedResults.length === 0) {
|
||||
window.message.info(t('settings.mcp.npx_list.no_packages'))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
window.message.error(`${t('settings.mcp.npx_list.search_error')}: ${error.message}`)
|
||||
} else {
|
||||
window.message.error(t('settings.mcp.npx_list.search_error'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
window.message.error(`${t('settings.mcp.npx_list.search_error')}: ${error.message}`)
|
||||
} finally {
|
||||
setSearchLoading(false)
|
||||
}
|
||||
@@ -96,7 +92,7 @@ const NpxSearch: FC = () => {
|
||||
<Spin />
|
||||
</div>
|
||||
) : searchResults.length > 0 ? (
|
||||
<Table
|
||||
<Table<SearchResult>
|
||||
dataSource={searchResults}
|
||||
columns={[
|
||||
{
|
||||
@@ -108,18 +104,15 @@ const NpxSearch: FC = () => {
|
||||
{
|
||||
title: t('settings.mcp.npx_list.description'),
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
render: (_, record: SearchResult) => (
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Text ellipsis={{ tooltip: true }}>{record.description}</Text>
|
||||
<Text ellipsis={{ tooltip: true }} type="secondary">
|
||||
<Space direction="vertical" size="small">
|
||||
<Text>{record.description}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{t('settings.mcp.npx_list.usage')}: {record.usage}
|
||||
</Text>
|
||||
<Paragraph ellipsis={{ tooltip: true }}>
|
||||
<Link href={record.npmLink} target="_blank" rel="noopener noreferrer">
|
||||
{record.npmLink}
|
||||
</Link>
|
||||
</Paragraph>
|
||||
<a href={record.npmLink} target="_blank" rel="noopener noreferrer" style={{ fontSize: '12px' }}>
|
||||
{record.npmLink}
|
||||
</a>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
@@ -132,7 +125,7 @@ const NpxSearch: FC = () => {
|
||||
{
|
||||
title: t('settings.mcp.npx_list.actions'),
|
||||
key: 'actions',
|
||||
width: '120px',
|
||||
width: '100px',
|
||||
render: (_, record: SearchResult) => (
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -156,6 +149,7 @@ const NpxSearch: FC = () => {
|
||||
}
|
||||
]}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
isReasoningModel,
|
||||
isRerankModel,
|
||||
isVisionModel,
|
||||
isWebSearchModel,
|
||||
SYSTEM_MODELS
|
||||
@@ -18,7 +17,7 @@ import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/ut
|
||||
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd'
|
||||
import Search from 'antd/es/input/Search'
|
||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -45,7 +44,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [filterType, setFilterType] = useState<string>('all')
|
||||
const { t, i18n } = useTranslation()
|
||||
const searchInputRef = useRef<any>(null)
|
||||
|
||||
const systemModels = SYSTEM_MODELS[_provider.id] || []
|
||||
const allModels = uniqBy([...systemModels, ...listModels, ...models], 'id')
|
||||
@@ -72,8 +70,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
return isEmbeddingModel(model)
|
||||
case 'function_calling':
|
||||
return isFunctionCallingModel(model)
|
||||
case 'rerank':
|
||||
return isRerankModel(model)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
@@ -131,14 +127,6 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && searchInputRef.current) {
|
||||
setTimeout(() => {
|
||||
searchInputRef.current?.focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const ModalHeader = () => {
|
||||
return (
|
||||
<Flex>
|
||||
@@ -160,7 +148,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}
|
||||
width="680px"
|
||||
width="600px"
|
||||
styles={{
|
||||
content: { padding: 0 },
|
||||
header: { padding: 22, paddingBottom: 15 }
|
||||
@@ -168,23 +156,17 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
centered>
|
||||
<SearchContainer>
|
||||
<Center>
|
||||
<Radio.Group
|
||||
size={i18n.language.startsWith('zh') ? 'middle' : 'small'}
|
||||
value={filterType}
|
||||
onChange={(e) => setFilterType(e.target.value)}
|
||||
buttonStyle="solid">
|
||||
<Radio.Group value={filterType} onChange={(e) => setFilterType(e.target.value)} buttonStyle="solid">
|
||||
<Radio.Button value="all">{t('models.all')}</Radio.Button>
|
||||
<Radio.Button value="reasoning">{t('models.type.reasoning')}</Radio.Button>
|
||||
<Radio.Button value="vision">{t('models.type.vision')}</Radio.Button>
|
||||
<Radio.Button value="websearch">{t('models.type.websearch')}</Radio.Button>
|
||||
<Radio.Button value="free">{t('models.type.free')}</Radio.Button>
|
||||
<Radio.Button value="embedding">{t('models.type.embedding')}</Radio.Button>
|
||||
<Radio.Button value="rerank">{t('models.type.rerank')}</Radio.Button>
|
||||
<Radio.Button value="function_calling">{t('models.type.function_calling')}</Radio.Button>
|
||||
<Radio.Button value="reasoning">{t('models.reasoning')}</Radio.Button>
|
||||
<Radio.Button value="vision">{t('models.vision')}</Radio.Button>
|
||||
<Radio.Button value="websearch">{t('models.websearch')}</Radio.Button>
|
||||
<Radio.Button value="free">{t('models.free')}</Radio.Button>
|
||||
<Radio.Button value="embedding">{t('models.embedding')}</Radio.Button>
|
||||
<Radio.Button value="function_calling">{t('models.function_calling')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Center>
|
||||
<Search
|
||||
ref={searchInputRef}
|
||||
placeholder={t('settings.provider.search_placeholder')}
|
||||
allowClear
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Model, Provider } from '@renderer/types'
|
||||
import { maskApiKey } from '@renderer/utils/api'
|
||||
import { Avatar, Button, Card, Flex, Space, Tooltip, Typography } from 'antd'
|
||||
import { groupBy, sortBy, toPairs } from 'lodash'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -38,7 +38,6 @@ const STATUS_COLORS = {
|
||||
interface ModelListProps {
|
||||
provider: Provider
|
||||
modelStatuses?: ModelStatus[]
|
||||
searchText?: string
|
||||
}
|
||||
|
||||
export interface ModelStatus {
|
||||
@@ -166,7 +165,7 @@ function useModelStatusRendering() {
|
||||
return { renderStatusIndicator, renderLatencyText }
|
||||
}
|
||||
|
||||
const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuses = [], searchText = '' }) => {
|
||||
const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuses = [] }) => {
|
||||
const { t } = useTranslation()
|
||||
const { provider } = useProvider(_provider.id)
|
||||
const { updateProvider, models, removeModel } = useProvider(_provider.id)
|
||||
@@ -180,21 +179,7 @@ const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuse
|
||||
const modelsWebsite = providerConfig?.websites?.models
|
||||
|
||||
const [editingModel, setEditingModel] = useState<Model | null>(null)
|
||||
const [debouncedSearchText, setDebouncedSearchText] = useState(searchText)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchText(searchText)
|
||||
}, 50)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchText])
|
||||
|
||||
const filteredModels = debouncedSearchText
|
||||
? models.filter((model) => model.name.toLowerCase().includes(debouncedSearchText.toLowerCase()))
|
||||
: models
|
||||
|
||||
const modelGroups = groupBy(filteredModels, 'group')
|
||||
const modelGroups = groupBy(models, 'group')
|
||||
const sortedModelGroups = sortBy(toPairs(modelGroups), [0]).reduce((acc, [key, value]) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
@@ -257,7 +242,6 @@ const ModelList: React.FC<ModelListProps> = ({ provider: _provider, modelStatuse
|
||||
{sortedModelGroups[group].map((model) => {
|
||||
const modelStatus = modelStatuses.find((status) => status.model.id === model.id)
|
||||
const isChecking = modelStatus?.checking === true
|
||||
console.log('model', model.id, getModelLogo(model.id))
|
||||
|
||||
return (
|
||||
<ModelListItem key={model.id}>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Input, Tooltip } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface ModelListSearchBarProps {
|
||||
onSearch: (text: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A collapsible search bar for the model list
|
||||
* Renders as an icon initially, expands to full search input when clicked
|
||||
*/
|
||||
const ModelListSearchBar: React.FC<ModelListSearchBarProps> = ({ onSearch }) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchVisible, setSearchVisible] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const handleTextChange = (text: string) => {
|
||||
setSearchText(text)
|
||||
onSearch(text)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setSearchText('')
|
||||
setSearchVisible(false)
|
||||
onSearch('')
|
||||
}
|
||||
|
||||
return searchVisible ? (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t('models.search')}
|
||||
size="small"
|
||||
style={{ width: '160px' }}
|
||||
suffix={<SearchOutlined style={{ color: 'var(--color-text-3)' }} />}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleTextChange('')
|
||||
if (!searchText) setSearchVisible(false)
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!searchText) setSearchVisible(false)
|
||||
}}
|
||||
autoFocus
|
||||
allowClear
|
||||
onClear={handleClear}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip title={t('models.search')} mouseEnterDelay={0.5}>
|
||||
<SearchOutlined onClick={() => setSearchVisible(true)} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelListSearchBar
|
||||
@@ -12,7 +12,7 @@ import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/servic
|
||||
import { Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
import { providerCharge } from '@renderer/utils/oauth'
|
||||
import { Button, Divider, Flex, Input, Space, Switch, Tooltip } from 'antd'
|
||||
import { Button, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
@@ -34,7 +34,6 @@ import GraphRAGSettings from './GraphRAGSettings'
|
||||
import HealthCheckPopup from './HealthCheckPopup'
|
||||
import LMStudioSettings from './LMStudioSettings'
|
||||
import ModelList, { ModelStatus } from './ModelList'
|
||||
import ModelListSearchBar from './ModelListSearchBar'
|
||||
import OllamSettings from './OllamaSettings'
|
||||
import ProviderSettingsPopup from './ProviderSettingsPopup'
|
||||
import SelectProviderModelPopup from './SelectProviderModelPopup'
|
||||
@@ -50,7 +49,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const [apiVersion, setApiVersion] = useState(provider.apiVersion)
|
||||
const [apiValid, setApiValid] = useState(false)
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { updateProvider, models } = useProvider(provider.id)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
@@ -97,10 +95,14 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
.map((k) => k.trim())
|
||||
.filter((k) => k)
|
||||
|
||||
// Add an empty key to enable health checks for local models.
|
||||
// Error messages will be shown for each model if a valid key is needed.
|
||||
if (keys.length === 0) {
|
||||
keys.push('')
|
||||
window.message.error({
|
||||
key: 'no-api-keys',
|
||||
style: { marginTop: '3vh' },
|
||||
duration: 5,
|
||||
content: t('settings.models.check.no_api_keys')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Show configuration dialog to get health check parameters
|
||||
@@ -110,7 +112,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
apiKeys: keys
|
||||
})
|
||||
|
||||
if (result.cancelled) {
|
||||
if (result.cancelled || result.apiKeys.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,9 +162,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
duration: 10,
|
||||
content: t('settings.models.check.model_status_summary', {
|
||||
provider: provider.name,
|
||||
count_passed: successModels.length + partialModels.length,
|
||||
count_partial: partialModels.length,
|
||||
count_failed: failedModels.length
|
||||
count_passed: successModels.length,
|
||||
count_failed: failedModels.length,
|
||||
count_partial: partialModels.length
|
||||
})
|
||||
})
|
||||
|
||||
@@ -269,11 +271,9 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
</Link>
|
||||
)}
|
||||
{!provider.isSystem && (
|
||||
<SettingOutlined
|
||||
type="text"
|
||||
style={{ width: 30 }}
|
||||
onClick={() => ProviderSettingsPopup.show({ provider })}
|
||||
/>
|
||||
<Button type="text" style={{ width: 30 }} onClick={() => ProviderSettingsPopup.show({ provider })}>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Switch
|
||||
@@ -363,25 +363,22 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
)}
|
||||
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
|
||||
<SettingSubtitle style={{ marginBottom: 5 }}>
|
||||
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<span>{t('common.models')}</span>
|
||||
<Space>
|
||||
<span>{t('common.models')}</span>
|
||||
{!isEmpty(models) && <ModelListSearchBar onSearch={setSearchText} />}
|
||||
</Space>
|
||||
{!isEmpty(models) && (
|
||||
<Tooltip title={t('settings.models.check.button_caption')} mouseEnterDelay={0.5}>
|
||||
{!isEmpty(models) && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<HeartOutlined />}
|
||||
onClick={onHealthCheck}
|
||||
loading={isHealthChecking}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
title={t('settings.models.check.button_caption')}></Button>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
</SettingSubtitle>
|
||||
<ModelList provider={provider} modelStatuses={modelStatuses} searchText={searchText} />
|
||||
<ModelList provider={provider} modelStatuses={modelStatuses} />
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -197,7 +197,9 @@ const TranslatePage: FC = () => {
|
||||
|
||||
// Calculate scroll position by ratio
|
||||
const inputScrollRatio = inputEl.scrollTop / (inputEl.scrollHeight - inputEl.clientHeight || 1)
|
||||
outputEl.scrollTop = inputScrollRatio * (outputEl.scrollHeight - outputEl.clientHeight || 1)
|
||||
const outputScrollPosition = inputScrollRatio * (outputEl.scrollHeight - outputEl.clientHeight || 1)
|
||||
|
||||
outputEl.scrollTop = outputScrollPosition
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
isProgrammaticScroll.current = false
|
||||
@@ -215,7 +217,9 @@ const TranslatePage: FC = () => {
|
||||
|
||||
// Calculate scroll position by ratio
|
||||
const outputScrollRatio = outputEl.scrollTop / (outputEl.scrollHeight - outputEl.clientHeight || 1)
|
||||
inputEl.scrollTop = outputScrollRatio * (inputEl.scrollHeight - inputEl.clientHeight || 1)
|
||||
const inputScrollPosition = outputScrollRatio * (inputEl.scrollHeight - inputEl.clientHeight || 1)
|
||||
|
||||
inputEl.scrollTop = inputScrollPosition
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
isProgrammaticScroll.current = false
|
||||
@@ -377,7 +381,7 @@ const TranslatePage: FC = () => {
|
||||
/>
|
||||
</OperationBar>
|
||||
|
||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className="selectable">
|
||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll}>
|
||||
{result || t('translate.output.placeholder')}
|
||||
</OutputText>
|
||||
</OutputContainer>
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
ContentListUnion,
|
||||
createPartFromBase64,
|
||||
FinishReason,
|
||||
GenerateContentResponse,
|
||||
GoogleGenAI
|
||||
} from '@google/genai'
|
||||
import {
|
||||
Content,
|
||||
FileDataPart,
|
||||
@@ -42,19 +35,16 @@ import axios from 'axios'
|
||||
import { isEmpty, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { ChunkCallbackData, CompletionsParams } from '.'
|
||||
import { CompletionsParams } from '.'
|
||||
import BaseProvider from './BaseProvider'
|
||||
|
||||
export default class GeminiProvider extends BaseProvider {
|
||||
private sdk: GoogleGenerativeAI
|
||||
private requestOptions: RequestOptions
|
||||
private imageSdk: GoogleGenAI
|
||||
|
||||
constructor(provider: Provider) {
|
||||
super(provider)
|
||||
this.sdk = new GoogleGenerativeAI(this.apiKey)
|
||||
/// this sdk is experimental
|
||||
this.imageSdk = new GoogleGenAI({ apiKey: this.apiKey })
|
||||
this.requestOptions = {
|
||||
baseUrl: this.getBaseURL()
|
||||
}
|
||||
@@ -115,25 +105,6 @@ export default class GeminiProvider extends BaseProvider {
|
||||
const role = message.role === 'user' ? 'user' : 'model'
|
||||
|
||||
const parts: Part[] = [{ text: await this.getMessageContent(message) }]
|
||||
// Add any generated images from previous responses
|
||||
if (message.metadata?.generateImage?.images && message.metadata.generateImage.images.length > 0) {
|
||||
for (const imageUrl of message.metadata.generateImage.images) {
|
||||
if (imageUrl && imageUrl.startsWith('data:')) {
|
||||
// Extract base64 data and mime type from the data URL
|
||||
const matches = imageUrl.match(/^data:(.+);base64,(.*)$/)
|
||||
if (matches && matches.length === 3) {
|
||||
const mimeType = matches[1]
|
||||
const base64Data = matches[2]
|
||||
parts.push({
|
||||
inlineData: {
|
||||
data: base64Data,
|
||||
mimeType: mimeType
|
||||
}
|
||||
} as InlineDataPart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of message.files || []) {
|
||||
if (file.type === FileTypes.IMAGE) {
|
||||
@@ -208,184 +179,180 @@ export default class GeminiProvider extends BaseProvider {
|
||||
* @param onFilterMessages - The onFilterMessages callback
|
||||
*/
|
||||
public async completions({ messages, assistant, mcpTools, onChunk, onFilterMessages }: CompletionsParams) {
|
||||
if (assistant.enableGenerateImage) {
|
||||
await this.generateImageExp({ messages, assistant, onFilterMessages, onChunk })
|
||||
} else {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant)
|
||||
|
||||
const userMessages = filterUserRoleStartMessages(
|
||||
filterEmptyMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
||||
)
|
||||
onFilterMessages(userMessages)
|
||||
const userMessages = filterUserRoleStartMessages(
|
||||
filterEmptyMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
||||
)
|
||||
onFilterMessages(userMessages)
|
||||
|
||||
const userLastMessage = userMessages.pop()
|
||||
const userLastMessage = userMessages.pop()
|
||||
|
||||
const history: Content[] = []
|
||||
const history: Content[] = []
|
||||
|
||||
for (const message of userMessages) {
|
||||
history.push(await this.getMessageContents(message))
|
||||
}
|
||||
for (const message of userMessages) {
|
||||
history.push(await this.getMessageContents(message))
|
||||
}
|
||||
|
||||
const tools = mcpToolsToGeminiTools(mcpTools)
|
||||
const toolResponses: MCPToolResponse[] = []
|
||||
const tools = mcpToolsToGeminiTools(mcpTools)
|
||||
const toolResponses: MCPToolResponse[] = []
|
||||
|
||||
if (assistant.enableWebSearch && isWebSearchModel(model)) {
|
||||
tools.push({
|
||||
// @ts-ignore googleSearch is not a valid tool for Gemini
|
||||
googleSearch: {}
|
||||
})
|
||||
}
|
||||
if (assistant.enableWebSearch && isWebSearchModel(model)) {
|
||||
tools.push({
|
||||
// @ts-ignore googleSearch is not a valid tool for Gemini
|
||||
googleSearch: {}
|
||||
})
|
||||
}
|
||||
|
||||
const geminiModel = this.sdk.getGenerativeModel(
|
||||
{
|
||||
model: model.id,
|
||||
...(isGemmaModel(model) ? {} : { systemInstruction: assistant.prompt }),
|
||||
safetySettings: this.getSafetySettings(model.id),
|
||||
tools: tools,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
topP: assistant?.settings?.topP,
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
},
|
||||
this.requestOptions
|
||||
)
|
||||
|
||||
const chat = geminiModel.startChat({ history })
|
||||
const messageContents = await this.getMessageContents(userLastMessage!)
|
||||
|
||||
if (isGemmaModel(model) && assistant.prompt) {
|
||||
const isFirstMessage = history.length === 0
|
||||
if (isFirstMessage) {
|
||||
const systemMessage = {
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text:
|
||||
'<start_of_turn>user\n' +
|
||||
assistant.prompt +
|
||||
'<end_of_turn>\n' +
|
||||
'<start_of_turn>user\n' +
|
||||
messageContents.parts[0].text +
|
||||
'<end_of_turn>'
|
||||
}
|
||||
]
|
||||
}
|
||||
messageContents.parts = systemMessage.parts
|
||||
const geminiModel = this.sdk.getGenerativeModel(
|
||||
{
|
||||
model: model.id,
|
||||
...(isGemmaModel(model) ? {} : { systemInstruction: assistant.prompt }),
|
||||
safetySettings: this.getSafetySettings(model.id),
|
||||
tools: tools,
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
topP: assistant?.settings?.topP,
|
||||
...this.getCustomParameters(assistant)
|
||||
}
|
||||
},
|
||||
this.requestOptions
|
||||
)
|
||||
|
||||
const chat = geminiModel.startChat({ history })
|
||||
const messageContents = await this.getMessageContents(userLastMessage!)
|
||||
|
||||
if (isGemmaModel(model) && assistant.prompt) {
|
||||
const isFirstMessage = history.length === 0
|
||||
if (isFirstMessage) {
|
||||
const systemMessage = {
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
text:
|
||||
'<start_of_turn>user\n' +
|
||||
assistant.prompt +
|
||||
'<end_of_turn>\n' +
|
||||
'<start_of_turn>user\n' +
|
||||
messageContents.parts[0].text +
|
||||
'<end_of_turn>'
|
||||
}
|
||||
]
|
||||
}
|
||||
messageContents.parts = systemMessage.parts
|
||||
}
|
||||
}
|
||||
|
||||
const start_time_millsec = new Date().getTime()
|
||||
const { abortController, cleanup } = this.createAbortController(userLastMessage?.id)
|
||||
const { signal } = abortController
|
||||
const start_time_millsec = new Date().getTime()
|
||||
const { abortController, cleanup } = this.createAbortController(userLastMessage?.id)
|
||||
const { signal } = abortController
|
||||
|
||||
if (!streamOutput) {
|
||||
const { response } = await chat.sendMessage(messageContents.parts, { signal })
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
onChunk({
|
||||
text: response.candidates?.[0].content.parts[0].text,
|
||||
usage: {
|
||||
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
metrics: {
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount,
|
||||
time_completion_millsec,
|
||||
time_first_token_millsec: 0
|
||||
},
|
||||
search: response.candidates?.[0]?.groundingMetadata
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal })
|
||||
let time_first_token_millsec = 0
|
||||
|
||||
const processStream = async (stream: GenerateContentStreamResult, idx: number) => {
|
||||
for await (const chunk of stream.stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||
|
||||
if (time_first_token_millsec == 0) {
|
||||
time_first_token_millsec = new Date().getTime() - start_time_millsec
|
||||
}
|
||||
|
||||
if (!streamOutput) {
|
||||
const { response } = await chat.sendMessage(messageContents.parts, { signal })
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
|
||||
const functionCalls = chunk.functionCalls()
|
||||
|
||||
if (functionCalls) {
|
||||
const fcallParts: FunctionCallPart[] = []
|
||||
const fcRespParts: FunctionResponsePart[] = []
|
||||
for (const call of functionCalls) {
|
||||
console.log('Function call:', call)
|
||||
fcallParts.push({ functionCall: call } as FunctionCallPart)
|
||||
const mcpTool = geminiFunctionCallToMcpTool(mcpTools, call)
|
||||
if (mcpTool) {
|
||||
upsertMCPToolResponse(
|
||||
toolResponses,
|
||||
{
|
||||
tool: mcpTool,
|
||||
status: 'invoking',
|
||||
id: `${call.name}-${idx}`
|
||||
},
|
||||
onChunk
|
||||
)
|
||||
const toolCallResponse = await callMCPTool(mcpTool)
|
||||
fcRespParts.push({
|
||||
functionResponse: {
|
||||
name: mcpTool.id,
|
||||
response: toolCallResponse
|
||||
}
|
||||
})
|
||||
upsertMCPToolResponse(
|
||||
toolResponses,
|
||||
{
|
||||
tool: mcpTool,
|
||||
status: 'done',
|
||||
response: toolCallResponse,
|
||||
id: `${call.name}-${idx}`
|
||||
},
|
||||
onChunk
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fcRespParts) {
|
||||
history.push(messageContents)
|
||||
history.push({
|
||||
role: 'model',
|
||||
parts: fcallParts
|
||||
})
|
||||
const newChat = geminiModel.startChat({ history })
|
||||
const newStream = await newChat.sendMessageStream(fcRespParts, { signal })
|
||||
await processStream(newStream, idx + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onChunk({
|
||||
text: response.candidates?.[0].content.parts[0].text,
|
||||
text: chunk.text(),
|
||||
usage: {
|
||||
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
||||
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
metrics: {
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount,
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
|
||||
time_completion_millsec,
|
||||
time_first_token_millsec: 0
|
||||
time_first_token_millsec
|
||||
},
|
||||
search: response.candidates?.[0]?.groundingMetadata
|
||||
search: chunk.candidates?.[0]?.groundingMetadata,
|
||||
mcpToolResponse: toolResponses
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userMessagesStream = await chat.sendMessageStream(messageContents.parts, { signal })
|
||||
let time_first_token_millsec = 0
|
||||
|
||||
const processStream = async (stream: GenerateContentStreamResult, idx: number) => {
|
||||
for await (const chunk of stream.stream) {
|
||||
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
|
||||
|
||||
if (time_first_token_millsec == 0) {
|
||||
time_first_token_millsec = new Date().getTime() - start_time_millsec
|
||||
}
|
||||
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
|
||||
const functionCalls = chunk.functionCalls()
|
||||
|
||||
if (functionCalls) {
|
||||
const fcallParts: FunctionCallPart[] = []
|
||||
const fcRespParts: FunctionResponsePart[] = []
|
||||
for (const call of functionCalls) {
|
||||
console.log('Function call:', call)
|
||||
fcallParts.push({ functionCall: call } as FunctionCallPart)
|
||||
const mcpTool = geminiFunctionCallToMcpTool(mcpTools, call)
|
||||
if (mcpTool) {
|
||||
upsertMCPToolResponse(
|
||||
toolResponses,
|
||||
{
|
||||
tool: mcpTool,
|
||||
status: 'invoking',
|
||||
id: `${call.name}-${idx}`
|
||||
},
|
||||
onChunk
|
||||
)
|
||||
const toolCallResponse = await callMCPTool(mcpTool)
|
||||
fcRespParts.push({
|
||||
functionResponse: {
|
||||
name: mcpTool.id,
|
||||
response: toolCallResponse
|
||||
}
|
||||
})
|
||||
upsertMCPToolResponse(
|
||||
toolResponses,
|
||||
{
|
||||
tool: mcpTool,
|
||||
status: 'done',
|
||||
response: toolCallResponse,
|
||||
id: `${call.name}-${idx}`
|
||||
},
|
||||
onChunk
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (fcRespParts) {
|
||||
history.push(messageContents)
|
||||
history.push({
|
||||
role: 'model',
|
||||
parts: fcallParts
|
||||
})
|
||||
const newChat = geminiModel.startChat({ history })
|
||||
const newStream = await newChat.sendMessageStream(fcRespParts, { signal })
|
||||
await processStream(newStream, idx + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onChunk({
|
||||
text: chunk.text(),
|
||||
usage: {
|
||||
prompt_tokens: chunk.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: chunk.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
metrics: {
|
||||
completion_tokens: chunk.usageMetadata?.candidatesTokenCount,
|
||||
time_completion_millsec,
|
||||
time_first_token_millsec
|
||||
},
|
||||
search: chunk.candidates?.[0]?.groundingMetadata,
|
||||
mcpToolResponse: toolResponses
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await processStream(userMessagesStream, 0).finally(cleanup)
|
||||
}
|
||||
|
||||
await processStream(userMessagesStream, 0).finally(cleanup)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,150 +536,6 @@ export default class GeminiProvider extends BaseProvider {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图像
|
||||
* @param messages - 消息列表
|
||||
* @param assistant - 助手配置
|
||||
* @param onChunk - 处理生成块的回调
|
||||
* @param onFilterMessages - 过滤消息的回调
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
private async generateImageExp({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
||||
const defaultModel = getDefaultModel()
|
||||
const model = assistant.model || defaultModel
|
||||
const { contextCount } = getAssistantSettings(assistant)
|
||||
|
||||
const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
||||
onFilterMessages(userMessages)
|
||||
|
||||
const userLastMessage = userMessages.pop()
|
||||
if (!userLastMessage) {
|
||||
throw new Error('No user message found')
|
||||
}
|
||||
|
||||
const history: Content[] = []
|
||||
|
||||
for (const message of userMessages) {
|
||||
history.push(await this.getMessageContents(message))
|
||||
}
|
||||
|
||||
const userLastMessageContent = await this.getMessageContents(userLastMessage)
|
||||
const allContents = [...history, userLastMessageContent]
|
||||
|
||||
let contents: ContentListUnion = allContents.length > 0 ? (allContents as ContentListUnion) : []
|
||||
|
||||
contents = await this.addImageFileToContents(userLastMessage, contents)
|
||||
|
||||
const response = await this.callGeminiGenerateContent(model.id, contents)
|
||||
|
||||
console.log('response', response)
|
||||
|
||||
const { isValid, message } = this.isValidGeminiResponse(response)
|
||||
if (!isValid) {
|
||||
throw new Error(`Gemini API error: ${message}`)
|
||||
}
|
||||
|
||||
this.processGeminiImageResponse(response, onChunk)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加图片文件到内容列表
|
||||
* @param message - 用户消息
|
||||
* @param contents - 内容列表
|
||||
* @returns 更新后的内容列表
|
||||
*/
|
||||
private async addImageFileToContents(message: Message, contents: ContentListUnion): Promise<ContentListUnion> {
|
||||
if (message.files && message.files.length > 0) {
|
||||
const file = message.files[0]
|
||||
const fileContent = await window.api.file.base64Image(file.id + file.ext)
|
||||
|
||||
if (fileContent && fileContent.base64) {
|
||||
const contentsArray = Array.isArray(contents) ? contents : [contents]
|
||||
return [...contentsArray, createPartFromBase64(fileContent.base64, fileContent.mime)]
|
||||
}
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Gemini API生成内容
|
||||
* @param modelId - 模型ID
|
||||
* @param contents - 内容列表
|
||||
* @returns 生成结果
|
||||
*/
|
||||
private async callGeminiGenerateContent(
|
||||
modelId: string,
|
||||
contents: ContentListUnion
|
||||
): Promise<GenerateContentResponse> {
|
||||
try {
|
||||
return await this.imageSdk.models.generateContent({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
responseModalities: ['Text', 'Image'],
|
||||
responseMimeType: 'text/plain'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Gemini API error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Gemini响应是否有效
|
||||
* @param response - Gemini响应
|
||||
* @returns 是否有效
|
||||
*/
|
||||
private isValidGeminiResponse(response: GenerateContentResponse): { isValid: boolean; message: string } {
|
||||
return {
|
||||
isValid: response?.candidates?.[0]?.finishReason === FinishReason.STOP ? true : false,
|
||||
message: response?.candidates?.[0]?.finishReason || ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Gemini图像响应
|
||||
* @param response - Gemini响应
|
||||
* @param onChunk - 处理生成块的回调
|
||||
*/
|
||||
private processGeminiImageResponse(response: any, onChunk: (chunk: ChunkCallbackData) => void): void {
|
||||
const parts = response.candidates[0].content.parts
|
||||
|
||||
// 提取图像数据
|
||||
const images = parts
|
||||
.filter((part: Part) => part.inlineData)
|
||||
.map((part: Part) => {
|
||||
if (!part.inlineData) {
|
||||
return null
|
||||
}
|
||||
const dataPrefix = `data:${part.inlineData.mimeType || 'image/png'};base64,`
|
||||
return part.inlineData.data.startsWith('data:') ? part.inlineData.data : dataPrefix + part.inlineData.data
|
||||
})
|
||||
|
||||
// 提取文本数据
|
||||
const text = parts
|
||||
.filter((part: Part) => part.text !== undefined)
|
||||
.map((part: Part) => part.text)
|
||||
.join('')
|
||||
|
||||
// 返回结果
|
||||
onChunk({
|
||||
text,
|
||||
generateImage: {
|
||||
images
|
||||
},
|
||||
usage: {
|
||||
prompt_tokens: response.usageMetadata?.promptTokenCount || 0,
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount || 0,
|
||||
total_tokens: response.usageMetadata?.totalTokenCount || 0
|
||||
},
|
||||
metrics: {
|
||||
completion_tokens: response.usageMetadata?.candidatesTokenCount
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the model is valid
|
||||
* @param model - The model
|
||||
|
||||
@@ -474,7 +474,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
const finishReason = chunk.choices[0]?.finish_reason
|
||||
|
||||
if (delta?.tool_calls?.length) {
|
||||
if (delta?.tool_calls) {
|
||||
const chunkToolCalls = delta.tool_calls
|
||||
for (const t of chunkToolCalls) {
|
||||
const { index, id, function: fn, type } = t
|
||||
|
||||
1
src/renderer/src/providers/index.d.ts
vendored
1
src/renderer/src/providers/index.d.ts
vendored
@@ -9,7 +9,6 @@ interface ChunkCallbackData {
|
||||
search?: GroundingMetadata
|
||||
citations?: string[]
|
||||
mcpToolResponse?: MCPToolResponse[]
|
||||
generateImage?: GenerateImageResponse
|
||||
}
|
||||
|
||||
interface CompletionsParams {
|
||||
|
||||
@@ -111,7 +111,7 @@ export async function fetchChatCompletion({
|
||||
messages: filterUsefulMessages(messages),
|
||||
assistant,
|
||||
onFilterMessages: (messages) => (_messages = messages),
|
||||
onChunk: ({ text, reasoning_content, usage, metrics, search, citations, mcpToolResponse, generateImage }) => {
|
||||
onChunk: ({ text, reasoning_content, usage, metrics, search, citations, mcpToolResponse }) => {
|
||||
message.content = message.content + text || ''
|
||||
message.usage = usage
|
||||
message.metrics = metrics
|
||||
@@ -127,12 +127,6 @@ export async function fetchChatCompletion({
|
||||
if (mcpToolResponse) {
|
||||
message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) }
|
||||
}
|
||||
if (generateImage) {
|
||||
message.metadata = {
|
||||
...message.metadata,
|
||||
generateImage: generateImage
|
||||
}
|
||||
}
|
||||
|
||||
// Handle citations from Perplexity API
|
||||
if (isFirstChunk && citations) {
|
||||
@@ -168,7 +162,6 @@ export async function fetchChatCompletion({
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('message', message)
|
||||
} catch (error: any) {
|
||||
if (isAbortError(error)) {
|
||||
message.status = 'paused'
|
||||
@@ -227,9 +220,7 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
try {
|
||||
const text = await AI.summaries(filterMessages(messages), assistant)
|
||||
// Remove all quotes from the text
|
||||
return text?.replace(/["']/g, '') || null
|
||||
return await AI.summaries(filterMessages(messages), assistant)
|
||||
} catch (error: any) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import mcp from './mcp'
|
||||
import messagesReducer from './messages'
|
||||
import migrate from './migrate'
|
||||
import minapps from './minapps'
|
||||
import nodeapps from './nodeapps'
|
||||
import paintings from './paintings'
|
||||
import runtime from './runtime'
|
||||
import settings from './settings'
|
||||
@@ -33,6 +34,7 @@ const rootReducer = combineReducers({
|
||||
websearch,
|
||||
mcp,
|
||||
copilot,
|
||||
nodeapps,
|
||||
messages: messagesReducer
|
||||
})
|
||||
|
||||
@@ -40,7 +42,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 84,
|
||||
version: 82,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -456,16 +456,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
|
||||
models: SYSTEM_MODELS.gpustack,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'voyageai',
|
||||
name: 'VoyageAI',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.voyageai.com',
|
||||
models: SYSTEM_MODELS.voyageai,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user