Compare commits
6 Commits
feat/mcp-u
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1cd453ca4 | ||
|
|
ca3c9927d2 | ||
|
|
8e82f54c2b | ||
|
|
ea797ac88a | ||
|
|
fe097a937c | ||
|
|
1ac32bad14 |
@@ -118,6 +118,7 @@ releaseInfo:
|
|||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
输入框快捷菜单增加清除按钮
|
输入框快捷菜单增加清除按钮
|
||||||
侧边栏增加代码工具入口,代码工具增加环境变量设置
|
侧边栏增加代码工具入口,代码工具增加环境变量设置
|
||||||
|
翻译列表增加搜索功能
|
||||||
小程序增加多语言显示
|
小程序增加多语言显示
|
||||||
优化 MCP 服务器列表
|
优化 MCP 服务器列表
|
||||||
新增 Web 搜索图标
|
新增 Web 搜索图标
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.5.7-rc.2",
|
"version": "1.5.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default abstract class BasePreprocessProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async readPdf(buffer: Buffer) {
|
public async readPdf(buffer: Buffer) {
|
||||||
const pdfDoc = await PDFDocument.load(buffer)
|
const pdfDoc = await PDFDocument.load(buffer, { ignoreEncryption: true })
|
||||||
return {
|
return {
|
||||||
numPages: pdfDoc.getPageCount()
|
numPages: pdfDoc.getPageCount()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import {
|
|||||||
ResponderProvided
|
ResponderProvided
|
||||||
} from '@hello-pangea/dnd'
|
} from '@hello-pangea/dnd'
|
||||||
import { droppableReorder } from '@renderer/utils'
|
import { droppableReorder } from '@renderer/utils'
|
||||||
import { List } from 'antd'
|
import { List, ListProps } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
list: T[]
|
list: T[]
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
listStyle?: React.CSSProperties
|
listStyle?: React.CSSProperties
|
||||||
|
listProps?: ListProps<T>
|
||||||
children: (item: T, index: number) => React.ReactNode
|
children: (item: T, index: number) => React.ReactNode
|
||||||
onUpdate: (list: T[]) => void
|
onUpdate: (list: T[]) => void
|
||||||
onDragStart?: OnDragStartResponder
|
onDragStart?: OnDragStartResponder
|
||||||
@@ -28,6 +29,7 @@ const DraggableList: FC<Props<any>> = ({
|
|||||||
list,
|
list,
|
||||||
style,
|
style,
|
||||||
listStyle,
|
listStyle,
|
||||||
|
listProps,
|
||||||
droppableProps,
|
droppableProps,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@@ -51,6 +53,7 @@ const DraggableList: FC<Props<any>> = ({
|
|||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
|
||||||
<List
|
<List
|
||||||
|
{...listProps}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
renderItem={(item, index) => {
|
renderItem={(item, index) => {
|
||||||
const id = item.id || item
|
const id = item.id || item
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Failed to save translation history"
|
"save": "Failed to save translation history"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Search translation history"
|
||||||
|
},
|
||||||
"title": "Translation History"
|
"title": "Translation History"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "保存翻訳履歴に失敗しました"
|
"save": "保存翻訳履歴に失敗しました"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "翻訳履歴を検索する"
|
||||||
|
},
|
||||||
"title": "翻訳履歴"
|
"title": "翻訳履歴"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Не удалось сохранить историю переводов"
|
"save": "Не удалось сохранить историю переводов"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Поиск истории переводов"
|
||||||
|
},
|
||||||
"title": "История переводов"
|
"title": "История переводов"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "保存翻译历史失败"
|
"save": "保存翻译历史失败"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜索翻译历史"
|
||||||
|
},
|
||||||
"title": "翻译历史"
|
"title": "翻译历史"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "保存翻譯歷史失敗"
|
"save": "保存翻譯歷史失敗"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "搜索翻譯歷史"
|
||||||
|
},
|
||||||
"title": "翻譯歷史"
|
"title": "翻譯歷史"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων"
|
"save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Αναζήτηση ιστορικού μεταφράσεων"
|
||||||
|
},
|
||||||
"title": "Ιστορικό μετάφρασης"
|
"title": "Ιστορικό μετάφρασης"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Error al guardar el historial de traducciones"
|
"save": "Error al guardar el historial de traducciones"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Historial de búsqueda de traducción"
|
||||||
|
},
|
||||||
"title": "Historial de traducciones"
|
"title": "Historial de traducciones"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Échec de la sauvegarde de l'historique des traductions"
|
"save": "Échec de la sauvegarde de l'historique des traductions"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Rechercher l'historique des traductions"
|
||||||
|
},
|
||||||
"title": "Historique des traductions"
|
"title": "Historique des traductions"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -3723,6 +3723,9 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"save": "Falha ao guardar o histórico de traduções"
|
"save": "Falha ao guardar o histórico de traduções"
|
||||||
},
|
},
|
||||||
|
"search": {
|
||||||
|
"placeholder": "Pesquisar histórico de tradução"
|
||||||
|
},
|
||||||
"title": "Histórico de Tradução"
|
"title": "Histórico de Tradução"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
|
|||||||
@@ -11,22 +11,19 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setIsBunInstalled } from '@renderer/store/mcp'
|
import { setIsBunInstalled } from '@renderer/store/mcp'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { codeTools } from '@shared/config/constant'
|
|
||||||
import { Alert, Button, Checkbox, Input, Select, Space } from 'antd'
|
import { Alert, Button, Checkbox, Input, Select, Space } from 'antd'
|
||||||
import { Download, Terminal, X } from 'lucide-react'
|
import { Download, Terminal, X } from 'lucide-react'
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
// CLI 工具选项
|
import {
|
||||||
const CLI_TOOLS = [
|
CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS,
|
||||||
{ value: codeTools.qwenCode, label: 'Qwen Code' },
|
CLI_TOOL_PROVIDER_MAP,
|
||||||
{ value: codeTools.claudeCode, label: 'Claude Code' },
|
CLI_TOOLS,
|
||||||
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
|
generateToolEnvironment,
|
||||||
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
|
parseEnvironmentVariables
|
||||||
]
|
} from '.'
|
||||||
|
|
||||||
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
|
||||||
|
|
||||||
const logger = loggerService.withContext('CodeToolsPage')
|
const logger = loggerService.withContext('CodeToolsPage')
|
||||||
|
|
||||||
@@ -51,25 +48,17 @@ const CodeToolsPage: FC = () => {
|
|||||||
} = useCodeTools()
|
} = useCodeTools()
|
||||||
const { setTimeoutTimer } = useTimer()
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
|
||||||
// 状态管理
|
|
||||||
const [isLaunching, setIsLaunching] = useState(false)
|
const [isLaunching, setIsLaunching] = useState(false)
|
||||||
const [isInstallingBun, setIsInstallingBun] = useState(false)
|
const [isInstallingBun, setIsInstallingBun] = useState(false)
|
||||||
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
|
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
|
||||||
|
|
||||||
const handleCliToolChange = (value: codeTools) => setCliTool(value)
|
|
||||||
|
|
||||||
const openAiCompatibleProviders = providers.filter((p) => p.type.includes('openai'))
|
|
||||||
const openAiProviders = providers.filter((p) => p.id === 'openai')
|
|
||||||
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
|
|
||||||
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
|
|
||||||
|
|
||||||
const modelPredicate = useCallback(
|
const modelPredicate = useCallback(
|
||||||
(m: Model) => {
|
(m: Model) => {
|
||||||
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (selectedCliTool === 'claude-code') {
|
if (selectedCliTool === 'claude-code') {
|
||||||
return m.id.includes('claude')
|
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
|
||||||
}
|
}
|
||||||
if (selectedCliTool === 'gemini-cli') {
|
if (selectedCliTool === 'gemini-cli') {
|
||||||
return m.id.includes('gemini')
|
return m.id.includes('gemini')
|
||||||
@@ -80,20 +69,9 @@ const CodeToolsPage: FC = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const availableProviders = useMemo(() => {
|
const availableProviders = useMemo(() => {
|
||||||
if (selectedCliTool === codeTools.claudeCode) {
|
const filterFn = CLI_TOOL_PROVIDER_MAP[selectedCliTool]
|
||||||
return claudeProviders
|
return filterFn ? filterFn(providers) : []
|
||||||
}
|
}, [providers, selectedCliTool])
|
||||||
if (selectedCliTool === codeTools.geminiCli) {
|
|
||||||
return geminiProviders
|
|
||||||
}
|
|
||||||
if (selectedCliTool === codeTools.qwenCode) {
|
|
||||||
return openAiCompatibleProviders
|
|
||||||
}
|
|
||||||
if (selectedCliTool === codeTools.openaiCodex) {
|
|
||||||
return openAiProviders
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}, [claudeProviders, geminiProviders, openAiCompatibleProviders, openAiProviders, selectedCliTool])
|
|
||||||
|
|
||||||
const handleModelChange = (value: string) => {
|
const handleModelChange = (value: string) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -111,25 +89,6 @@ const CodeToolsPage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理环境变量更改
|
|
||||||
const handleEnvVarsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setEnvVars(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理文件夹选择
|
|
||||||
const handleFolderSelect = async () => {
|
|
||||||
try {
|
|
||||||
await selectFolder()
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('选择文件夹失败:', error as Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理目录选择
|
|
||||||
const handleDirectoryChange = (value: string) => {
|
|
||||||
setCurrentDir(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理删除目录
|
// 处理删除目录
|
||||||
const handleRemoveDirectory = (directory: string, e: React.MouseEvent) => {
|
const handleRemoveDirectory = (directory: string, e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -170,104 +129,74 @@ const CodeToolsPage: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理启动
|
// 验证启动条件
|
||||||
const handleLaunch = async () => {
|
const validateLaunch = (): { isValid: boolean; message?: string } => {
|
||||||
if (!canLaunch || !isBunInstalled) {
|
if (!canLaunch || !isBunInstalled) {
|
||||||
if (!isBunInstalled) {
|
return {
|
||||||
window.message.warning({
|
isValid: false,
|
||||||
content: t('code.launch.bun_required'),
|
message: !isBunInstalled ? t('code.launch.bun_required') : t('code.launch.validation_error')
|
||||||
key: 'code-launch-message'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
window.message.warning({
|
|
||||||
content: t('code.launch.validation_error'),
|
|
||||||
key: 'code-launch-message'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLaunching(true)
|
|
||||||
|
|
||||||
if (!selectedModel) {
|
if (!selectedModel) {
|
||||||
window.message.error({
|
return { isValid: false, message: t('code.model_required') }
|
||||||
content: t('code.model_required'),
|
|
||||||
key: 'code-launch-message'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { isValid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备启动环境
|
||||||
|
const prepareLaunchEnvironment = async (): Promise<Record<string, string> | null> => {
|
||||||
|
if (!selectedModel) return null
|
||||||
|
|
||||||
const modelProvider = getProviderByModel(selectedModel)
|
const modelProvider = getProviderByModel(selectedModel)
|
||||||
const aiProvider = new AiProvider(modelProvider)
|
const aiProvider = new AiProvider(modelProvider)
|
||||||
const baseUrl = await aiProvider.getBaseURL()
|
const baseUrl = await aiProvider.getBaseURL()
|
||||||
const apiKey = await aiProvider.getApiKey()
|
const apiKey = await aiProvider.getApiKey()
|
||||||
|
|
||||||
let env: Record<string, string> = {}
|
// 生成工具特定的环境变量
|
||||||
if (selectedCliTool === codeTools.claudeCode) {
|
const toolEnv = generateToolEnvironment({
|
||||||
env = {
|
tool: selectedCliTool,
|
||||||
ANTHROPIC_API_KEY: apiKey,
|
model: selectedModel,
|
||||||
ANTHROPIC_BASE_URL: modelProvider.apiHost,
|
modelProvider,
|
||||||
ANTHROPIC_MODEL: selectedModel.id
|
apiKey,
|
||||||
}
|
baseUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
// 合并用户自定义的环境变量
|
||||||
|
const userEnv = parseEnvironmentVariables(environmentVariables)
|
||||||
|
|
||||||
|
return { ...toolEnv, ...userEnv }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行启动操作
|
||||||
|
const executeLaunch = async (env: Record<string, string>) => {
|
||||||
|
window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { autoUpdateToLatest })
|
||||||
|
window.message.success({ content: t('code.launch.success'), key: 'code-launch-message' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理启动
|
||||||
|
const handleLaunch = async () => {
|
||||||
|
const validation = validateLaunch()
|
||||||
|
|
||||||
|
if (!validation.isValid) {
|
||||||
|
window.message.warning({ content: validation.message, key: 'code-launch-message' })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCliTool === codeTools.geminiCli) {
|
setIsLaunching(true)
|
||||||
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
|
|
||||||
const apiBaseUrl = modelProvider.apiHost + apiSuffix
|
|
||||||
env = {
|
|
||||||
GEMINI_API_KEY: apiKey,
|
|
||||||
GEMINI_BASE_URL: apiBaseUrl,
|
|
||||||
GOOGLE_GEMINI_BASE_URL: apiBaseUrl,
|
|
||||||
GEMINI_MODEL: selectedModel.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.openaiCodex) {
|
|
||||||
env = {
|
|
||||||
OPENAI_API_KEY: apiKey,
|
|
||||||
OPENAI_BASE_URL: baseUrl,
|
|
||||||
OPENAI_MODEL: selectedModel.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析用户自定义的环境变量
|
|
||||||
if (environmentVariables) {
|
|
||||||
const lines = environmentVariables.split('\n')
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim()
|
|
||||||
if (trimmedLine && trimmedLine.includes('=')) {
|
|
||||||
const [key, ...valueParts] = trimmedLine.split('=')
|
|
||||||
const trimmedKey = key.trim()
|
|
||||||
const value = valueParts.join('=').trim()
|
|
||||||
if (trimmedKey) {
|
|
||||||
env[trimmedKey] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 这里可以添加实际的启动逻辑
|
const env = await prepareLaunchEnvironment()
|
||||||
logger.info('启动配置:', {
|
if (!env) {
|
||||||
cliTool: selectedCliTool,
|
window.message.error({ content: t('code.model_required'), key: 'code-launch-message' })
|
||||||
model: selectedModel,
|
return
|
||||||
folder: currentDirectory
|
}
|
||||||
})
|
|
||||||
|
|
||||||
window.api.codeTools.run(selectedCliTool, selectedModel?.id, currentDirectory, env, {
|
await executeLaunch(env)
|
||||||
autoUpdateToLatest
|
|
||||||
})
|
|
||||||
|
|
||||||
window.message.success({
|
|
||||||
content: t('code.launch.success'),
|
|
||||||
key: 'code-launch-message'
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('启动失败:', error as Error)
|
logger.error('启动失败:', error as Error)
|
||||||
window.message.error({
|
window.message.error({ content: t('code.launch.error'), key: 'code-launch-message' })
|
||||||
content: t('code.launch.error'),
|
|
||||||
key: 'code-launch-message'
|
|
||||||
})
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLaunching(false)
|
setIsLaunching(false)
|
||||||
}
|
}
|
||||||
@@ -320,7 +249,7 @@ const CodeToolsPage: FC = () => {
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder={t('code.cli_tool_placeholder')}
|
placeholder={t('code.cli_tool_placeholder')}
|
||||||
value={selectedCliTool}
|
value={selectedCliTool}
|
||||||
onChange={handleCliToolChange}
|
onChange={setCliTool}
|
||||||
options={CLI_TOOLS}
|
options={CLI_TOOLS}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
@@ -345,7 +274,7 @@ const CodeToolsPage: FC = () => {
|
|||||||
style={{ flex: 1, width: 480 }}
|
style={{ flex: 1, width: 480 }}
|
||||||
placeholder={t('code.folder_placeholder')}
|
placeholder={t('code.folder_placeholder')}
|
||||||
value={currentDirectory || undefined}
|
value={currentDirectory || undefined}
|
||||||
onChange={handleDirectoryChange}
|
onChange={setCurrentDir}
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
filterOption={(input, option) => {
|
filterOption={(input, option) => {
|
||||||
@@ -366,7 +295,7 @@ const CodeToolsPage: FC = () => {
|
|||||||
)
|
)
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
|
<Button onClick={selectFolder} style={{ width: 120 }}>
|
||||||
{t('code.select_folder')}
|
{t('code.select_folder')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -377,7 +306,7 @@ const CodeToolsPage: FC = () => {
|
|||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder={`KEY1=value1\nKEY2=value2`}
|
placeholder={`KEY1=value1\nKEY2=value2`}
|
||||||
value={environmentVariables}
|
value={environmentVariables}
|
||||||
onChange={handleEnvVarsChange}
|
onChange={(e) => setEnvVars(e.target.value)}
|
||||||
rows={2}
|
rows={2}
|
||||||
style={{ fontFamily: 'monospace' }}
|
style={{ fontFamily: 'monospace' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1 +1,135 @@
|
|||||||
|
import { EndpointType, Model, Provider } from '@renderer/types'
|
||||||
|
import { codeTools } from '@shared/config/constant'
|
||||||
|
|
||||||
|
export interface LaunchValidationResult {
|
||||||
|
isValid: boolean
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolEnvironmentConfig {
|
||||||
|
tool: codeTools
|
||||||
|
model: Model
|
||||||
|
modelProvider: Provider
|
||||||
|
apiKey: string
|
||||||
|
baseUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI 工具选项
|
||||||
|
export const CLI_TOOLS = [
|
||||||
|
{ value: codeTools.qwenCode, label: 'Qwen Code' },
|
||||||
|
{ value: codeTools.claudeCode, label: 'Claude Code' },
|
||||||
|
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
|
||||||
|
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
|
||||||
|
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu']
|
||||||
|
export const CLAUDE_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', ...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS]
|
||||||
|
|
||||||
|
// Provider 过滤映射
|
||||||
|
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
|
||||||
|
[codeTools.claudeCode]: (providers) =>
|
||||||
|
providers.filter((p) => p.type === 'anthropic' || CLAUDE_SUPPORTED_PROVIDERS.includes(p.id)),
|
||||||
|
[codeTools.geminiCli]: (providers) =>
|
||||||
|
providers.filter((p) => p.type === 'gemini' || GEMINI_SUPPORTED_PROVIDERS.includes(p.id)),
|
||||||
|
[codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')),
|
||||||
|
[codeTools.openaiCodex]: (providers) => providers.filter((p) => p.id === 'openai')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
|
||||||
|
const CODE_TOOLS_API_ENDPOINTS = {
|
||||||
|
aihubmix: {
|
||||||
|
gemini: {
|
||||||
|
api_base_url: 'https://api.aihubmix.com/gemini'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deepseek: {
|
||||||
|
anthropic: {
|
||||||
|
api_base_url: 'https://api.deepseek.com/anthropic'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moonshot: {
|
||||||
|
anthropic: {
|
||||||
|
api_base_url: 'https://api.moonshot.cn/anthropic'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zhipu: {
|
||||||
|
anthropic: {
|
||||||
|
api_base_url: 'https://open.bigmodel.cn/api/anthropic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = model.provider
|
||||||
|
|
||||||
|
return CODE_TOOLS_API_ENDPOINTS[provider]?.[type]?.api_base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析环境变量字符串为对象
|
||||||
|
export const parseEnvironmentVariables = (envVars: string): Record<string, string> => {
|
||||||
|
const env: Record<string, string> = {}
|
||||||
|
if (!envVars) return env
|
||||||
|
|
||||||
|
const lines = envVars.split('\n')
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim()
|
||||||
|
if (trimmedLine && trimmedLine.includes('=')) {
|
||||||
|
const [key, ...valueParts] = trimmedLine.split('=')
|
||||||
|
const trimmedKey = key.trim()
|
||||||
|
const value = valueParts.join('=').trim()
|
||||||
|
if (trimmedKey) {
|
||||||
|
env[trimmedKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为不同 CLI 工具生成环境变量配置
|
||||||
|
export const generateToolEnvironment = ({
|
||||||
|
tool,
|
||||||
|
model,
|
||||||
|
modelProvider,
|
||||||
|
apiKey,
|
||||||
|
baseUrl
|
||||||
|
}: {
|
||||||
|
tool: codeTools
|
||||||
|
model: Model
|
||||||
|
modelProvider: Provider
|
||||||
|
apiKey: string
|
||||||
|
baseUrl: string
|
||||||
|
}): Record<string, string> => {
|
||||||
|
const env: Record<string, string> = {}
|
||||||
|
|
||||||
|
switch (tool) {
|
||||||
|
case codeTools.claudeCode:
|
||||||
|
env.ANTHROPIC_BASE_URL = getCodeToolsApiBaseUrl(model, 'anthropic') || modelProvider.apiHost
|
||||||
|
env.ANTHROPIC_MODEL = model.id
|
||||||
|
if (modelProvider.type === 'anthropic') {
|
||||||
|
env.ANTHROPIC_API_KEY = apiKey
|
||||||
|
} else {
|
||||||
|
env.ANTHROPIC_AUTH_TOKEN = apiKey
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case codeTools.geminiCli: {
|
||||||
|
const apiBaseUrl = getCodeToolsApiBaseUrl(model, 'gemini') || modelProvider.apiHost
|
||||||
|
env.GEMINI_API_KEY = apiKey
|
||||||
|
env.GEMINI_BASE_URL = apiBaseUrl
|
||||||
|
env.GOOGLE_GEMINI_BASE_URL = apiBaseUrl
|
||||||
|
env.GEMINI_MODEL = model.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case codeTools.qwenCode:
|
||||||
|
case codeTools.openaiCodex:
|
||||||
|
env.OPENAI_API_KEY = apiKey
|
||||||
|
env.OPENAI_BASE_URL = baseUrl
|
||||||
|
env.OPENAI_MODEL = model.id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
export { default } from './CodeToolsPage'
|
export { default } from './CodeToolsPage'
|
||||||
|
|||||||
@@ -210,7 +210,21 @@ const McpServersList: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<DraggableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
|
<DraggableList
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
list={mcpServers}
|
||||||
|
onUpdate={updateMcpServers}
|
||||||
|
listProps={{
|
||||||
|
locale: {
|
||||||
|
emptyText: (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={t('settings.mcp.noServers')}
|
||||||
|
style={{ marginTop: 20 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
{(server: MCPServer) => (
|
{(server: MCPServer) => (
|
||||||
<div onClick={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}>
|
<div onClick={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}>
|
||||||
<McpServerCard
|
<McpServerCard
|
||||||
@@ -225,13 +239,6 @@ const McpServersList: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DraggableList>
|
</DraggableList>
|
||||||
{mcpServers.length === 0 && (
|
|
||||||
<Empty
|
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
description={t('settings.mcp.noServers')}
|
|
||||||
style={{ marginTop: 20 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<McpMarketList />
|
<McpMarketList />
|
||||||
<BuiltinMCPServerList />
|
<BuiltinMCPServerList />
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
import { DeleteOutlined } from '@ant-design/icons'
|
import { DeleteOutlined } from '@ant-design/icons'
|
||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import useTranslate from '@renderer/hooks/useTranslate'
|
import useTranslate from '@renderer/hooks/useTranslate'
|
||||||
import { clearHistory, deleteHistory } from '@renderer/services/TranslateService'
|
import { clearHistory, deleteHistory } from '@renderer/services/TranslateService'
|
||||||
import { TranslateHistory, TranslateLanguage } from '@renderer/types'
|
import { TranslateHistory, TranslateLanguage } from '@renderer/types'
|
||||||
import { Button, Drawer, Dropdown, Empty, Flex, Popconfirm } from 'antd'
|
import { Button, Drawer, Dropdown, Empty, Flex, Input, Popconfirm } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useMemo } from 'react'
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
type DisplayedTranslateHistory = TranslateHistory & {
|
type DisplayedTranslateHistoryItem = TranslateHistory & {
|
||||||
_sourceLanguage: TranslateLanguage
|
_sourceLanguage: TranslateLanguage
|
||||||
_targetLanguage: TranslateLanguage
|
_targetLanguage: TranslateLanguage
|
||||||
}
|
}
|
||||||
|
|
||||||
type TranslateHistoryProps = {
|
type TranslateHistoryProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onHistoryItemClick: (history: DisplayedTranslateHistory) => void
|
onHistoryItemClick: (history: DisplayedTranslateHistoryItem) => void
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const logger = loggerService.withContext('TranslateHistory')
|
||||||
|
|
||||||
// px
|
// px
|
||||||
const ITEM_HEIGHT = 140
|
const ITEM_HEIGHT = 140
|
||||||
|
|
||||||
@@ -30,17 +34,35 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { getLanguageByLangcode } = useTranslate()
|
const { getLanguageByLangcode } = useTranslate()
|
||||||
const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [displayedHistory, setDisplayedHistory] = useState<DisplayedTranslateHistoryItem[]>([])
|
||||||
|
|
||||||
const translateHistory: DisplayedTranslateHistory[] = useMemo(() => {
|
const translateHistory: DisplayedTranslateHistoryItem[] = useMemo(() => {
|
||||||
if (!_translateHistory) return []
|
if (!_translateHistory) return []
|
||||||
|
|
||||||
return _translateHistory.map((item) => ({
|
return _translateHistory.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
_sourceLanguage: getLanguageByLangcode(item.sourceLanguage),
|
_sourceLanguage: getLanguageByLangcode(item.sourceLanguage),
|
||||||
_targetLanguage: getLanguageByLangcode(item.targetLanguage)
|
_targetLanguage: getLanguageByLangcode(item.targetLanguage),
|
||||||
|
createdAt: dayjs(item.createdAt).format('MM/DD HH:mm')
|
||||||
}))
|
}))
|
||||||
}, [_translateHistory, getLanguageByLangcode])
|
}, [_translateHistory, getLanguageByLangcode])
|
||||||
|
|
||||||
|
const searchFilter = useCallback(
|
||||||
|
(item: DisplayedTranslateHistoryItem) => {
|
||||||
|
if (isEmpty(search)) return true
|
||||||
|
const content = `${item._sourceLanguage.label()} ${item._targetLanguage.label()} ${item.sourceText} ${item.targetText} ${item.createdAt}`
|
||||||
|
return content.includes(search)
|
||||||
|
},
|
||||||
|
[search]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisplayedHistory(translateHistory.filter(searchFilter))
|
||||||
|
}, [searchFilter, translateHistory])
|
||||||
|
|
||||||
|
const deferredHistory = useDeferredValue(displayedHistory)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={t('translate.history.title')}
|
title={t('translate.history.title')}
|
||||||
@@ -71,9 +93,32 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
|
|||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<HistoryContainer>
|
<HistoryContainer>
|
||||||
{translateHistory && translateHistory.length ? (
|
{/* Search Bar */}
|
||||||
|
<HStack style={{ padding: '0 12px', borderBottom: '1px solid var(--ant-color-split)' }}>
|
||||||
|
<Input
|
||||||
|
prefix={
|
||||||
|
<IconWrapper>
|
||||||
|
<SearchIcon size={18} />
|
||||||
|
</IconWrapper>
|
||||||
|
}
|
||||||
|
placeholder={t('translate.history.search.placeholder')}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value)
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
autoFocus
|
||||||
|
spellCheck={false}
|
||||||
|
style={{ paddingLeft: 0, height: '3em' }}
|
||||||
|
variant="borderless"
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* Virtual List */}
|
||||||
|
{deferredHistory.length > 0 ? (
|
||||||
<HistoryList>
|
<HistoryList>
|
||||||
<DynamicVirtualList list={translateHistory} estimateSize={() => ITEM_HEIGHT}>
|
<DynamicVirtualList list={deferredHistory} estimateSize={() => ITEM_HEIGHT}>
|
||||||
{(item) => {
|
{(item) => {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -98,7 +143,7 @@ const TranslateHistoryList: FC<TranslateHistoryProps> = ({ isOpen, onHistoryItem
|
|||||||
<HistoryListItemLanguage>{item._sourceLanguage.label()} →</HistoryListItemLanguage>
|
<HistoryListItemLanguage>{item._sourceLanguage.label()} →</HistoryListItemLanguage>
|
||||||
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
|
<HistoryListItemLanguage>{item._targetLanguage.label()}</HistoryListItemLanguage>
|
||||||
</Flex>
|
</Flex>
|
||||||
<HistoryListItemDate>{dayjs(item.createdAt).format('MM/DD HH:mm')}</HistoryListItemDate>
|
<HistoryListItemDate>{item.createdAt}</HistoryListItemDate>
|
||||||
</Flex>
|
</Flex>
|
||||||
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
|
<HistoryListItemTitle>{item.sourceText}</HistoryListItemTitle>
|
||||||
<HistoryListItemTitle style={{ color: 'var(--color-text-2)' }}>
|
<HistoryListItemTitle style={{ color: 'var(--color-text-2)' }}>
|
||||||
@@ -192,4 +237,14 @@ const HistoryListItemLanguage = styled.div`
|
|||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const IconWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
`
|
||||||
|
|
||||||
export default TranslateHistoryList
|
export default TranslateHistoryList
|
||||||
|
|||||||
@@ -122,8 +122,6 @@ const TranslatePage: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTranslating(true)
|
|
||||||
|
|
||||||
let translated: string
|
let translated: string
|
||||||
try {
|
try {
|
||||||
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100))
|
translated = await translateText(text, actualTargetLanguage, throttle(setTranslatedContent, 100))
|
||||||
@@ -145,8 +143,6 @@ const TranslatePage: FC = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to translate', e as Error)
|
logger.error('Failed to translate', e as Error)
|
||||||
window.message.error(t('translate.error.unknown') + ': ' + (e as Error).message)
|
window.message.error(t('translate.error.unknown') + ': ' + (e as Error).message)
|
||||||
} finally {
|
|
||||||
setTranslating(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setTranslatedContent, setTranslating, t, translating]
|
[setTranslatedContent, setTranslating, t, translating]
|
||||||
@@ -163,6 +159,8 @@ const TranslatePage: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTranslating(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
||||||
let actualSourceLanguage: TranslateLanguage
|
let actualSourceLanguage: TranslateLanguage
|
||||||
@@ -202,11 +200,14 @@ const TranslatePage: FC = () => {
|
|||||||
key: 'translate-message'
|
key: 'translate-message'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
} finally {
|
||||||
|
setTranslating(false)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
bidirectionalPair,
|
bidirectionalPair,
|
||||||
getLanguageByLangcode,
|
getLanguageByLangcode,
|
||||||
isBidirectional,
|
isBidirectional,
|
||||||
|
setTranslating,
|
||||||
sourceLanguage,
|
sourceLanguage,
|
||||||
t,
|
t,
|
||||||
targetLanguage,
|
targetLanguage,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const detectLanguage = async (inputText: string): Promise<TranslateLangua
|
|||||||
throw new Error('Invalid detection method.')
|
throw new Error('Invalid detection method.')
|
||||||
}
|
}
|
||||||
logger.info(`Detected Language: ${result}`)
|
logger.info(`Detected Language: ${result}`)
|
||||||
return result
|
return result.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguageCode> => {
|
const detectLanguageByLLM = async (inputText: string): Promise<TranslateLanguageCode> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user