Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dashboard/src/i18n/locales/en-US/features/subagent.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"actions": {
"refresh": "Refresh",
"save": "Save",
"import": "Import JSON",
"export": "Export JSON",
"add": "Add SubAgent",
"delete": "Delete",
"close": "Close"
Expand Down Expand Up @@ -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}",
Expand Down
9 changes: 8 additions & 1 deletion dashboard/src/i18n/locales/ru-RU/features/subagent.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"actions": {
"refresh": "Обновить",
"save": "Сохранить",
"import": "Импорт JSON",
"export": "Экспорт JSON",
"add": "Добавить SubAgent",
"delete": "Удалить",
"close": "Закрыть"
Expand Down Expand Up @@ -48,6 +50,11 @@
"messages": {
"loadConfigFailed": "Не удалось загрузить конфигурацию",
"loadPersonaFailed": "Не удалось загрузить список персонажей",
"importSuccess": "Конфигурация успешно импортирована",
"importFailed": "Не удалось импортировать конфигурацию",
"importInvalidJson": "Импортируемый файл должен быть валидным JSON-объектом",
"exportSuccess": "Конфигурация успешно экспортирована",
"exportFailed": "Не удалось экспортировать конфигурацию",
"nameMissing": "У SubAgent отсутствует имя",
"nameInvalid": "Недопустимое имя SubAgent: только строчные латинские буквы/цифры/подчеркивания, должно начинаться с буквы",
"nameDuplicate": "Дублирующееся имя SubAgent: {name}",
Expand All @@ -62,4 +69,4 @@
"subtitle": "Добавьте первого под-агента, чтобы начать",
"action": "Создать первого агента"
}
}
}
7 changes: 7 additions & 0 deletions dashboard/src/i18n/locales/zh-CN/features/subagent.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"actions": {
"refresh": "刷新",
"save": "保存",
"import": "导入 JSON",
"export": "导出 JSON",
"add": "新增 SubAgent",
"delete": "删除",
"close": "关闭"
Expand Down Expand Up @@ -49,6 +51,11 @@
"messages": {
"loadConfigFailed": "获取配置失败",
"loadPersonaFailed": "获取 Persona 列表失败",
"importSuccess": "配置导入成功",
"importFailed": "导入配置失败",
"importInvalidJson": "导入文件不是有效的 JSON 对象",
"exportSuccess": "配置导出成功",
"exportFailed": "导出配置失败",
"nameMissing": "存在未填写名称的 SubAgent",
"nameInvalid": "SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头",
"nameDuplicate": "SubAgent 名称重复:{name}",
Expand Down
115 changes: 103 additions & 12 deletions dashboard/src/views/SubAgentPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@
</div>

<div class="d-flex align-center gap-2">
<v-btn
variant="text"
color="primary"
prepend-icon="mdi-download"
@click="exportConfig"
>
{{ tm('actions.export') }}
</v-btn>
<v-btn
variant="text"
color="primary"
prepend-icon="mdi-upload"
@click="openImportDialog"
>
{{ tm('actions.import') }}
</v-btn>
<v-btn
variant="text"
color="primary"
Expand Down Expand Up @@ -244,6 +260,14 @@
<v-btn variant="text" @click="snackbar.show = false">{{ tm('actions.close') }}</v-btn>
</template>
</v-snackbar>

<input
ref="importFileInputRef"
type="file"
accept="application/json,.json"
style="display: none;"
@change="handleImportFile"
/>
</div>
</template>

Expand Down Expand Up @@ -275,6 +299,7 @@ const { tm } = useModuleI18n('features/subagent')

const loading = ref(false)
const saving = ref(false)
const importFileInputRef = ref<HTMLInputElement | null>(null)

const snackbar = ref({
show: false,
Expand All @@ -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 : []

Expand Down Expand Up @@ -352,6 +377,82 @@ 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,
provider_id: a.provider_id
Comment on lines +387 to +389

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider normalizing enabled to a boolean in the persisted payload.

In toPersistedConfig, other flags are coerced to booleans, but enabled is not. If SubAgentConfig.enabled is ever null/undefined or another truthy/falsy value (e.g., from older configs or UI issues), the persisted payload may not match backend expectations. Coercing here (e.g., enabled: !!a.enabled) would keep this consistent with main_enable/remove_main_duplicate_tools and make saves/imports more robust.

Suggested change
public_description: a.public_description || '',
enabled: a.enabled,
provider_id: a.provider_id
public_description: a.public_description || '',
enabled: !!a.enabled,
provider_id: a.provider_id

}))
}
}

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' })
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()
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)
}
}
}

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
}

const obj = parsed as Record<string, unknown>
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) {
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<string>()
Expand Down Expand Up @@ -382,17 +483,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') {
Expand Down