From ce494de793c02fdcc14039a9e8f52b56d022d772 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Tue, 24 Mar 2026 19:43:53 -0500 Subject: [PATCH 1/4] feat(subagent): add JSON import/export actions in dashboard --- .../i18n/locales/en-US/features/subagent.json | 7 ++ .../i18n/locales/ru-RU/features/subagent.json | 9 +- .../i18n/locales/zh-CN/features/subagent.json | 7 ++ dashboard/src/views/SubAgentPage.vue | 98 ++++++++++++++++--- 4 files changed, 108 insertions(+), 13 deletions(-) diff --git a/dashboard/src/i18n/locales/en-US/features/subagent.json b/dashboard/src/i18n/locales/en-US/features/subagent.json index 7bfb0b08da..5f047af349 100644 --- a/dashboard/src/i18n/locales/en-US/features/subagent.json +++ b/dashboard/src/i18n/locales/en-US/features/subagent.json @@ -7,6 +7,8 @@ "actions": { "refresh": "Refresh", "save": "Save", + "import": "Import JSON", + "export": "Export JSON", "add": "Add SubAgent", "delete": "Delete", "close": "Close" @@ -48,6 +50,11 @@ "messages": { "loadConfigFailed": "Failed to load config", "loadPersonaFailed": "Failed to load persona list", + "importSuccess": "Configuration imported successfully", + "importFailed": "Failed to import configuration", + "importInvalidJson": "The imported file must be a valid JSON object", + "exportSuccess": "Configuration exported successfully", + "exportFailed": "Failed to export configuration", "nameMissing": "A SubAgent is missing a name", "nameInvalid": "Invalid SubAgent name: only lowercase letters/numbers/underscores, starting with a letter", "nameDuplicate": "Duplicate SubAgent name: {name}", diff --git a/dashboard/src/i18n/locales/ru-RU/features/subagent.json b/dashboard/src/i18n/locales/ru-RU/features/subagent.json index 368bd20467..b6b1791237 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/subagent.json +++ b/dashboard/src/i18n/locales/ru-RU/features/subagent.json @@ -7,6 +7,8 @@ "actions": { "refresh": "Обновить", "save": "Сохранить", + "import": "Импорт JSON", + "export": "Экспорт JSON", "add": "Добавить SubAgent", "delete": "Удалить", "close": "Закрыть" @@ -48,6 +50,11 @@ "messages": { "loadConfigFailed": "Не удалось загрузить конфигурацию", "loadPersonaFailed": "Не удалось загрузить список персонажей", + "importSuccess": "Конфигурация успешно импортирована", + "importFailed": "Не удалось импортировать конфигурацию", + "importInvalidJson": "Импортируемый файл должен быть валидным JSON-объектом", + "exportSuccess": "Конфигурация успешно экспортирована", + "exportFailed": "Не удалось экспортировать конфигурацию", "nameMissing": "У SubAgent отсутствует имя", "nameInvalid": "Недопустимое имя SubAgent: только строчные латинские буквы/цифры/подчеркивания, должно начинаться с буквы", "nameDuplicate": "Дублирующееся имя SubAgent: {name}", @@ -62,4 +69,4 @@ "subtitle": "Добавьте первого под-агента, чтобы начать", "action": "Создать первого агента" } -} \ No newline at end of file +} diff --git a/dashboard/src/i18n/locales/zh-CN/features/subagent.json b/dashboard/src/i18n/locales/zh-CN/features/subagent.json index 9c7a43d7f2..7de92d9bad 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/subagent.json +++ b/dashboard/src/i18n/locales/zh-CN/features/subagent.json @@ -7,6 +7,8 @@ "actions": { "refresh": "刷新", "save": "保存", + "import": "导入 JSON", + "export": "导出 JSON", "add": "新增 SubAgent", "delete": "删除", "close": "关闭" @@ -49,6 +51,11 @@ "messages": { "loadConfigFailed": "获取配置失败", "loadPersonaFailed": "获取 Persona 列表失败", + "importSuccess": "配置导入成功", + "importFailed": "导入配置失败", + "importInvalidJson": "导入文件不是有效的 JSON 对象", + "exportSuccess": "配置导出成功", + "exportFailed": "导出配置失败", "nameMissing": "存在未填写名称的 SubAgent", "nameInvalid": "SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头", "nameDuplicate": "SubAgent 名称重复:{name}", diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index 029cc5a82c..c1ee7b62af 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -14,6 +14,22 @@
+ + {{ tm('actions.export') }} + + + {{ tm('actions.import') }} + {{ tm('actions.close') }} + +
@@ -275,6 +299,7 @@ const { tm } = useModuleI18n('features/subagent') const loading = ref(false) const saving = ref(false) +const importFileInputRef = ref(null) const snackbar = ref({ show: false, @@ -297,7 +322,7 @@ const mainStateDescription = computed(() => ) function normalizeConfig(raw: any): SubAgentConfig { - const main_enable = !!raw?.main_enable + const main_enable = raw?.main_enable !== undefined ? !!raw.main_enable : !!raw?.enable const remove_main_duplicate_tools = !!raw?.remove_main_duplicate_tools const agentsRaw = Array.isArray(raw?.agents) ? raw.agents : [] @@ -352,6 +377,65 @@ function removeAgent(idx: number) { cfg.value.agents.splice(idx, 1) } +function toPersistedConfig(source: SubAgentConfig) { + return { + main_enable: !!source.main_enable, + remove_main_duplicate_tools: !!source.remove_main_duplicate_tools, + agents: source.agents.map((a) => ({ + name: (a.name || '').trim(), + persona_id: (a.persona_id || '').trim(), + public_description: a.public_description || '', + enabled: a.enabled !== false, + provider_id: a.provider_id + })) + } +} + +function exportConfig() { + try { + const payload = toPersistedConfig(cfg.value) + const json = JSON.stringify(payload, null, 2) + const blob = new Blob([json], { type: 'application/json;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + const date = new Date().toISOString().slice(0, 10) + link.href = url + link.download = `subagent-config-${date}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + toast(tm('messages.exportSuccess'), 'success') + } catch (e: unknown) { + toast(tm('messages.exportFailed'), 'error') + } +} + +function openImportDialog() { + importFileInputRef.value?.click() +} + +async function handleImportFile(event: Event) { + const target = event.target as HTMLInputElement | null + const file = target?.files?.[0] + if (!file) return + + try { + const text = await file.text() + const parsed = JSON.parse(text) as unknown + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + toast(tm('messages.importInvalidJson'), 'error') + return + } + cfg.value = normalizeConfig(parsed) + toast(tm('messages.importSuccess'), 'success') + } catch (e: unknown) { + toast(tm('messages.importFailed'), 'error') + } finally { + if (target) target.value = '' + } +} + function validateBeforeSave(): boolean { const nameRe = /^[a-z][a-z0-9_]{0,63}$/ const seen = new Set() @@ -382,17 +466,7 @@ async function save() { if (!validateBeforeSave()) return saving.value = true try { - const payload = { - main_enable: cfg.value.main_enable, - remove_main_duplicate_tools: cfg.value.remove_main_duplicate_tools, - agents: cfg.value.agents.map((a) => ({ - name: a.name, - persona_id: a.persona_id, - public_description: a.public_description, - enabled: a.enabled, - provider_id: a.provider_id - })) - } + const payload = toPersistedConfig(cfg.value) const res = await axios.post('/api/subagent/config', payload) if (res.data.status === 'ok') { From a3abb226dca5c9300727c5b5c4f4f4119c98a319 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Tue, 24 Mar 2026 19:54:19 -0500 Subject: [PATCH 2/4] fix(subagent-ui): always revoke export object URL --- dashboard/src/views/SubAgentPage.vue | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index c1ee7b62af..33aa26a8ea 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -392,22 +392,30 @@ function toPersistedConfig(source: SubAgentConfig) { } function exportConfig() { + let url: string | null = null + let link: HTMLAnchorElement | null = null + try { const payload = toPersistedConfig(cfg.value) const json = JSON.stringify(payload, null, 2) const blob = new Blob([json], { type: 'application/json;charset=utf-8' }) - const url = URL.createObjectURL(blob) - const link = document.createElement('a') + url = URL.createObjectURL(blob) + link = document.createElement('a') const date = new Date().toISOString().slice(0, 10) link.href = url link.download = `subagent-config-${date}.json` document.body.appendChild(link) link.click() - document.body.removeChild(link) - URL.revokeObjectURL(url) toast(tm('messages.exportSuccess'), 'success') } catch (e: unknown) { toast(tm('messages.exportFailed'), 'error') + } finally { + if (link?.parentNode) { + link.parentNode.removeChild(link) + } + if (url) { + URL.revokeObjectURL(url) + } } } From 4a534b061a8d37f8366c0df70d40ad9527157537 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Tue, 24 Mar 2026 19:57:47 -0500 Subject: [PATCH 3/4] fix(subagent-ui): preserve enabled serialization semantics --- dashboard/src/views/SubAgentPage.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index 33aa26a8ea..0f519b78ce 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -385,7 +385,7 @@ function toPersistedConfig(source: SubAgentConfig) { name: (a.name || '').trim(), persona_id: (a.persona_id || '').trim(), public_description: a.public_description || '', - enabled: a.enabled !== false, + enabled: a.enabled, provider_id: a.provider_id })) } From a333bd8f00764841a0e57fbe1a9f656794abce30 Mon Sep 17 00:00:00 2001 From: Jacobinwwey Date: Tue, 24 Mar 2026 20:02:06 -0500 Subject: [PATCH 4/4] fix(subagent-ui): reject imports without expected config keys --- dashboard/src/views/SubAgentPage.vue | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index 0f519b78ce..f124a80f6e 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -435,6 +435,15 @@ async function handleImportFile(event: Event) { toast(tm('messages.importInvalidJson'), 'error') return } + + const obj = parsed as Record + const hasExpectedTopLevelKey = + 'agents' in obj || 'main_enable' in obj || 'enable' in obj + if (!hasExpectedTopLevelKey) { + toast(tm('messages.importInvalidJson'), 'error') + return + } + cfg.value = normalizeConfig(parsed) toast(tm('messages.importSuccess'), 'success') } catch (e: unknown) {