feat: 支持自定义助手地址 (#5540)

* feat: 支持自定义助手地址

feat: 支持自定义助手地址

* feat: 更新多语言支持,添加“defaultaides”字段至隐私设置,并修改默认智能体的值为设置中的引用。

* feat: 更新默认助手设置,修复函数命名错误并优化状态管理

* refactor: update agent loading logic to use settings and improve error handling

* refactor: update DefaultaidesSettings to use custom hook for state management and replace icon in DataSettings

* fix: improve error message formatting in callMCPTool function

* feat: 添加多语言支持的默认助手设置,包括英文、日文、俄文和中文的翻译。

* refactor: 优化智能体加载逻辑,合并本地和远程智能体,并改进错误处理。

* feat: add import functionality for agents and update translations in multiple languages.

* feat: implement agent import functionality with validation and error handling.

* feat(i18n): add import success and error messages for agents in multiple languages.

* fix(i18n): standardize import success message key across multiple languages.

* refactor(i18n): remove default aides section from multiple language files

* refactor(i18n): update Traditional Chinese translations for agent management and privacy settings.

* feat(i18n): add import functionality for agents with URL and file options, including error handling and translations in multiple languages.

* refactor(i18n): rename 'defaultaides' to 'defaultAgent' across multiple language files and update related settings.

* refactor(AgentsPage): remove unused addAgent function from useAgents hook.

---------

Co-authored-by: 上房揭瓦 <hoaobo@foxmail.com>
Co-authored-by: George Zhao <georgezhao@SKJLAB>
Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
George Zhao 2025-05-04 21:04:04 +08:00 committed by GitHub
parent 0bad95230e
commit 1e5ec5df7f
10 changed files with 317 additions and 28 deletions

View File

@ -9,6 +9,23 @@
"add.prompt": "Prompt",
"add.prompt.placeholder": "Enter prompt",
"add.title": "Create Agent",
"import.title": "Import from External",
"import": {
"title": "Import from External",
"type": {
"url": "URL",
"file": "File"
},
"url_placeholder": "Enter JSON URL",
"select_file": "Select File",
"button": "Import",
"file_filter": "JSON Files",
"error": {
"url_required": "Please enter a URL",
"fetch_failed": "Failed to fetch from URL",
"invalid_format": "Invalid agent format: missing required fields"
}
},
"delete.popup.content": "Are you sure you want to delete this agent?",
"edit.message.add.title": "Add",
"edit.message.assistant.placeholder": "Enter assistant message",
@ -498,6 +515,10 @@
"title": "Mermaid Diagram"
},
"message": {
"agents": {
"imported": "Imported successfully",
"import.error": "Import failed"
},
"api.check.model.title": "Select the model to use for detection",
"api.connection.failed": "Connection failed",
"api.connection.success": "Connection successful",
@ -707,7 +728,7 @@
"aspect_ratio": "Aspect Ratio",
"style_type": "Style",
"learn_more": "Learn More",
"prompt_placeholder_edit": "Enter your image description, text drawing uses “double quotes” to wrap",
"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",
"image_file_retry": "Please re-upload an image first",
@ -1515,7 +1536,8 @@
"privacy": {
"title": "Privacy Settings",
"enable_privacy_mode": "Anonymous reporting of errors and statistics"
}
},
"defaultAgent": "Built-in"
},
"translate": {
"any.language": "Any language",

View File

@ -9,6 +9,23 @@
"add.prompt": "プロンプト",
"add.prompt.placeholder": "プロンプトを入力",
"add.title": "エージェントを作成",
"import.title": "外部からインポート",
"import": {
"title": "外部からインポート",
"type": {
"url": "URL",
"file": "ファイル"
},
"url_placeholder": "JSON URLを入力",
"select_file": "ファイルを選択",
"button": "インポート",
"file_filter": "JSONファイル",
"error": {
"url_required": "URLを入力してください",
"fetch_failed": "URLからのデータ取得に失敗しました",
"invalid_format": "無効なエージェント形式:必須フィールドが不足しています"
}
},
"delete.popup.content": "このエージェントを削除してもよろしいですか?",
"edit.message.add.title": "追加",
"edit.message.assistant.placeholder": "アシスタントのメッセージを入力",
@ -498,6 +515,10 @@
"title": "Mermaid図"
},
"message": {
"agents": {
"imported": "インポートに成功しました",
"import.error": "インポートに失敗しました"
},
"api.check.model.title": "検出に使用するモデルを選択してください",
"api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました",
@ -1515,6 +1536,7 @@
"title": "プライバシー設定",
"enable_privacy_mode": "匿名エラーレポートとデータ統計の送信"
},
"defaultAgent": "内蔵",
"input.show_translate_confirm": "翻訳確認ダイアログを表示"
},
"translate": {

View File

@ -1,7 +1,7 @@
{
"translation": {
"agents": {
"add.button": "Добавить к ассистенту",
"add.button": "Добавить в ассистента",
"add.knowledge_base": "База знаний",
"add.knowledge_base.placeholder": "Выберите базу знаний",
"add.name": "Имя",
@ -9,6 +9,7 @@
"add.prompt": "Промпт",
"add.prompt.placeholder": "Введите промпт",
"add.title": "Создать агента",
"import.title": "Импорт из внешнего источника",
"delete.popup.content": "Вы уверены, что хотите удалить этого агента?",
"edit.message.add.title": "Добавить",
"edit.message.assistant.placeholder": "Введите сообщение ассистента",
@ -29,7 +30,23 @@
"tag.default": "По умолчанию",
"tag.new": "Новый",
"tag.system": "Система",
"title": "Агенты"
"title": "Агенты",
"import": {
"title": "Импорт из внешнего источника",
"type": {
"url": "URL",
"file": "Файл"
},
"url_placeholder": "Введите URL JSON",
"select_file": "Выбрать файл",
"button": "Импорт",
"file_filter": "JSON файлы",
"error": {
"url_required": "Пожалуйста, введите URL",
"fetch_failed": "Не удалось получить данные по URL",
"invalid_format": "Неверный формат агента: отсутствуют обязательные поля"
}
}
},
"assistants": {
"title": "Ассистенты",
@ -498,6 +515,10 @@
"title": "Диаграмма Mermaid"
},
"message": {
"agents": {
"imported": "Импорт успешно выполнен",
"import.error": "Импорт не выполнен"
},
"api.check.model.title": "Выберите модель для проверки",
"api.connection.failed": "Соединение не удалось",
"api.connection.success": "Соединение успешно",
@ -1512,9 +1533,10 @@
"multiple": "Множественный выбор"
},
"privacy": {
"title": "Настройки приватности",
"enable_privacy_mode": "Анонимная отправка отчетов об ошибках и статистики"
"title": "Настройки конфиденциальности",
"enable_privacy_mode": "Анонимная отчетность об ошибках и статистике"
},
"defaultAgent": "Встроенный",
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода"
},
"translate": {

View File

@ -9,6 +9,22 @@
"add.prompt": "提示词",
"add.prompt.placeholder": "输入提示词",
"add.title": "创建智能体",
"import": {
"title": "从外部导入",
"type": {
"url": "URL",
"file": "文件"
},
"url_placeholder": "输入 JSON URL",
"select_file": "选择文件",
"button": "导入",
"file_filter": "JSON 文件",
"error": {
"url_required": "请输入 URL",
"fetch_failed": "从 URL 获取数据失败",
"invalid_format": "无效的代理格式:缺少必填字段"
}
},
"delete.popup.content": "确定要删除此智能体吗?",
"edit.message.add.title": "添加",
"edit.message.assistant.placeholder": "输入助手消息",
@ -498,6 +514,10 @@
"title": "Mermaid 图表"
},
"message": {
"agents": {
"imported": "导入成功",
"import.error": "导入失败"
},
"api.check.model.title": "请选择要检测的模型",
"api.connection.failed": "连接失败",
"api.connection.success": "连接成功",
@ -707,7 +727,7 @@
"aspect_ratio": "画幅比例",
"style_type": "风格",
"learn_more": "了解更多",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 “双引号” 包裹",
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
"image_file_required": "请先上传图片",
"image_file_retry": "请重新上传图片",

View File

@ -9,6 +9,22 @@
"add.prompt": "提示詞",
"add.prompt.placeholder": "輸入提示詞",
"add.title": "建立智慧代理人",
"import": {
"title": "從外部導入",
"type": {
"url": "URL",
"file": "檔案"
},
"url_placeholder": "輸入 JSON URL",
"select_file": "選擇檔案",
"button": "導入",
"file_filter": "JSON 檔案",
"error": {
"url_required": "請輸入 URL",
"fetch_failed": "從 URL 獲取資料失敗",
"invalid_format": "無效的代理人格式:缺少必填欄位"
}
},
"delete.popup.content": "確定要刪除此智慧代理人嗎?",
"edit.message.add.title": "新增",
"edit.message.assistant.placeholder": "輸入助手訊息",
@ -498,6 +514,10 @@
"title": "Mermaid 圖表"
},
"message": {
"agents": {
"imported": "匯入成功",
"import.error": "匯入失敗"
},
"api.check.model.title": "請選擇要偵測的模型",
"api.connection.failed": "連接失敗",
"api.connection.success": "連接成功",

View File

@ -53,7 +53,7 @@
"settings.reasoning_effort.medium": "Moyen",
"settings.reasoning_effort.off": "Off",
"settings.reasoning_effort.tip": "Prise en charge uniquement des modèles de raisonnement OpenAI o-series et Anthropic",
"title": "Aides"
"title": "Agent"
},
"auth": {
"error": "Échec de l'obtention automatique de la clé, veuillez la récupérer manuellement",

View File

@ -1,4 +1,4 @@
import { PlusOutlined } from '@ant-design/icons'
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem'
@ -20,6 +20,7 @@ import { groupTranslations } from './agentGroupTranslations'
import AddAgentPopup from './components/AddAgentPopup'
import AgentCard from './components/AgentCard'
import { AgentGroupIcon } from './components/AgentGroupIcon'
import ImportAgentPopup from './components/ImportAgentPopup'
const AgentsPage: FC = () => {
const [search, setSearch] = useState('')
@ -138,6 +139,17 @@ const AgentsPage: FC = () => {
})
}
const handleImportAgent = async () => {
try {
await ImportAgentPopup.show()
} catch (error) {
window.message.error({
content: error instanceof Error ? error.message : t('message.agents.import.error'),
key: 'agents-import-error'
})
}
}
return (
<Container>
<Navbar>
@ -208,9 +220,14 @@ const AgentsPage: FC = () => {
</CustomTag>
}
</AgentsListTitle>
<Button type="text" onClick={handleAddAgent} icon={<PlusOutlined />}>
{t('agents.add.title')}
</Button>
<Flex gap={8}>
<Button type="text" onClick={handleImportAgent} icon={<ImportOutlined />}>
{t('agents.import.title')}
</Button>
<Button type="text" onClick={handleAddAgent} icon={<PlusOutlined />}>
{t('agents.add.title')}
</Button>
</Flex>
</AgentsListHeader>
{filteredAgents.length > 0 ? (

View File

@ -0,0 +1,148 @@
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Agent } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Form, Input, Modal, Radio, Space } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
resolve: (value: Agent[] | null) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [form] = Form.useForm()
const { t } = useTranslation()
const { addAgent } = useAgents()
const [importType, setImportType] = useState<'url' | 'file'>('url')
const [loading, setLoading] = useState(false)
const onFinish = async (values: { url?: string }) => {
setLoading(true)
try {
let agents: Agent[] = []
if (importType === 'url') {
if (!values.url) {
throw new Error(t('agents.import.error.url_required'))
}
const response = await fetch(values.url)
if (!response.ok) {
throw new Error(t('agents.import.error.fetch_failed'))
}
const data = await response.json()
agents = Array.isArray(data) ? data : [data]
} else {
const result = await window.api.file.open({
filters: [{ name: t('agents.import.file_filter'), extensions: ['json'] }]
})
if (result) {
agents = JSON.parse(new TextDecoder('utf-8').decode(result.content))
if (!Array.isArray(agents)) {
agents = [agents]
}
}
}
// Validate and process agents
for (const agent of agents) {
if (!agent.name || !agent.prompt) {
throw new Error(t('agents.import.error.invalid_format'))
}
const newAgent: Agent = {
id: uuid(),
name: agent.name,
emoji: agent.emoji || '🤖',
group: agent.group || [],
prompt: agent.prompt,
description: agent.description || '',
type: 'agent',
topics: [],
messages: [],
defaultModel: getDefaultModel()
}
addAgent(newAgent)
}
window.message.success({
content: t('message.agents.imported'),
key: 'agents-imported'
})
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
setOpen(false)
resolve(agents)
} catch (error) {
window.message.error({
content: error instanceof Error ? error.message : t('message.agents.import.error'),
key: 'agents-import-error'
})
} finally {
setLoading(false)
}
}
const onCancel = () => {
setOpen(false)
resolve(null)
}
return (
<Modal title={t('agents.import.title')} open={open} onCancel={onCancel} footer={null} centered>
<Form form={form} onFinish={onFinish} layout="vertical">
<Form.Item>
<Radio.Group value={importType} onChange={(e) => setImportType(e.target.value)}>
<Radio.Button value="url">{t('agents.import.type.url')}</Radio.Button>
<Radio.Button value="file">{t('agents.import.type.file')}</Radio.Button>
</Radio.Group>
</Form.Item>
{importType === 'url' && (
<Form.Item name="url" rules={[{ required: true, message: t('agents.import.error.url_required') }]}>
<Input placeholder={t('agents.import.url_placeholder')} />
</Form.Item>
)}
{importType === 'file' && (
<Form.Item>
<Button onClick={() => form.submit()}>{t('agents.import.select_file')}</Button>
</Form.Item>
)}
<Form.Item>
<Space>
<Button onClick={onCancel}>{t('common.cancel')}</Button>
<Button type="primary" onClick={() => form.submit()} loading={loading}>
{t('agents.import.button')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
)
}
export default class ImportAgentPopup {
static show() {
return new Promise<Agent[] | null>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'ImportAgentPopup'
)
})
}
static hide() {
TopView.hide('ImportAgentPopup')
}
}

View File

@ -1,8 +1,7 @@
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Agent } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect, useState } from 'react'
let _agents: Agent[] = []
export const getAgentsFromSystemAgents = (systemAgents: any) => {
@ -17,17 +16,30 @@ export const getAgentsFromSystemAgents = (systemAgents: any) => {
}
export function useSystemAgents() {
const [agents, setAgents] = useState<Agent[]>(_agents)
const { defaultAgent } = useSettings()
const [agents, setAgents] = useState<Agent[]>([])
const { resourcesPath } = useRuntime()
useEffect(() => {
runAsyncFunction(async () => {
if (!resourcesPath || _agents.length > 0) return
const agents = await window.api.fs.read(resourcesPath + '/data/agents.json')
_agents = JSON.parse(agents) as Agent[]
setAgents(_agents)
})
}, [resourcesPath])
const loadAgents = async () => {
try {
// 始终加载本地 agents
if (resourcesPath && _agents.length === 0) {
const localAgentsData = await window.api.fs.read(resourcesPath + '/data/agents.json')
_agents = JSON.parse(localAgentsData) as Agent[]
}
// 如果没有远程配置或获取失败,使用本地 agents
setAgents(_agents)
} catch (error) {
console.error('Failed to load agents:', error)
// 发生错误时使用本地 agents
setAgents(_agents)
}
}
loadAgents()
}, [defaultAgent, resourcesPath])
return agents
}

View File

@ -104,6 +104,7 @@ export interface SettingsState {
joplinToken: string | null
joplinUrl: string | null
defaultObsidianVault: string | null
defaultAgent: string | null
// 思源笔记配置
siyuanApiUrl: string | null
siyuanToken: string | null
@ -210,6 +211,7 @@ export const initialState: SettingsState = {
joplinToken: '',
joplinUrl: '',
defaultObsidianVault: null,
defaultAgent: null,
siyuanApiUrl: null,
siyuanToken: null,
siyuanBoxId: null,
@ -465,6 +467,15 @@ const settingsSlice = createSlice({
setJoplinUrl: (state, action: PayloadAction<string>) => {
state.joplinUrl = action.payload
},
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
state.messageNavigation = action.payload
},
setDefaultObsidianVault: (state, action: PayloadAction<string>) => {
state.defaultObsidianVault = action.payload
},
setDefaultAgent: (state, action: PayloadAction<string>) => {
state.defaultAgent = action.payload
},
setSiyuanApiUrl: (state, action: PayloadAction<string>) => {
state.siyuanApiUrl = action.payload
},
@ -477,12 +488,6 @@ const settingsSlice = createSlice({
setSiyuanRootPath: (state, action: PayloadAction<string>) => {
state.siyuanRootPath = action.payload
},
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
state.messageNavigation = action.payload
},
setDefaultObsidianVault: (state, action: PayloadAction<string>) => {
state.defaultObsidianVault = action.payload
},
setMaxKeepAliveMinapps: (state, action: PayloadAction<number>) => {
state.maxKeepAliveMinapps = action.payload
},
@ -584,6 +589,7 @@ export const {
setJoplinUrl,
setMessageNavigation,
setDefaultObsidianVault,
setDefaultAgent,
setSiyuanApiUrl,
setSiyuanToken,
setSiyuanBoxId,