Compare commits

..

9 Commits

Author SHA1 Message Date
suyao
92ba7089bb feat: implement default parameter configuration for MCP tools
- Added functionality to apply default parameters to tool arguments in MCPService.
- Introduced UI components for managing tool parameter configurations in MCPSettings.
- Updated translations for new settings and tool configurations across multiple languages.
- Enhanced MCPToolsSection to allow editing and saving of tool parameter defaults.
2025-05-25 03:19:21 +08:00
kangfenmao
bca4fbe7de fix: update MainTextBlock to use a class for markdown styling
- Changed the paragraph element in MainTextBlock to include a "markdown" class for improved styling consistency.
2025-05-24 18:35:57 +08:00
Caelan
2b99d6066f feat: 文字生成图新增提供商DMXAPI (#6352)
dmxapi文字生成图

Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
2025-05-24 16:14:32 +08:00
自由的世界人
9357bd6e0f feat: add disable MCP server functionality and update translations (#6398)
* feat: add disable MCP server functionality and update translations

* feat: update MCPToolsButton and WebSearchButton to use CircleX icon and change labels to 'close'

---------

Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-05-24 16:00:55 +08:00
Pleasurecruise
c4e0744806 fix: improve multi-select functionality in Messages and SelectionBox components 2025-05-24 15:44:50 +08:00
one
1b6cba454d chore: add dependabot (#6369) 2025-05-24 08:58:24 +08:00
SuYao
77cd958d08 fix: floating-sidebar header sticky (#6371) 2025-05-24 08:55:20 +08:00
kangfenmao
d8aac9ecb8 feat: add support for Windows ARM64 architecture in bun installation script
- Included package mappings for 'win32-arm64' and 'win32-arm64-baseline' to the BUN_PACKAGES object in install-bun.js, enhancing compatibility with ARM64 devices on Windows.
2025-05-23 18:53:55 +08:00
kangfenmao
2758321821 chore: disable code signature verification for Windows updates in electron-builder configuration 2025-05-23 17:50:50 +08:00
37 changed files with 1819 additions and 245 deletions

86
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 7
target-branch: "main"
commit-message:
prefix: "chore"
include: "scope"
groups:
# 核心框架
core-framework:
patterns:
- "react"
- "react-dom"
- "electron"
- "typescript"
- "@types/react*"
- "@types/node"
update-types:
- "minor"
- "patch"
# Electron 生态和构建工具
electron-build:
patterns:
- "electron-*"
- "@electron*"
- "vite"
- "@vitejs/*"
- "dotenv-cli"
- "rollup-plugin-*"
- "@swc/*"
update-types:
- "minor"
- "patch"
# 测试工具
testing-tools:
patterns:
- "vitest"
- "@vitest/*"
- "playwright"
- "@playwright/*"
- "eslint*"
- "@eslint*"
- "prettier"
- "husky"
- "lint-staged"
update-types:
- "minor"
- "patch"
# CherryStudio 自定义包
cherrystudio-packages:
patterns:
- "@cherrystudio/*"
update-types:
- "minor"
- "patch"
# 兜底其他 dependencies
other-dependencies:
dependency-type: "production"
# 兜底其他 devDependencies
other-dev-dependencies:
dependency-type: "development"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 3
commit-message:
prefix: "ci"
include: "scope"
groups:
github-actions:
patterns:
- "*"
update-types:
- "minor"
- "patch"

View File

@@ -47,6 +47,7 @@ win:
- target: portable
signtoolOptions:
sign: scripts/win-sign.js
verifyUpdateCodeSignature: false
nsis:
artifactName: ${productName}-${version}-${arch}-setup.${ext}
shortcutName: ${productName}

View File

@@ -15,6 +15,8 @@ const BUN_PACKAGES = {
'darwin-x64': 'bun-darwin-x64.zip',
'win32-x64': 'bun-windows-x64.zip',
'win32-x64-baseline': 'bun-windows-x64-baseline.zip',
'win32-arm64': 'bun-windows-x64.zip',
'win32-arm64-baseline': 'bun-windows-x64-baseline.zip',
'linux-x64': 'bun-linux-x64.zip',
'linux-x64-baseline': 'bun-linux-x64-baseline.zip',
'linux-arm64': 'bun-linux-aarch64.zip',

View File

@@ -386,7 +386,11 @@ class FileStorage {
}
}
public downloadFile = async (_: Electron.IpcMainInvokeEvent, url: string): Promise<FileType> => {
public downloadFile = async (
_: Electron.IpcMainInvokeEvent,
url: string,
isUseContentType?: boolean
): Promise<FileType> => {
try {
const response = await fetch(url)
if (!response.ok) {
@@ -411,7 +415,7 @@ class FileStorage {
}
// 如果文件名没有后缀根据Content-Type添加后缀
if (!filename.includes('.')) {
if (isUseContentType || !filename.includes('.')) {
const contentType = response.headers.get('Content-Type')
const ext = this.getExtensionFromMimeType(contentType)
filename += ext

View File

@@ -450,6 +450,35 @@ class McpService {
return cachedListTools(server)
}
/**
* Apply default parameters to tool arguments
*/
private applyDefaultParameters(
toolName: string,
server: MCPServer,
providedArgs: Record<string, any> = {}
): Record<string, any> {
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
if (!toolConfig) {
return providedArgs
}
const mergedArgs = { ...providedArgs }
toolConfig.parameters.forEach((paramConfig) => {
if (
paramConfig.defaultValue !== undefined &&
paramConfig.defaultValue !== null &&
paramConfig.defaultValue !== ''
) {
mergedArgs[paramConfig.name] = paramConfig.defaultValue
}
})
return mergedArgs
}
/**
* Call a tool on an MCP server
*/
@@ -466,8 +495,13 @@ class McpService {
Logger.error('[MCP] args parse error', args)
}
}
Logger.info('[MCP] Calling with args:', server.name, name, args)
const mergedArgs = this.applyDefaultParameters(name, server, args || {})
Logger.info('[MCP] Calling with merged args:', server.name, name, mergedArgs)
const client = await this.initClient(server)
const result = await client.callTool({ name, arguments: args }, undefined, {
const result = await client.callTool({ name, arguments: mergedArgs }, undefined, {
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
})
return result as MCPCallToolResponse

View File

@@ -74,7 +74,7 @@ const api = {
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
download: (url: string, isUseContentType?: boolean) => ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -60,6 +60,7 @@
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--scrollbar-width: 5px;
--chat-background: #111111;
--chat-background-user: #28b561;

View File

@@ -5,8 +5,6 @@ import { FC, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import styled from 'styled-components'
import Scrollbar from '../Scrollbar'
interface Props {
children: React.ReactNode
activeAssistant: Assistant
@@ -55,7 +53,8 @@ const FloatingSidebar: FC<Props> = ({
forceToSeeAllTab={true}
style={{
background: 'transparent',
border: 'none'
border: 'none',
maxHeight: maxHeight
}}
/>
</PopoverContent>
@@ -81,9 +80,8 @@ const FloatingSidebar: FC<Props> = ({
)
}
const PopoverContent = styled(Scrollbar)<{ maxHeight: number }>`
const PopoverContent = styled.div<{ maxHeight: number }>`
max-height: ${(props) => props.maxHeight}px;
overflow-y: auto;
`
export default FloatingSidebar

View File

@@ -80,7 +80,11 @@ export const useChatContext = (activeTopic: Topic) => {
(messageId: string, selected: boolean) => {
dispatch(
setSelectedMessageIds(
selected ? [...selectedMessageIds, messageId] : selectedMessageIds.filter((id) => id !== messageId)
selected
? selectedMessageIds.includes(messageId)
? selectedMessageIds
: [...selectedMessageIds, messageId]
: selectedMessageIds.filter((id) => id !== messageId)
)
)
},

View File

@@ -9,10 +9,12 @@ export function usePaintings() {
const remix = useAppSelector((state) => state.paintings.remix)
const edit = useAppSelector((state) => state.paintings.edit)
const upscale = useAppSelector((state) => state.paintings.upscale)
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
const dispatch = useAppDispatch()
return {
paintings,
DMXAPIPaintings,
persistentData: {
generate,
remix,

View File

@@ -45,7 +45,10 @@
"search.no_results": "No results found",
"sorting.title": "Sorting",
"settings": {
"title": "Agent Setting"
"title": "Agent Configuration",
"subscription": {
"title": "Agent Subscription Configuration"
}
},
"tag.agent": "Agent",
"tag.default": "Default",
@@ -57,50 +60,50 @@
"title": "Assistants",
"abbr": "Assistants",
"settings.title": "Assistant Settings",
"clear.content": "Clearing the topic will delete all topics and files in the assistant. Are you sure you want to continue?",
"clear.title": "Clear topics",
"clear.content": "Clearing topics will delete all topics and files under the assistant. Are you sure you want to continue?",
"clear.title": "Clear Topics",
"copy.title": "Copy Assistant",
"delete.content": "Deleting an assistant will delete all topics and files under the assistant. Are you sure you want to delete it?",
"delete.content": "Deleting an assistant will delete all topics and files under that assistant. Are you sure you want to continue?",
"delete.title": "Delete Assistant",
"edit.title": "Edit Assistant",
"save.success": "Saved successfully",
"save.title": "Save to agent",
"save.title": "Save to Agent",
"icon.type": "Assistant Icon",
"search": "Search assistants...",
"search": "Search Assistants",
"settings.mcp": "MCP Servers",
"settings.mcp.enableFirst": "Please enable this server in MCP Settings first",
"settings.mcp.title": "MCP Settings",
"settings.mcp.noServersAvailable": "No MCP servers available. Please add a server in settings.",
"settings.mcp.description": "Default enabled MCP servers",
"settings.default_model": "Default Model",
"settings.knowledge_base": "Knowledge Base Settings",
"settings.mcp": "MCP Servers",
"settings.mcp.enableFirst": "Enable this server in MCP settings first",
"settings.mcp.title": "MCP Settings",
"settings.mcp.noServersAvailable": "No MCP servers available. Add servers in settings",
"settings.mcp.description": "Default enabled MCP servers",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Reasoning effort",
"settings.reasoning_effort.off": "Off",
"settings.reasoning_effort.high": "Think harder",
"settings.reasoning_effort.low": "Think less",
"settings.reasoning_effort.medium": "Think normally",
"settings.reasoning_effort.default": "Default",
"settings.more": "Assistant Settings",
"settings.knowledge_base.recognition.tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities",
"settings.knowledge_base.recognition": "Use Knowledge Base",
"settings.knowledge_base.recognition.off": "Force Search",
"settings.knowledge_base.recognition.tip": "The agent will use the model's intent recognition ability to determine if it needs to call the knowledge base for an answer. This feature will depend on the model's capabilities.",
"settings.knowledge_base.recognition": "Call Knowledge Base",
"settings.knowledge_base.recognition.off": "Force Retrieval",
"settings.knowledge_base.recognition.on": "Intent Recognition",
"settings.tool_use_mode": "Tool Use Mode",
"settings.tool_use_mode.function": "Function",
"settings.tool_use_mode.prompt": "Prompt",
"settings.model": "Model Settings",
"settings.preset_messages": "Preset Messages",
"settings.prompt": "Prompt Settings",
"settings.reasoning_effort": "Chain of Thought Length",
"settings.reasoning_effort.off": "Off",
"settings.reasoning_effort.low": "Imagine",
"settings.reasoning_effort.medium": "Consider",
"settings.reasoning_effort.high": "Ponder",
"settings.reasoning_effort.default": "Default",
"settings.more": "Assistant Settings",
"settings.regular_phrases": {
"title": "Regular Phrase",
"title": "Regular Phrases",
"add": "Add Phrase",
"edit": "Edit Phrase",
"delete": "Delete Phrase",
"deleteConfirm": "Are you sure to delete this phrase?",
"deleteConfirm": "Are you sure you want to delete this phrase?",
"titleLabel": "Title",
"titlePlaceholder": "Enter title",
"contentLabel": "Content",
"contentPlaceholder": "Please enter phrase content, support using variables, and press Tab to quickly locate the variable to modify. For example: \nHelp me plan a route from ${from} to ${to}, and send it to ${email}."
"contentPlaceholder": "Enter phrase content, supports variables, then press Tab to quickly navigate to variables for modification. E.g.:\nHelp me plan a route from ${from} to ${to}, then send to ${email}"
}
},
"auth": {
@@ -355,7 +358,7 @@
"add": "Add",
"advanced_settings": "Advanced Settings",
"and": "and",
"assistant": "Assistant",
"assistant": "Agent",
"avatar": "Avatar",
"back": "Back",
"cancel": "Cancel",
@@ -376,9 +379,9 @@
"edit": "Edit",
"expand": "Expand",
"collapse": "Collapse",
"footnote": "Reference content",
"footnotes": "References",
"fullscreen": "Entered fullscreen mode. Press F11 to exit",
"footnote": "Citation",
"footnotes": "Citations",
"fullscreen": "Entered fullscreen mode, press F11 to exit",
"knowledge_base": "Knowledge Base",
"language": "Language",
"loading": "Loading...",
@@ -392,7 +395,10 @@
"regenerate": "Regenerate",
"rename": "Rename",
"reset": "Reset",
"required": "REQUIRED",
"allowed_values": "Allowed values",
"save": "Save",
"unsaved_changes": "You have unsaved changes",
"search": "Search",
"select": "Select",
"selectedMessages": "Selected {{count}} messages",
@@ -813,6 +819,7 @@
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
"seed": "Seed",
"seed_tip": "The same seed and prompt can produce similar images",
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
"title": "Images",
"magic_prompt_option": "Magic Prompt",
"model": "Model Version",
@@ -820,6 +827,7 @@
"style_type": "Style",
"rendering_speed": "Rendering Speed",
"learn_more": "Learn More",
"paint_course": "tutorial",
"prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap",
"proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future",
"image_file_required": "Please upload an image first",
@@ -885,7 +893,8 @@
"number_images_tip": "Number of upscaled results to generate",
"seed_tip": "Controls upscaling randomness",
"magic_prompt_option_tip": "Intelligently enhances upscaling prompts"
}
},
"text_desc_required": "Please enter image description first"
},
"prompts": {
"explanation": "Explain this concept to me",
@@ -1305,6 +1314,8 @@
"stdio": "Standard Input/Output (stdio)",
"inMemory": "Memory",
"config_description": "Configure Model Context Protocol servers",
"disable": "Disable MCP Server",
"disable.description": "Do not enable MCP server functionality",
"deleteError": "Failed to delete server",
"deleteSuccess": "Server deleted successfully",
"dependenciesInstall": "Install Dependencies",
@@ -1368,7 +1379,13 @@
"inputSchema": "Input Schema",
"availableTools": "Available Tools",
"noToolsAvailable": "No tools available",
"loadError": "Get tools Error"
"loadError": "Get tools Error",
"configureDefaults": "Configure Default Parameters",
"configureDefaultsDescription": "Configure default values for tool parameters. When enabled, these values will be automatically used if the AI model doesn't provide them.",
"defaultValue": "Default Value",
"configSaved": "Tool configuration saved successfully",
"hasDefaults": "DEFAULTS",
"hasDefaultTooltip": "This tool has configured default parameters."
},
"prompts": {
"availablePrompts": "Available Prompts",
@@ -1565,6 +1582,9 @@
"rate_limit": "Rate limiting",
"tooltip": "You need to log in to Github before using Github Copilot"
},
"dmxapi": {
"select_platform": "Select the platform"
},
"delete.content": "Are you sure you want to delete this provider?",
"delete.title": "Delete Provider",
"docs_check": "Check",

View File

@@ -50,7 +50,10 @@
"tag.system": "システム",
"title": "エージェント",
"settings": {
"title": "エージェント設定"
"title": "エージェント設定",
"subscription": {
"title": "エージェントサブスクリプション設定"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート"
},
"no_results": "検索結果なし"
"no_results": "検索結果なし",
"required": "必須",
"allowed_values": "許可された値",
"unsaved_changes": "未保存の変更があります"
},
"docs": {
"title": "ドキュメント"
@@ -599,9 +605,8 @@
"delete.confirm.content": "選択した{{count}}件のメッセージを削除しますか?",
"delete.failed": "削除に失敗しました",
"delete.success": "削除が成功しました",
"error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません",
"empty_url": "画像をダウンロードできません。プロンプトに不適切なコンテンツや禁止用語が含まれている可能性があります",
"error.chunk_overlap_too_large": "チャンクのオーバーラップがチャンクサイズより大きくなることはできません",
"empty_url": "画像をダウンロードできません。プロンプトに不適切なコンテンツや禁止用語が含まれている可能性があります",
"error.dimension_too_large": "内容のサイズが大きすぎます",
"error.enter.api.host": "APIホストを入力してください",
"error.enter.api.key": "APIキーを入力してください",
@@ -814,6 +819,7 @@
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
"title": "画像",
"magic_prompt_option": "プロンプト強化",
"model": "モデルバージョン",
@@ -821,6 +827,7 @@
"style_type": "スタイル",
"learn_more": "詳しくはこちら",
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
"paint_course": "チュートリアル",
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
"image_file_required": "画像を先にアップロードしてください",
"image_file_retry": "画像を先にアップロードしてください",
@@ -886,7 +893,8 @@
"magic_prompt_option_tip": "拡大効果を向上させるための提示詞を最適化します"
},
"rendering_speed": "レンダリング速度",
"translating": "翻訳中..."
"translating": "翻訳中...",
"text_desc_required": "画像の説明を先に入力してください"
},
"prompts": {
"explanation": "この概念を説明してください",
@@ -1302,6 +1310,8 @@
"stdio": "標準入力/出力 (stdio)",
"inMemory": "メモリ",
"config_description": "モデルコンテキストプロトコルサーバーの設定",
"disable": "MCPサーバーを無効にする",
"disable.description": "MCP機能を有効にしない",
"deleteError": "サーバーの削除に失敗しました",
"deleteSuccess": "サーバーが正常に削除されました",
"dependenciesInstall": "依存関係をインストール",
@@ -1365,7 +1375,13 @@
"inputSchema": "入力スキーマ",
"availableTools": "利用可能なツール",
"noToolsAvailable": "利用可能なツールなし",
"loadError": "ツール取得エラー"
"loadError": "ツール取得エラー",
"configureDefaults": "デフォルトパラメーターを設定",
"configureDefaultsDescription": "ツールパラメーターのデフォルト値を設定します。有効にすると、AIモデルが提供しない場合、これらの値が自動的に使用されます。",
"defaultValue": "デフォルト値",
"configSaved": "ツール設定が正常に保存されました",
"hasDefaults": "デフォルトあり",
"hasDefaultTooltip": "このツールには、設定済みのデフォルトパラメーターがあります。"
},
"prompts": {
"availablePrompts": "利用可能なプロンプト",
@@ -1553,6 +1569,9 @@
"rate_limit": "レート制限",
"tooltip": "Github Copilot を使用するには、まず Github にログインする必要があります。"
},
"dmxapi": {
"select_platform": "プラットフォームを選択"
},
"delete.content": "このプロバイダーを削除してもよろしいですか?",
"delete.title": "プロバイダーを削除",
"docs_check": "チェック",

View File

@@ -50,7 +50,10 @@
"agent": "Экспорт агента"
},
"settings": {
"title": "Настройки агента"
"title": "Настройки агента",
"subscription": {
"title": "Конфигурация подписки агента"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
},
"no_results": "Результатов не найдено"
"no_results": "Результатов не найдено",
"required": "Обязательно",
"allowed_values": "Допустимые значения",
"unsaved_changes": "У вас есть несохраненные изменения"
},
"docs": {
"title": "Документация"
@@ -813,6 +819,7 @@
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
"seed": "Ключ генерации",
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
"title": "Изображения",
"magic_prompt_option": "Улучшение промпта",
"model": "Версия",
@@ -821,6 +828,7 @@
"rendering_speed": "Скорость рендеринга",
"learn_more": "Узнать больше",
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"paint_course": "Руководство / Учебник",
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
"image_file_required": "Пожалуйста, сначала загрузите изображение",
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
@@ -886,7 +894,7 @@
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
},
"rendering_speed": "Скорость рендеринга"
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
},
"prompts": {
"explanation": "Объясните мне этот концепт",
@@ -1302,6 +1310,8 @@
"stdio": "Стандартный ввод/вывод (stdio)",
"inMemory": "Память",
"config_description": "Настройка серверов протокола контекста модели",
"disable": "Отключить сервер MCP",
"disable.description": "Не включать функциональность сервера MCP",
"deleteError": "Не удалось удалить сервер",
"deleteSuccess": "Сервер успешно удален",
"dependenciesInstall": "Установить зависимости",
@@ -1365,7 +1375,13 @@
"inputSchema": "Схема ввода",
"availableTools": "Доступные инструменты",
"noToolsAvailable": "Нет доступных инструментов",
"loadError": "Ошибка получения инструментов"
"loadError": "Ошибка получения инструментов",
"configureDefaults": "Настроить параметры по умолчанию",
"configureDefaultsDescription": "Настройте значения по умолчанию для параметров инструмента. Когда эта функция включена, эти значения будут использоваться автоматически, если их не предоставит AI модель.",
"defaultValue": "Значение по умолчанию",
"configSaved": "Конфигурация инструмента успешно сохранена",
"hasDefaults": "ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ",
"hasDefaultTooltip": "Для этого инструмента настроены параметры по умолчанию."
},
"prompts": {
"availablePrompts": "Доступные подсказки",
@@ -1553,6 +1569,9 @@
"rate_limit": "Ограничение скорости",
"tooltip": "Для использования Github Copilot необходимо сначала войти в Github."
},
"dmxapi": {
"select_platform": "Выберите платформу"
},
"delete.content": "Вы уверены, что хотите удалить этот провайдер?",
"delete.title": "Удалить провайдер",
"docs_check": "Проверить",

View File

@@ -50,7 +50,10 @@
"tag.system": "系统",
"title": "智能体",
"settings": {
"title": "智能体配置"
"title": "智能体配置",
"subscription": {
"title": "智能体订阅配置"
}
}
},
"assistants": {
@@ -392,7 +395,10 @@
"regenerate": "重新生成",
"rename": "重命名",
"reset": "重置",
"required": "必填",
"allowed_values": "允许的值",
"save": "保存",
"unsaved_changes": "您有未保存的更改",
"search": "搜索",
"select": "选择",
"selectedMessages": "选中 {{count}} 条消息",
@@ -813,6 +819,7 @@
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
"seed": "随机种子",
"seed_tip": "相同的种子和提示词可以生成相似的图片",
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
"title": "图片",
"magic_prompt_option": "提示词增强",
"model": "版本",
@@ -820,6 +827,7 @@
"style_type": "风格",
"rendering_speed": "渲染速度",
"learn_more": "了解更多",
"paint_course": "教程",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
"image_file_required": "请先上传图片",
@@ -885,7 +893,8 @@
"number_images_tip": "生成的放大结果数量",
"seed_tip": "控制放大结果的随机性",
"magic_prompt_option_tip": "智能优化放大提示词"
}
},
"text_desc_required": "请先输入图片描述"
},
"prompts": {
"explanation": "帮我解释一下这个概念",
@@ -1305,6 +1314,8 @@
"stdio": "标准输入/输出 (stdio)",
"inMemory": "内存",
"config_description": "配置模型上下文协议服务器",
"disable": "不使用 MCP 服务器",
"disable.description": "不启用 MCP 服务功能",
"deleteError": "删除服务器失败",
"deleteSuccess": "服务器删除成功",
"dependenciesInstall": "安装依赖项",
@@ -1368,7 +1379,13 @@
"inputSchema": "输入模式",
"availableTools": "可用工具",
"noToolsAvailable": "无可用工具",
"loadError": "获取工具失败"
"loadError": "获取工具失败",
"configureDefaults": "配置默认参数",
"configureDefaultsDescription": "配置工具参数的默认值。启用后如果AI模型没有提供这些参数将自动使用这些默认值。",
"defaultValue": "默认值",
"configSaved": "工具配置保存成功",
"hasDefaults": "有默认值",
"hasDefaultTooltip": "此工具已配置默认参数。"
},
"prompts": {
"availablePrompts": "可用提示",
@@ -1565,6 +1582,9 @@
"rate_limit": "速率限制",
"tooltip": "使用 Github Copilot 需要先登录 Github"
},
"dmxapi": {
"select_platform": "选择平台"
},
"delete.content": "确定要删除此模型提供商吗?",
"delete.title": "删除提供商",
"docs_check": "查看",

View File

@@ -50,7 +50,10 @@
"tag.system": "系統",
"title": "智慧代理人",
"settings": {
"title": "智慧代理人設定"
"title": "智慧代理人設定",
"subscription": {
"title": "智慧代理人訂閱設定"
}
}
},
"assistants": {
@@ -406,7 +409,10 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "沒有結果"
"no_results": "沒有結果",
"required": "必填",
"allowed_values": "允許的值",
"unsaved_changes": "您有未儲存的變更"
},
"docs": {
"title": "說明文件"
@@ -594,12 +600,11 @@
"citations": "引用內容",
"copied": "已複製!",
"copy.failed": "複製失敗",
"copy.success": "複製",
"copy.success": "複製成功",
"delete.confirm.title": "刪除確認",
"delete.confirm.content": "確認刪除選中的 {{count}} 條訊息嗎?",
"delete.failed": "刪除失敗",
"delete.success": "刪除成功",
"copy.success": "複製成功",
"empty_url": "無法下載圖片,可能是提示詞包含敏感內容或違禁詞彙",
"error.chunk_overlap_too_large": "分段重疊不能大於分段大小",
"error.dimension_too_large": "內容尺寸過大",
@@ -814,6 +819,7 @@
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
"seed": "隨機種子",
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
"title": "繪圖",
"magic_prompt_option": "提示詞增強",
"model": "版本",
@@ -821,6 +827,7 @@
"style_type": "風格",
"learn_more": "了解更多",
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
"paint_course": "教程",
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
"image_file_required": "請先上傳圖片",
"image_file_retry": "請重新上傳圖片",
@@ -886,7 +893,8 @@
"seed_tip": "控制放大結果的隨機性",
"magic_prompt_option_tip": "智能優化放大提示詞"
},
"rendering_speed": "渲染速度"
"rendering_speed": "渲染速度",
"text_desc_required": "請先輸入圖片描述"
},
"prompts": {
"explanation": "幫我解釋一下這個概念",
@@ -1305,6 +1313,8 @@
"stdio": "標準輸入/輸出 (stdio)",
"inMemory": "記憶體",
"config_description": "設定模型上下文協議伺服器",
"disable": "不使用 MCP 伺服器",
"disable.description": "不啟用 MCP 服務功能",
"deleteError": "刪除伺服器失敗",
"deleteSuccess": "伺服器刪除成功",
"dependenciesInstall": "安裝相依套件",
@@ -1368,7 +1378,13 @@
"inputSchema": "輸入模式",
"availableTools": "可用工具",
"noToolsAvailable": "無可用工具",
"loadError": "獲取工具失敗"
"loadError": "獲取工具失敗",
"configureDefaults": "配置預設參數",
"configureDefaultsDescription": "配置工具參數的預設值。啟用後,如果 AI 模型未提供這些值,將自動使用。",
"defaultValue": "預設值",
"configSaved": "工具配置已成功保存",
"hasDefaults": "預設值已配置",
"hasDefaultTooltip": "此工具已配置預設參數。"
},
"prompts": {
"availablePrompts": "可用提示",
@@ -1556,6 +1572,9 @@
"rate_limit": "速率限制",
"tooltip": "使用 Github Copilot 需要先登入 Github"
},
"dmxapi": {
"select_platform": "選擇平臺"
},
"delete.content": "確定要刪除此提供者嗎?",
"delete.title": "刪除提供者",
"docs_check": "檢查",

View File

@@ -36,9 +36,9 @@ const Chat: FC<Props> = (props) => {
const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth} - 5px)`
const minusAssistantsWidth = showAssistants ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
const minusRightTopicsWidth = showRightTopics ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => {

View File

@@ -4,9 +4,8 @@ import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { EventEmitter } from '@renderer/services/EventService'
import { Assistant, MCPPrompt, MCPResource, MCPServer } from '@renderer/types'
import { Form, Input, Tooltip } from 'antd'
import { Plus, SquareTerminal } from 'lucide-react'
import { FC, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import React from 'react'
import { CircleX, Plus, SquareTerminal } from 'lucide-react'
import React, { FC, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@@ -132,9 +131,6 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
() => activedMcpServers.filter((server) => mcpServers.some((s) => s.id === server.id)),
[activedMcpServers, mcpServers]
)
const buttonEnabled = assistantMcpServers.length > 0
const handleMcpServerSelect = useCallback(
(server: MCPServer) => {
if (assistantMcpServers.some((s) => s.id === server.id)) {
@@ -156,6 +152,18 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
return () => EventEmitter.off('mcp-server-select', handler)
}, [])
const updateMcpEnabled = useCallback(
(enabled: boolean) => {
setTimeout(() => {
updateAssistant({
...assistant,
mcpServers: enabled ? assistant.mcpServers || [] : []
})
}, 200)
},
[assistant, updateAssistant]
)
const menuItems = useMemo(() => {
const newList: QuickPanelListItem[] = activedMcpServers.map((server) => ({
label: server.name,
@@ -171,8 +179,16 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
action: () => navigate('/settings/mcp')
})
newList.unshift({
label: t('common.close'),
description: t('settings.mcp.disable.description'),
icon: <CircleX />,
isSelected: !(assistant.mcpServers && assistant.mcpServers.length > 0),
action: () => updateMcpEnabled(false)
})
return newList
}, [activedMcpServers, t, assistantMcpServers, navigate])
}, [activedMcpServers, t, assistant.mcpServers, assistantMcpServers, navigate, updateMcpEnabled])
const openQuickPanel = useCallback(() => {
quickPanel.open({
@@ -412,10 +428,9 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
}, [activedMcpServers])
const openResourcesList = useCallback(async () => {
const resources = resourcesList
quickPanel.open({
title: t('settings.mcp.title'),
list: resources,
list: resourcesList,
symbol: 'mcp-resource',
multiple: true
})
@@ -442,7 +457,10 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
return (
<Tooltip placement="top" title={t('settings.mcp.title')} arrow>
<ToolbarButton type="text" onClick={handleOpenQuickPanel}>
<SquareTerminal size={18} color={buttonEnabled ? 'var(--color-primary)' : 'var(--color-icon)'} />
<SquareTerminal
size={18}
color={assistant.mcpServers && assistant.mcpServers.length > 0 ? 'var(--color-primary)' : 'var(--color-icon)'}
/>
</ToolbarButton>
</Tooltip>
)

View File

@@ -6,7 +6,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
import { Assistant, WebSearchProvider } from '@renderer/types'
import { hasObjectKey } from '@renderer/utils'
import { Tooltip } from 'antd'
import { Globe, Settings } from 'lucide-react'
import { CircleX, Globe, Settings } from 'lucide-react'
import { FC, memo, useCallback, useImperativeHandle, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
@@ -85,9 +85,9 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
})
items.unshift({
label: t('chat.input.web_search.no_web_search'),
label: t('common.close'),
description: t('chat.input.web_search.no_web_search.description'),
icon: <Globe />,
icon: <CircleX />,
isSelected: !assistant.enableWebSearch && !assistant.webSearchProviderId,
action: () => {
updateSelectedWebSearchProvider(undefined)

View File

@@ -163,7 +163,9 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex>
)}
{role === 'user' && !renderInputMessageAsMarkdown ? (
<p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{block.content}</p>
<p className="markdown" style={{ marginBottom: 5 }}>
{block.content}
</p>
) : (
<Markdown block={{ ...block, content: ignoreToolUse }} />
)}

View File

@@ -2,11 +2,13 @@ import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
import Scrollbar from '@renderer/components/Scrollbar'
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic'
import SelectionBox from '@renderer/pages/home/Messages/SelectionBox'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getContextCount, getGroupedMessages, getUserMessage } from '@renderer/services/MessagesService'
@@ -64,7 +66,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const { displayCount, clearTopicMessages, deleteMessage, createTopicBranch } = useMessageOperations(topic)
const messagesRef = useRef<Message[]>(messages)
// const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
const { isMultiSelectMode, handleSelectMessage } = useChatContext(topic)
useEffect(() => {
messagesRef.current = messages
@@ -86,9 +88,9 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth} - 5px)`
const minusAssistantsWidth = showAssistants ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
const minusRightTopicsWidth = showRightTopics ? `- var(--assistants-width) - var(--scrollbar-width)` : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition])
const scrollToBottom = useCallback(() => {
@@ -313,13 +315,13 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
</NarrowLayout>
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
{/* TODO: 多选功能实现有问题,需要重新改改 */}
{/* <SelectionBox
<SelectionBox
isMultiSelectMode={isMultiSelectMode}
scrollContainerRef={scrollContainerRef}
messageElements={messageElements.current}
handleSelectMessage={handleSelectMessage}
/> */}
/>
</Container>
)
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
interface SelectionBoxProps {
@@ -18,6 +18,8 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [dragCurrent, setDragCurrent] = useState({ x: 0, y: 0 })
const dragSelectedIds = useRef<Set<string>>(new Set())
useEffect(() => {
if (!isMultiSelectMode) return
@@ -25,96 +27,90 @@ const SelectionBox: React.FC<SelectionBoxProps> = ({
const container = scrollContainerRef.current!
if (!container) return { x: 0, y: 0 }
const rect = container.getBoundingClientRect()
const x = e.clientX - rect.left + container.scrollLeft
const y = e.clientY - rect.top + container.scrollTop
return { x, y }
return {
x: e.clientX - rect.left + container.scrollLeft,
y: e.clientY - rect.top + container.scrollTop
}
}
const handleMouseDown = (e: MouseEvent) => {
if ((e.target as HTMLElement).closest('.ant-checkbox-wrapper')) return
if ((e.target as HTMLElement).closest('.MessageFooter')) return
e.preventDefault()
setIsDragging(true)
const pos = updateDragPos(e)
setDragStart(pos)
setDragCurrent(pos)
dragSelectedIds.current.clear()
document.body.classList.add('no-select')
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return
setDragCurrent(updateDragPos(e))
const container = scrollContainerRef.current!
if (container) {
const { top, bottom } = container.getBoundingClientRect()
const scrollSpeed = 15
if (e.clientY < top + 50) {
container.scrollBy(0, -scrollSpeed)
} else if (e.clientY > bottom - 50) {
container.scrollBy(0, scrollSpeed)
e.preventDefault()
const pos = updateDragPos(e)
setDragCurrent(pos)
// 计算当前框选矩形
const left = Math.min(dragStart.x, pos.x)
const right = Math.max(dragStart.x, pos.x)
const top = Math.min(dragStart.y, pos.y)
const bottom = Math.max(dragStart.y, pos.y)
// 创建新选中的消息ID集合
const newSelectedIds = new Set<string>()
messageElements.forEach((el, id) => {
// 检查消息是否已被选中(不管是拖动选中还是手动选中)
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement | null
const isAlreadySelected = checkbox?.checked || false
// 如果已经被记录为拖动选中,跳过
if (dragSelectedIds.current.has(id)) return
const rect = el.getBoundingClientRect()
const container = scrollContainerRef.current!
const eTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const eLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const eBottom = eTop + rect.height
const eRight = eLeft + rect.width
// 检查消息是否在当前选择框内
const isInSelectionBox = !(eRight < left || eLeft > right || eBottom < top || eTop > bottom)
// 只有在选择框内且未被选中的消息才需要处理
if (isInSelectionBox && !isAlreadySelected) {
handleSelectMessage(id, true)
dragSelectedIds.current.add(id)
newSelectedIds.add(id)
el.classList.add('selection-highlight')
setTimeout(() => el.classList.remove('selection-highlight'), 300)
}
}
})
}
const handleMouseUp = () => {
if (!isDragging) return
const left = Math.min(dragStart.x, dragCurrent.x)
const right = Math.max(dragStart.x, dragCurrent.x)
const top = Math.min(dragStart.y, dragCurrent.y)
const bottom = Math.max(dragStart.y, dragCurrent.y)
const MIN_SELECTION_SIZE = 5
const isValidSelection =
Math.abs(right - left) > MIN_SELECTION_SIZE && Math.abs(bottom - top) > MIN_SELECTION_SIZE
if (isValidSelection) {
messageElements.forEach((element, messageId) => {
try {
const rect = element.getBoundingClientRect()
const container = scrollContainerRef.current!
const elementTop = rect.top - container.getBoundingClientRect().top + container.scrollTop
const elementLeft = rect.left - container.getBoundingClientRect().left + container.scrollLeft
const elementBottom = elementTop + rect.height
const elementRight = elementLeft + rect.width
const isIntersecting = !(
elementRight < left ||
elementLeft > right ||
elementBottom < top ||
elementTop > bottom
)
if (isIntersecting) {
handleSelectMessage(messageId, true)
element.classList.add('selection-highlight')
setTimeout(() => element.classList.remove('selection-highlight'), 300)
}
} catch (error) {
console.error('Error calculating element intersection:', error)
}
})
}
setIsDragging(false)
document.body.classList.remove('no-select')
}
const container = scrollContainerRef.current!
if (container) {
container.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}
container?.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
return () => {
if (container) {
container.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
container?.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
document.body.classList.remove('no-select')
}
}, [isMultiSelectMode, isDragging, dragStart, dragCurrent, handleSelectMessage, scrollContainerRef, messageElements])
}, [isMultiSelectMode, isDragging, dragStart, scrollContainerRef, messageElements, handleSelectMessage])
if (!isDragging || !isMultiSelectMode) return null

View File

@@ -0,0 +1,702 @@
import { PlusOutlined, RedoOutlined } from '@ant-design/icons'
import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { FileType, PaintingsState } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { DmxapiPainting, PaintingAction } from '@types'
import { Avatar, Button, Input, Radio, Select, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import React, { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton'
import { SettingHelpLink, SettingTitle } from '../settings'
import Artboard from './Artboard'
import {
COURSE_URL,
DEFAULT_PAINTING,
IMAGE_SIZES,
STYLE_TYPE_OPTIONS,
TEXT_TO_IMAGES_MODELS
} from './config/DmxapiConfig'
import PaintingsList from './PaintingsList'
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => {
const [mode] = useState<keyof PaintingsState>('DMXAPIPaintings')
const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings()
const [painting, setPainting] = useState<DmxapiPainting>(DMXAPIPaintings?.[0] || DEFAULT_PAINTING)
const { theme } = useTheme()
const { t } = useTranslation()
const providers = useAllProviders()
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
return {
label: t(`provider.${provider?.id}`),
value: provider?.id
}
})
const dmxapiProvider = providers.find((p) => p.id === 'dmxapi')!
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const getNewPainting = () => {
return {
...DEFAULT_PAINTING,
id: uuid(),
seed: generateRandomSeed()
}
}
const modelOptions = TEXT_TO_IMAGES_MODELS.map((model) => ({
label: model.name,
value: model.id
}))
const textareaRef = useRef<any>(null)
const updatePaintingState = (updates: Partial<DmxapiPainting>) => {
const updatedPainting = { ...painting, ...updates }
setPainting(updatedPainting)
updatePainting('DMXAPIPaintings', updatedPainting)
}
const onSelectModel = (modelId: string) => {
const model = TEXT_TO_IMAGES_MODELS.find((m) => m.id === modelId)
if (model) {
updatePaintingState({ model: modelId })
}
}
const onCancel = () => {
abortController?.abort()
}
const onSelectImageSize = (v: string) => {
const size = IMAGE_SIZES.find((i) => i.value === v)
size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label })
}
const onSelectStyleType = (v: string) => {
if (v === painting.style_type) {
updatePaintingState({ style_type: '' })
} else {
updatePaintingState({ style_type: v })
}
}
const onInputSeed = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
// 允许空值或合法整数,且大于等于 -1
if (value === '' || value === '-' || /^-?\d+$/.test(value)) {
const numValue = parseInt(value, 10)
if (numValue >= -1 || value === '' || value === '-') {
updatePaintingState({ seed: value })
}
}
}
// 检查提供者状态函数
const checkProviderStatus = () => {
if (!dmxapiProvider.enabled) {
throw new Error('error.provider_disabled')
}
if (!dmxapiProvider.apiKey) {
throw new Error('error.no_api_key')
}
if (!painting.model) {
throw new Error('error.missing_required_fields')
}
if (!painting.prompt) {
throw new Error('paintings.text_desc_required')
}
}
// 准备V1生成请求函数
const prepareV1GenerateRequest = (prompt: string, painting: DmxapiPainting) => {
const params = {
prompt,
model: painting.model,
n: painting.n
}
if (painting.aspect_ratio) {
params['aspect_ratio'] = painting.aspect_ratio
}
if (painting.image_size) {
params['size'] = painting.image_size
}
if (painting.seed) {
if (Number(painting.seed) >= -1) {
params['seed'] = Number(painting.seed)
} else {
params['seed'] = -1
}
}
if (painting.style_type) {
params.prompt = prompt + ',风格:' + painting.style_type
}
return {
body: JSON.stringify(params),
endpoint: `${dmxapiProvider.apiHost}/v1/images/generations`
}
}
// API请求函数
const callApi = async (requestConfig: { endpoint: string; body: any }) => {
const { endpoint, body } = requestConfig
const headers = {}
// 如果是JSON数据添加Content-Type头
if (typeof body === 'string') {
headers['Content-Type'] = 'application/json'
headers['Authorization'] = `Bearer ${dmxapiProvider.apiKey}`
headers['User-Agent'] = 'DMXAPI/1.0.0 (https://www.dmxapi.com)'
headers['Accept'] = 'application/json'
}
const response = await fetch(endpoint, {
method: 'POST',
headers,
body
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error?.message || '操作失败')
}
const data = await response.json()
return data.data.map((item: { url: string }) => item.url)
}
// 下载图像函数
const downloadImages = async (urls: string[]) => {
return Promise.all(
urls.map(async (url) => {
try {
if (!url || url.trim() === '') {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url, true)
} catch (error) {
if (
error instanceof Error &&
(error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL'))
) {
window.message.warning({
content: t('message.empty_url'),
key: 'empty-url-warning'
})
}
return null
}
})
)
}
// 准备请求配置函数
const prepareRequestConfig = (prompt: string, painting: PaintingAction) => {
// 根据模式和模型版本返回不同的请求配置
return prepareV1GenerateRequest(prompt, painting)
}
const onGenerate = async () => {
try {
// 获取提示词
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
updatePaintingState({ prompt })
// 检查提供者状态
checkProviderStatus()
// 处理已有文件
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
content: t('paintings.regenerate.confirm'),
centered: true
})
if (!confirmed) return
await FileManager.deleteFiles(painting.files)
}
// 设置请求状态
const controller = new AbortController()
setAbortController(controller)
setIsLoading(true)
dispatch(setGenerating(true))
// 准备请求配置
const requestConfig = prepareRequestConfig(prompt, painting)
// 发送API请求
const urls = await callApi(requestConfig)
// 下载图像
if (urls.length > 0) {
const downloadedFiles = await downloadImages(urls)
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
// 保存文件并更新状态
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls })
}
} catch (error) {
// 错误处理
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content:
error.message.startsWith('paintings.') || error.message.startsWith('error.')
? t(error.message)
: getErrorMessage(error),
centered: true
})
}
} finally {
// 清理状态
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
}
const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
}
const prevImage = () => {
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
}
const onDeletePainting = (paintingToDelete: DmxapiPainting) => {
if (paintingToDelete.id === painting.id) {
const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id)
if (currentIndex > 0) {
setPainting(DMXAPIPaintings[currentIndex - 1])
} else if (DMXAPIPaintings.length > 1) {
setPainting(DMXAPIPaintings[1])
}
}
removePainting(mode, paintingToDelete).then(() => {})
}
const onSelectPainting = (newPainting: DmxapiPainting) => {
if (generating) return
setPainting(newPainting)
setCurrentImageIndex(0)
}
const { autoTranslateWithSpace } = useSettings()
const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false)
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const translate = async () => {
if (isTranslating) {
return
}
if (!painting.prompt) {
return
}
try {
setIsTranslating(true)
const translatedText = await translateText(painting.prompt, 'english')
updatePaintingState({ prompt: translatedText })
} catch (error) {
console.error('Translation failed:', error)
} finally {
setIsTranslating(false)
}
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (autoTranslateWithSpace && event.key === ' ') {
setSpaceClickCount((prev) => prev + 1)
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
spaceClickTimer.current = setTimeout(() => {
setSpaceClickCount(0)
}, 200)
if (spaceClickCount === 2) {
setSpaceClickCount(0)
setIsTranslating(true)
translate().then(() => {})
}
}
}
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
}
}
useEffect(() => {
if (!DMXAPIPaintings || DMXAPIPaintings.length === 0) {
const newPainting = getNewPainting()
addPainting('DMXAPIPaintings', newPainting)
setPainting(newPainting)
}
return () => {
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
}
}, [DMXAPIPaintings, DMXAPIPaintings.length, addPainting, mode])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button
size="small"
className="nodrag"
icon={<PlusOutlined />}
onClick={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
<ContentContainer id="content-container">
<LeftContainer>
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href={COURSE_URL}>
{t('paintings.paint_course')}
<ProviderLogo
shape="square"
src={getProviderLogo(dmxapiProvider.id)}
size={16}
style={{ marginLeft: 5 }}
/>
</SettingHelpLink>
</ProviderTitleContainer>
<Select value={providerOptions[2].value} onChange={handleProviderChange} style={{ marginBottom: 15 }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
<Radio.Group
value={painting.image_size}
onChange={(e) => onSelectImageSize(e.target.value)}
style={{ display: 'flex' }}>
{IMAGE_SIZES.map((size) => (
<RadioButton value={size.value} key={size.value}>
<VStack alignItems="center">
<ImageSizeImage src={size.icon} theme={theme} />
<span>{size.label}</span>
</VStack>
</RadioButton>
))}
</Radio.Group>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
{t('paintings.seed')}
<Tooltip title={t('paintings.seed_desc_tip')}>
<InfoIcon />
</Tooltip>
</SettingTitle>
<Input
value={painting.seed}
pattern="[0-9]*"
onChange={(e) => onInputSeed(e)}
suffix={
<RedoOutlined
onClick={() => updatePaintingState({ seed: Math.floor(Math.random() * 1000000).toString() })}
style={{ cursor: 'pointer', color: 'var(--color-text-2)' }}
/>
}
/>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.style_type')}</SettingTitle>
<SliderContainer>
<RadioTextBox>
{STYLE_TYPE_OPTIONS.map((ele) => (
<RadioTextItem
key={ele.label}
className={painting.style_type === ele.label ? 'selected' : ''}
onClick={() => onSelectStyleType(ele.label)}>
{ele.label}
</RadioTextItem>
))}
</RadioTextBox>
</SliderContainer>
</LeftContainer>
<MainContainer>
{painting?.urls?.length > 0 || DMXAPIPaintings?.length > 1 ? (
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
/>
) : (
<EmptyImgBox>
<EmptyImg></EmptyImg>
</EmptyImgBox>
)}
<InputContainer>
<Textarea
ref={textareaRef}
variant="borderless"
disabled={isLoading}
value={painting.prompt}
spellCheck={false}
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
onKeyDown={handleKeyDown}
/>
<Toolbar>
<ToolbarMenu>
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
</ToolbarMenu>
</Toolbar>
</InputContainer>
</MainContainer>
<PaintingsList
namespace="DMXAPIPaintings"
paintings={DMXAPIPaintings}
selectedPainting={painting}
onSelectPainting={onSelectPainting}
onDeletePainting={onDeletePainting}
onNewPainting={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))}
/>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
// 添加新的样式组件
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
background-color: var(--color-background);
overflow: hidden;
`
const LeftContainer = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 20px;
background-color: var(--color-background);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
`
const InputContainer = styled.div`
display: flex;
flex-direction: column;
min-height: 95px;
max-height: 95px;
position: relative;
border: 1px solid var(--color-border-soft);
transition: all 0.3s ease;
margin: 0 20px 15px 20px;
border-radius: 10px;
`
const Textarea = styled(TextArea)`
padding: 10px;
border-radius: 0;
display: flex;
flex: 1;
resize: none !important;
overflow: auto;
width: auto;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
padding: 0 8px;
height: 40px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px;
`
const RadioButton = styled(Radio.Button)`
width: 30px;
height: 55px;
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
align-items: center;
`
const InfoIcon = styled(Info)`
margin-left: 5px;
cursor: help;
color: var(--color-text-2);
opacity: 0.6;
width: 16px;
height: 16px;
&:hover {
opacity: 1;
}
`
const SliderContainer = styled.div`
display: flex;
align-items: center;
gap: 16px;
.ant-slider {
flex: 1;
}
`
const RadioTextBox = styled.div`
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
`
const RadioTextItem = styled.div`
cursor: pointer;
padding: 2px 6px;
border-radius: 6px;
transition: all 0.2s ease;
border: 1px solid var(--color-border);
/* 默认状态 */
background-color: var(--color-background);
/* 悬浮状态 */
&:hover {
background-color: var(--color-hover, #f0f0f0);
}
/* 选中状态 - 需要添加selected类名 */
&.selected {
background-color: var(--color-primary, #1890ff);
color: white;
border: 1px solid var(--color-primary, #1890ff);
}
`
const EmptyImgBox = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
`
const EmptyImg = styled.div`
width: 70vh;
height: 70vh;
background-size: 100% 100%;
background-image: url(${DMXAPIToImg});
`
export default DmxapiPage

View File

@@ -2,9 +2,10 @@ import { FC } from 'react'
import { Route, Routes } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
import SiliconPage from './PaintingsPage'
const Options = ['aihubmix', 'silicon']
const Options = ['aihubmix', 'silicon', 'dmxapi']
const PaintingsRoutePage: FC = () => {
return (
@@ -12,6 +13,7 @@ const PaintingsRoutePage: FC = () => {
<Route path="/" element={<AihubmixPage Options={Options} />} />
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
</Routes>
)
}

View File

@@ -0,0 +1,94 @@
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
import { uuid } from '@renderer/utils'
import { DmxapiPainting } from '@types'
export const STYLE_TYPE_OPTIONS = [
{ label: '吉卜力', value: '吉卜力' },
{ label: '皮克斯', value: '皮克斯' },
{ label: '绒线玩偶', value: '绒线玩偶' },
{ label: '水彩画', value: '水彩画' },
{ label: '卡通插画', value: '卡通插画' },
{ label: '3D卡通', value: '3D卡通' },
{ label: '日系动漫', value: '日系动漫' },
{ label: '木雕', value: '木雕' },
{ label: '唯美古风', value: '唯美古风' },
{ label: '2.5D动画', value: '2.5D动画' },
{ label: '清新日漫', value: '清新日漫' },
{ label: '黏土', value: '黏土' },
{ label: '小人书插画', value: '小人书插画' },
{ label: '浮世绘', value: '浮世绘' },
{ label: '毛毡', value: '毛毡' },
{ label: '美式复古', value: '美式复古' },
{ label: '赛博朋克', value: '赛博朋克' },
{ label: '素描', value: '素描' },
{ label: '莫奈花园', value: '莫奈花园' },
{ label: '厚涂手绘', value: '厚涂手绘' },
{ label: '扁平', value: '扁平' },
{ label: '肌理', value: '肌理' },
{ label: '像素艺术', value: '像素艺术' },
{ label: '街头艺术', value: '街头艺术' },
{ label: '迷幻', value: '迷幻' },
{ label: '国风工笔', value: '国风工笔' },
{ label: '巴洛克', value: '巴洛克' }
]
export const TEXT_TO_IMAGES_MODELS = [
{
id: 'seedream-3.0',
provider: 'DMXAPI',
name: ' 即梦 seedream-3.0'
}
]
export const IMAGE_SIZES = [
{
label: '1:1',
value: '1328x1328',
icon: ImageSize1_1
},
{
label: '1:2',
value: '800x1600',
icon: ImageSize1_2
},
{
label: '3:2',
value: '1584x1056',
icon: ImageSize3_2
},
{
label: '3:4',
value: '1104x1472',
icon: ImageSize3_4
},
{
label: '16:9',
value: '1664x936',
icon: ImageSize16_9
},
{
label: '9:16',
value: '936x1664',
icon: ImageSize9_16
}
]
export const COURSE_URL = 'http://seedream.dmxapi.cn/'
export const DEFAULT_PAINTING: DmxapiPainting = {
id: uuid(),
urls: [],
files: [],
prompt: '',
image_size: '1328x1328',
aspect_ratio: '1:1',
n: 1,
seed: '',
style_type: '',
model: TEXT_TO_IMAGES_MODELS[0].id
}

View File

@@ -40,6 +40,7 @@ export type AihubmixMode = keyof PaintingsState
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
return {
paintings: [],
DMXAPIPaintings: [],
generate: [
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
{

View File

@@ -22,10 +22,7 @@ const AgentsSubscribeUrlSettings: FC = () => {
return (
<SettingGroup theme={theme}>
<SettingTitle>
{t('agents.tag.agent')}
{t('settings.websearch.subscribe_add')}
</SettingTitle>
<SettingTitle>{t('agents.settings.subscription.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.websearch.subscribe_url')}</SettingRowTitle>

View File

@@ -2,7 +2,8 @@ import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { MCPPrompt, MCPResource, MCPServer, MCPTool, MCPToolConfig, MCPToolParameterConfig } from '@renderer/types'
import { isEmpty } from '@renderer/utils'
import { formatMcpError } from '@renderer/utils/error'
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
import TextArea from 'antd/es/input/TextArea'
@@ -398,6 +399,42 @@ const McpSettings: React.FC = () => {
[server, updateMCPServer]
)
// Handle updating tool parameter configuration
const handleUpdateToolConfig = useCallback(
async (toolName: string, parameterConfig: MCPToolParameterConfig[]) => {
let customToolConfigs = [...(server.customToolConfigs || [])]
const existingConfigIndex = customToolConfigs.findIndex((config) => config.toolName === toolName)
const newToolConfig: MCPToolConfig = {
toolName,
parameters: parameterConfig
}
if (existingConfigIndex >= 0) {
customToolConfigs[existingConfigIndex] = newToolConfig
} else {
customToolConfigs.push(newToolConfig)
}
customToolConfigs = customToolConfigs.filter((config) =>
config.parameters.some((param) => !isEmpty(param.defaultValue))
)
const updatedServer = {
...server,
customToolConfigs
}
updateMCPServer(updatedServer)
window.message.success({
content: t('settings.mcp.tools.configSaved'),
key: 'mcp-tool-config'
})
},
[server, updateMCPServer, t]
)
const tabs = [
{
key: 'settings',
@@ -592,7 +629,14 @@ const McpSettings: React.FC = () => {
{
key: 'tools',
label: t('settings.mcp.tabs.tools'),
children: <MCPToolsSection tools={tools} server={server} onToggleTool={handleToggleTool} />
children: (
<MCPToolsSection
tools={tools}
server={server}
onToggleTool={handleToggleTool}
onUpdateToolConfig={handleUpdateToolConfig}
/>
)
},
{
key: 'prompts',

View File

@@ -1,5 +1,21 @@
import { MCPServer, MCPTool } from '@renderer/types'
import { Badge, Collapse, Descriptions, Empty, Flex, Switch, Tag, Tooltip, Typography } from 'antd'
import { CloseOutlined, SaveOutlined, SettingOutlined } from '@ant-design/icons'
import { MCPServer, MCPTool, MCPToolParameterConfig } from '@renderer/types'
import { isEmpty } from '@renderer/utils'
import {
Button,
Collapse,
Descriptions,
Empty,
Flex,
Input,
InputNumber,
Space,
Switch,
Tag,
Tooltip,
Typography
} from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -7,22 +23,211 @@ interface MCPToolsSectionProps {
tools: MCPTool[]
server: MCPServer
onToggleTool: (tool: MCPTool, enabled: boolean) => void
onUpdateToolConfig?: (toolName: string, config: MCPToolParameterConfig[]) => void
}
const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps) => {
const MCPToolsSection = ({ tools, server, onToggleTool, onUpdateToolConfig }: MCPToolsSectionProps) => {
const { t } = useTranslation()
const [editingToolName, setEditingToolName] = useState<string | null>(null)
const [editableToolParams, setEditableToolParams] = useState<MCPToolParameterConfig[]>([])
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// Effect to reset editable params when editingToolName changes or server config changes
useEffect(() => {
if (editingToolName) {
const tool = tools.find((t) => t.name === editingToolName)
if (tool) {
initializeEditableParams(tool)
} else {
setEditingToolName(null)
}
} else {
setEditableToolParams([])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingToolName, server.customToolConfigs, tools])
// Check if a tool is enabled (not in the disabledTools array)
const isToolEnabled = (tool: MCPTool) => {
return !server.disabledTools?.includes(tool.name)
}
// Handle tool toggle
const handleToggle = (tool: MCPTool, checked: boolean) => {
onToggleTool(tool, checked)
}
// Render tool properties from the input schema
const getToolConfig = (toolName: string): MCPToolParameterConfig[] => {
const toolConfig = server.customToolConfigs?.find((config) => config.toolName === toolName)
return toolConfig?.parameters || []
}
const getDefaultValueForType = (type: string): any => {
switch (type) {
case 'string':
return ''
case 'number':
return 0
case 'boolean':
return false
case 'array':
return []
case 'object':
return {}
default:
return ''
}
}
const initializeEditableParams = (tool: MCPTool) => {
const currentConfig = getToolConfig(tool.name)
const initialConfig: MCPToolParameterConfig[] = []
if (tool.inputSchema?.properties) {
Object.entries(tool.inputSchema.properties).forEach(([paramName, paramDef]: [string, any]) => {
const existingConfig = currentConfig.find((c) => c.name === paramName)
initialConfig.push({
name: paramName,
defaultValue: existingConfig?.defaultValue ?? getDefaultValueForType(paramDef.type),
description: paramDef.description || ''
})
})
}
setEditableToolParams(initialConfig)
}
const handleEditToolParams = (tool: MCPTool) => {
if (editingToolName === tool.name) {
// If already editing this tool, cancel editing
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
} else {
setEditingToolName(tool.name)
setHasUnsavedChanges(false)
// initializeEditableParams will be called by the useEffect
}
}
const handleSaveToolParams = (toolName: string) => {
if (onUpdateToolConfig) {
onUpdateToolConfig(toolName, editableToolParams)
}
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
}
const handleCancelEditToolParams = () => {
setEditingToolName(null)
setEditableToolParams([])
setHasUnsavedChanges(false)
}
const updateParameterConfig = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
const newConfig = [...editableToolParams]
newConfig[index] = { ...newConfig[index], [field]: value }
setEditableToolParams(newConfig)
setHasUnsavedChanges(true)
}
const handleParameterBlur = (index: number, field: keyof MCPToolParameterConfig, value: any) => {
updateParameterConfig(index, field, value)
}
const renderParameterInput = (param: MCPToolParameterConfig, index: number, paramDef: any) => {
const { type } = paramDef
switch (type) {
case 'number':
return (
<InputNumber
value={param.defaultValue}
onChange={(value) => updateParameterConfig(index, 'defaultValue', value)}
onBlur={(e) => {
const value = e.target.value ? Number(e.target.value) : undefined
handleParameterBlur(index, 'defaultValue', value)
}}
style={{ width: '100%' }}
placeholder="Enter default number"
/>
)
case 'boolean':
return (
<Switch
checked={param.defaultValue}
onChange={(checked) => {
updateParameterConfig(index, 'defaultValue', checked)
// Switch 组件没有 onBlur直接在 onChange 中处理
handleParameterBlur(index, 'defaultValue', checked)
}}
/>
)
case 'array':
return (
<Input.TextArea
value={Array.isArray(param.defaultValue) ? param.defaultValue.join('\n') : ''}
onChange={(e) => {
const lines = e.target.value.split('\n').filter((line) => line.trim())
updateParameterConfig(index, 'defaultValue', lines)
}}
onBlur={(e) => {
const lines = e.target.value.split('\n').filter((line) => line.trim())
handleParameterBlur(index, 'defaultValue', lines)
}}
placeholder="Enter array items (one per line)"
rows={3}
/>
)
default: // 'string' and 'object' (as JSON string for simplicity here)
if (type === 'object') {
return (
<Input.TextArea
value={
typeof param.defaultValue === 'object'
? JSON.stringify(param.defaultValue, null, 2)
: param.defaultValue
}
onChange={(e) => {
try {
const val = e.target.value
// Attempt to parse if it's meant to be an object, otherwise store as string
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
updateParameterConfig(index, 'defaultValue', JSON.parse(val))
} else {
updateParameterConfig(index, 'defaultValue', val)
}
} catch (error) {
// If JSON is invalid while typing, keep the string value
updateParameterConfig(index, 'defaultValue', e.target.value)
}
}}
onBlur={(e) => {
try {
const val = e.target.value
if (val.trim().startsWith('{') || val.trim().startsWith('[')) {
handleParameterBlur(index, 'defaultValue', JSON.parse(val))
} else {
handleParameterBlur(index, 'defaultValue', val)
}
} catch (error) {
// If JSON is invalid, keep the string value
handleParameterBlur(index, 'defaultValue', e.target.value)
}
}}
placeholder="Enter default JSON object or string"
rows={3}
/>
)
}
return (
<Input
value={param.defaultValue}
onChange={(e) => updateParameterConfig(index, 'defaultValue', e.target.value)}
onBlur={(e) => handleParameterBlur(index, 'defaultValue', e.target.value)}
placeholder="Enter default value"
/>
)
}
}
const renderToolProperties = (tool: MCPTool) => {
if (!tool.inputSchema?.properties) return null
@@ -43,52 +248,123 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
}
}
const toolConfig = getToolConfig(tool.name)
const isEditingThisTool = editingToolName === tool.name
return (
<div style={{ marginTop: 12 }}>
<Typography.Title level={5}>{t('settings.mcp.tools.inputSchema')}:</Typography.Title>
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
<Typography.Title level={5} style={{ margin: 0 }}>
{t('settings.mcp.tools.inputSchema')}:
</Typography.Title>
<Tooltip title={t('settings.mcp.tools.configureDefaults')}>
<Button
type={isEditingThisTool ? 'primary' : 'text'}
icon={<SettingOutlined />}
size="small"
onClick={() => handleEditToolParams(tool)}
/>
</Tooltip>
</Flex>
<Descriptions bordered size="small" column={1} style={{ marginTop: 8 }}>
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => (
<Descriptions.Item
key={key}
label={
<Flex gap={4}>
<Typography.Text strong>{key}</Typography.Text>
{tool.inputSchema.required?.includes(key) && (
<Tooltip title="Required field">
<span style={{ color: '#f5222d' }}>*</span>
</Tooltip>
{Object.entries(tool.inputSchema.properties).map(([key, prop]: [string, any]) => {
const paramDefFromSchema = tool.inputSchema?.properties?.[key] as any
const currentParamSetting = toolConfig.find((c) => c.name === key)
const hasDefaultValue = !isEmpty(currentParamSetting?.defaultValue)
const editableParam = isEditingThisTool ? editableToolParams.find((p) => p.name === key) : null
return (
<Descriptions.Item
key={key}
label={
<Flex gap={4} align="center">
<Typography.Text strong>{key}</Typography.Text>
{tool.inputSchema.required?.includes(key) && (
<Tag color="red" style={{ margin: 0 }}>
{t('common.required')}
</Tag>
)}
{prop.type && (
<Tag color={getTypeColor(prop.type)} style={{ margin: 0 }}>
{prop.type}
</Tag>
)}
</Flex>
}>
<Flex vertical gap={8}>
{prop.description && (
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, fontSize: '13px' }}>
{prop.description}
</Typography.Paragraph>
)}
</Flex>
}>
<Flex vertical gap={4}>
<Flex align="center" gap={8}>
{prop.type && (
// <Typography.Text type="secondary">{prop.type} </Typography.Text>
<Badge
color={getTypeColor(prop.type)}
text={<Typography.Text type="secondary">{prop.type}</Typography.Text>}
/>
{isEditingThisTool && editableParam && paramDefFromSchema ? (
<Flex
align="center"
gap={8}
style={{
marginTop: 8,
border: '1px solid var(--color-border-secondary)',
borderRadius: 6,
backgroundColor: 'var(--color-background)'
}}>
<Typography.Text style={{ fontWeight: 500, marginBottom: 0, flexShrink: 0 }}>
{t('settings.mcp.tools.defaultValue')}:
</Typography.Text>
{renderParameterInput(
editableParam,
editableToolParams.findIndex((p) => p.name === key),
paramDefFromSchema
)}
</Flex>
) : (
hasDefaultValue && (
<div style={{ marginTop: 4 }}>
<Typography.Text type="secondary">{t('settings.mcp.tools.defaultValue')}: </Typography.Text>
<Tag color="geekblue">{JSON.stringify(currentParamSetting?.defaultValue)}</Tag>
</div>
)
)}
</Flex>
{prop.description && (
<Typography.Paragraph type="secondary" style={{ marginBottom: 0, marginTop: 4 }}>
{prop.description}
</Typography.Paragraph>
)}
{prop.enum && (
<div style={{ marginTop: 4 }}>
<Typography.Text type="secondary">Allowed values: </Typography.Text>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 }}>
{prop.enum.map((value: string, idx: number) => (
<Tag key={idx}>{value}</Tag>
))}
{prop.enum && (
<div style={{ marginTop: 4 }}>
<Typography.Text type="secondary">{t('common.allowed_values')}: </Typography.Text>
<Flex wrap="wrap" gap={4} style={{ marginTop: 4 }}>
{prop.enum.map((value: string, idx: number) => (
<Tag key={idx}>{value}</Tag>
))}
</Flex>
</div>
</div>
)}
</Flex>
</Descriptions.Item>
))}
)}
</Flex>
</Descriptions.Item>
)
})}
</Descriptions>
{isEditingThisTool && (
<Flex justify="space-between" align="center" style={{ marginTop: 16 }}>
<Flex align="center" gap={8}>
{hasUnsavedChanges && (
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
{t('common.unsaved_changes')}
</Typography.Text>
)}
</Flex>
<Flex gap={8}>
<Button icon={<CloseOutlined />} onClick={handleCancelEditToolParams}>
{t('common.cancel')}
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => handleSaveToolParams(tool.name)}
disabled={!hasUnsavedChanges}>
{t('common.save')}
</Button>
</Flex>
</Flex>
)}
</div>
)
}
@@ -97,35 +373,65 @@ const MCPToolsSection = ({ tools, server, onToggleTool }: MCPToolsSectionProps)
<Section>
<SectionTitle>{t('settings.mcp.tools.availableTools')}</SectionTitle>
{tools.length > 0 ? (
<Collapse bordered={false} ghost>
<Collapse bordered={false} ghost accordion>
{tools.map((tool) => (
<Collapse.Panel
key={tool.id}
header={
<Flex justify="space-between" align="center" style={{ width: '100%' }}>
<Flex vertical align="flex-start">
<Flex vertical align="flex-start" style={{ flexGrow: 1, maxWidth: 'calc(100% - 60px)' }}>
<Flex align="center" style={{ width: '100%' }}>
<Typography.Text strong>{tool.name}</Typography.Text>
<Typography.Text type="secondary" style={{ marginLeft: 8, fontSize: '12px' }}>
{tool.id}
<Typography.Text strong style={{ marginRight: 8 }}>
{tool.name}
</Typography.Text>
<Typography.Text
type="secondary"
style={{
fontSize: '12px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
({tool.id})
</Typography.Text>
{server.customToolConfigs
?.find((c) => c.toolName === tool.name)
?.parameters.some((p) => !isEmpty(p.defaultValue)) && (
<Tooltip title={t('settings.mcp.tools.hasDefaultTooltip')}>
<Tag color="blue" style={{ marginLeft: 'auto', marginRight: 8, flexShrink: 0 }}>
{t('settings.mcp.tools.hasDefaults')}
</Tag>
</Tooltip>
)}
</Flex>
{tool.description && (
<Typography.Text type="secondary" style={{ fontSize: '13px', marginTop: 4 }}>
{tool.description.length > 100 ? `${tool.description.substring(0, 100)}...` : tool.description}
<Typography.Text
type="secondary"
style={{
fontSize: '13px',
marginTop: 4,
width: '100%',
whiteSpace: 'normal',
wordBreak: 'break-word'
}}>
{tool.description.length > 150 ? `${tool.description.substring(0, 150)}...` : tool.description}
</Typography.Text>
)}
</Flex>
<Switch
checked={isToolEnabled(tool)}
onChange={(checked, event) => {
event?.stopPropagation()
handleToggle(tool, checked)
}}
/>
<Space onClick={(e) => e.stopPropagation()} style={{ marginLeft: 10 }}>
<Switch
checked={isToolEnabled(tool)}
onChange={(checked) => {
handleToggle(tool, checked)
}}
/>
</Space>
</Flex>
}>
<SelectableContent>{renderToolProperties(tool)}</SelectableContent>
<SelectableContent
onClick={(e) => e.stopPropagation() /* Prevent collapse toggle when clicking content */}>
{renderToolProperties(tool)}
</SelectableContent>
</Collapse.Panel>
))}
</Collapse>
@@ -150,7 +456,7 @@ const SectionTitle = styled.h3`
const SelectableContent = styled.div`
user-select: text;
padding: 0 12px;
padding: 0 12px 12px 12px;
`
export default MCPToolsSection

View File

@@ -0,0 +1,122 @@
import DmxapiLogo from '@renderer/assets/images/providers/dmxapi-logo.webp'
import { useProvider } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
import { Radio, RadioChangeEvent, Space } from 'antd'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingSubtitle } from '..'
interface DMXAPISettingsProps {
provider: Provider
setApiKey: (apiKey: string) => void
}
// DMXAPI平台选项
enum PlatformType {
OFFICIAL = 'https://www.DMXAPI.cn',
INTERNATIONAL = 'https://www.DMXAPI.com',
OVERSEA = 'https://ssvip.DMXAPI.com'
}
const PlatformOptions = [
{
label: 'www.DMXAPI.cn 人民币站',
value: PlatformType.OFFICIAL,
apiKeyWebsite: 'https://www.dmxapi.cn/register?aff=bwwY'
},
{
label: 'www.DMXAPI.com 国际站',
value: PlatformType.INTERNATIONAL,
apiKeyWebsite: 'https://www.dmxapi.com/register'
},
{
label: 'ssvip.DMXAPI.com 生产级商用站',
value: PlatformType.OVERSEA,
apiKeyWebsite: 'https://ssvip.dmxapi.com/register'
}
]
const DMXAPISettings: FC<DMXAPISettingsProps> = ({ provider: initialProvider }) => {
const { provider, updateProvider } = useProvider(initialProvider.id)
const { t } = useTranslation()
// 获取当前选中的平台,如果没有设置则默认为官方平台
const getCurrentPlatform = (): PlatformType => {
if (!provider.apiHost) return PlatformType.OFFICIAL
if (provider.apiHost.includes('DMXAPI.com')) {
return provider.apiHost.includes('ssvip') ? PlatformType.OVERSEA : PlatformType.INTERNATIONAL
}
return PlatformType.OFFICIAL
}
// 状态管理
const [selectedPlatform, setSelectedPlatform] = useState<PlatformType>(getCurrentPlatform())
// 处理平台选择变更
const handlePlatformChange = useCallback(
(e: RadioChangeEvent) => {
const platform = e.target.value as PlatformType
setSelectedPlatform(platform)
updateProvider({ ...provider, apiHost: platform })
},
[provider, updateProvider]
)
return (
<Container>
<Space direction="vertical" style={{ width: '100%' }}>
<LogoContainer>
<Logo src={DmxapiLogo}></Logo>
</LogoContainer>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.dmxapi.select_platform')}</SettingSubtitle>
<Radio.Group
style={{
display: 'flex',
flexDirection: 'column',
gap: 8
}}
onChange={handlePlatformChange}
value={selectedPlatform}
options={PlatformOptions.map((option) => ({
...option,
label: (
<span>
{option.label}{' '}
<a href={option.apiKeyWebsite} target="_blank" rel="noopener noreferrer">
( API密钥)
</a>
</span>
)
}))}></Radio.Group>
</Space>
</Container>
)
}
// 样式组件
const Container = styled.div`
margin-top: 16px;
margin-bottom: 30px;
`
const LogoContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 30px;
`
const Logo = styled.img`
height: 70px;
display: block;
width: auto;
`
export default DMXAPISettings

View File

@@ -32,6 +32,7 @@ import {
SettingTitle
} from '..'
import ApiCheckPopup from './ApiCheckPopup'
import DMXAPISettings from './DMXAPISettings'
import GithubCopilotSettings from './GithubCopilotSettings'
import GPUStackSettings from './GPUStackSettings'
import HealthCheckPopup from './HealthCheckPopup'
@@ -64,6 +65,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const isAzureOpenAI = provider.id === 'azure-openai' || provider.type === 'azure-openai'
const isDmxapi = provider.id === 'dmxapi'
const providerConfig = PROVIDER_CONFIG[provider.id]
const officialWebsite = providerConfig?.websites?.official
const apiKeyWebsite = providerConfig?.websites?.apiKey
@@ -328,6 +331,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
/>
)}
{provider.id === 'openai' && <OpenAIAlert />}
{isDmxapi && <DMXAPISettings provider={provider} setApiKey={setApiKey} />}
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
@@ -358,35 +362,43 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{apiKeyWebsite && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
{!isDmxapi && (
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
)}
</HStack>
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
</SettingHelpTextRow>
)}
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
<Button danger onClick={onReset}>
{t('settings.provider.api.url.reset')}
</Button>
)}
</Space.Compact>
{isOpenAIProvider(provider) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
{hostPreview()}
</SettingHelpText>
<SettingHelpText style={{ minWidth: 'fit-content' }}>{t('settings.provider.api.url.tip')}</SettingHelpText>
</SettingHelpTextRow>
{!isDmxapi && (
<>
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
<Button danger onClick={onReset}>
{t('settings.provider.api.url.reset')}
</Button>
)}
</Space.Compact>
{isOpenAIProvider(provider) && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<SettingHelpText
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
{hostPreview()}
</SettingHelpText>
<SettingHelpText style={{ minWidth: 'fit-content' }}>
{t('settings.provider.api.url.tip')}
</SettingHelpText>
</SettingHelpTextRow>
)}
</>
)}
{isAzureOpenAI && (
<>

View File

@@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 106,
version: 107,
blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate
},

View File

@@ -1445,6 +1445,16 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'107': (state: RootState) => {
try {
if (state.paintings && !state.paintings.DMXAPIPaintings) {
state.paintings.DMXAPIPaintings = []
}
return state
} catch (error) {
return state
}
}
}

View File

@@ -6,7 +6,8 @@ const initialState: PaintingsState = {
generate: [],
remix: [],
edit: [],
upscale: []
upscale: [],
DMXAPIPaintings: []
}
const paintingsSlice = createSlice({

View File

@@ -246,6 +246,16 @@ export interface ScalePainting extends PaintingParams {
renderingSpeed?: string
}
export interface DmxapiPainting extends PaintingParams {
model?: string
prompt?: string
n?: number
aspect_ratio?: string
image_size?: string
seed?: string
style_type?: string
}
export type PaintingAction = Partial<GeneratePainting & RemixPainting & EditPainting & ScalePainting> & PaintingParams
export interface PaintingsState {
@@ -254,6 +264,7 @@ export interface PaintingsState {
remix: Partial<RemixPainting> & PaintingParams[]
edit: Partial<EditPainting> & PaintingParams[]
upscale: Partial<ScalePainting> & PaintingParams[]
DMXAPIPaintings: DmxapiPainting[]
}
export type MinAppType = {
@@ -507,6 +518,19 @@ export interface MCPConfigSample {
env?: Record<string, string> | undefined
}
// MCP工具自定义参数配置接口
export interface MCPToolParameterConfig {
name: string
defaultValue: any
description?: string
}
// MCP工具配置接口
export interface MCPToolConfig {
toolName: string
parameters: MCPToolParameterConfig[]
}
export interface MCPServer {
id: string
name: string
@@ -519,6 +543,7 @@ export interface MCPServer {
env?: Record<string, string>
isActive: boolean
disabledTools?: string[] // List of tool names that are disabled for this server
customToolConfigs?: MCPToolConfig[]
configSample?: MCPConfigSample
headers?: Record<string, string> // Custom headers to be sent with requests to this server
searchKey?: string

View File

@@ -4,6 +4,17 @@ import { ModalFuncProps } from 'antd/es/modal/interface'
// @ts-ignore next-line`
import { v4 as uuidv4 } from 'uuid'
export function isEmpty(value: any) {
return (
value === null ||
value === undefined ||
value === '' ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && Object.keys(value).length === 0) ||
(typeof value === 'number' && value === 0)
)
}
/**
* 异步执行一个函数。
* @param fn 要执行的函数