diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index f0b33ba941..6932cf2d8d 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 737856f73a..f492ab71e9 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 5d6a81318d..2f5f03fa3c 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 0e8161cf0e..283c7377fd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "请重新上传图片", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 63515cdd4c..e0beba9b41 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "連接成功", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 9d25e9a3cd..cae791831b 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -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", diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx index 162177e838..b9873c310b 100644 --- a/src/renderer/src/pages/agents/AgentsPage.tsx +++ b/src/renderer/src/pages/agents/AgentsPage.tsx @@ -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 ( @@ -208,9 +220,14 @@ const AgentsPage: FC = () => { } - + + + + {filteredAgents.length > 0 ? ( diff --git a/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx new file mode 100644 index 0000000000..f9ed41f0fb --- /dev/null +++ b/src/renderer/src/pages/agents/components/ImportAgentPopup.tsx @@ -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 = ({ 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 ( + +
+ + setImportType(e.target.value)}> + {t('agents.import.type.url')} + {t('agents.import.type.file')} + + + + {importType === 'url' && ( + + + + )} + + {importType === 'file' && ( + + + + )} + + + + + + + +
+
+ ) +} + +export default class ImportAgentPopup { + static show() { + return new Promise((resolve) => { + TopView.show( + { + resolve(v) + this.hide() + }} + />, + 'ImportAgentPopup' + ) + }) + } + + static hide() { + TopView.hide('ImportAgentPopup') + } +} diff --git a/src/renderer/src/pages/agents/index.ts b/src/renderer/src/pages/agents/index.ts index d5bd6361eb..cf07d1df95 100644 --- a/src/renderer/src/pages/agents/index.ts +++ b/src/renderer/src/pages/agents/index.ts @@ -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(_agents) + const { defaultAgent } = useSettings() + const [agents, setAgents] = useState([]) 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 } diff --git a/src/renderer/src/store/settings.ts b/src/renderer/src/store/settings.ts index 1e6241c5ac..81db1454a2 100644 --- a/src/renderer/src/store/settings.ts +++ b/src/renderer/src/store/settings.ts @@ -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) => { state.joplinUrl = action.payload }, + setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => { + state.messageNavigation = action.payload + }, + setDefaultObsidianVault: (state, action: PayloadAction) => { + state.defaultObsidianVault = action.payload + }, + setDefaultAgent: (state, action: PayloadAction) => { + state.defaultAgent = action.payload + }, setSiyuanApiUrl: (state, action: PayloadAction) => { state.siyuanApiUrl = action.payload }, @@ -477,12 +488,6 @@ const settingsSlice = createSlice({ setSiyuanRootPath: (state, action: PayloadAction) => { state.siyuanRootPath = action.payload }, - setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => { - state.messageNavigation = action.payload - }, - setDefaultObsidianVault: (state, action: PayloadAction) => { - state.defaultObsidianVault = action.payload - }, setMaxKeepAliveMinapps: (state, action: PayloadAction) => { state.maxKeepAliveMinapps = action.payload }, @@ -584,6 +589,7 @@ export const { setJoplinUrl, setMessageNavigation, setDefaultObsidianVault, + setDefaultAgent, setSiyuanApiUrl, setSiyuanToken, setSiyuanBoxId,