feat: i18n

This commit is contained in:
Soulter
2026-02-01 22:04:44 +08:00
parent f66edc8d45
commit 382aaaf053
12 changed files with 437 additions and 120 deletions
+1 -1
View File
@@ -187,7 +187,7 @@ class FileDownloadTool(FunctionTool):
os.remove(local_path)
except Exception as e:
logger.error(f"Error removing temp file {local_path}: {e}")
return f"File downloaded successfully to {local_path} and sent to user. The file has been removed from local storage."
return f"File downloaded successfully to {local_path}"
+1 -1
View File
@@ -24,7 +24,6 @@ from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryMana
from astrbot.core.provider.entities import LLMResponse, ProviderRequest, ProviderType
from astrbot.core.provider.func_tool_manager import FunctionTool, FunctionToolManager
from astrbot.core.provider.manager import ProviderManager
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
from astrbot.core.provider.provider import (
EmbeddingProvider,
Provider,
@@ -36,6 +35,7 @@ from astrbot.core.star.filter.platform_adapter_type import (
ADAPTER_NAME_2_TYPE,
PlatformAdapterType,
)
from astrbot.core.subagent_orchestrator import SubAgentOrchestrator
from ..exceptions import ProviderNotFoundError
from .filter.command import CommandFilter
+1
View File
@@ -1,4 +1,5 @@
from datetime import datetime
from pydantic import Field
from pydantic.dataclasses import dataclass
+1 -3
View File
@@ -76,9 +76,7 @@ class CronRoute(Route):
run_at = payload.get("run_at")
if not session:
return jsonify(
Response().error("session is required").__dict__
)
return jsonify(Response().error("session is required").__dict__)
if run_once and not run_at:
return jsonify(
Response().error("run_at is required when run_once=true").__dict__
+2
View File
@@ -52,6 +52,8 @@ export class I18nLoader {
{ name: 'features/auth', path: 'features/auth.json' },
{ name: 'features/chart', path: 'features/chart.json' },
{ name: 'features/dashboard', path: 'features/dashboard.json' },
{ name: 'features/cron', path: 'features/cron.json' },
{ name: 'features/subagent', path: 'features/subagent.json' },
{ name: 'features/alkaid/index', path: 'features/alkaid/index.json' },
{ name: 'features/alkaid/knowledge-base', path: 'features/alkaid/knowledge-base.json' },
{ name: 'features/alkaid/memory', path: 'features/alkaid/memory.json' },
@@ -0,0 +1,64 @@
{
"page": {
"title": "Future Task Management",
"beta": "Beta",
"subtitle": "See scheduled tasks for AstrBot. AstrBot will wake up, run them, and deliver the results.",
"proactive": {
"supported": "Proactive delivery is available on: {platforms}",
"unsupported": "No proactive messaging platforms enabled. Turn them on in Platform settings."
}
},
"actions": {
"create": "New Task",
"refresh": "Refresh",
"delete": "Delete",
"cancel": "Cancel",
"submit": "Create"
},
"table": {
"title": "Registered Tasks",
"empty": "No tasks yet.",
"headers": {
"name": "Name",
"type": "Type",
"cron": "Cron",
"nextRun": "Next Run",
"lastRun": "Last Run",
"note": "Note",
"actions": "Actions"
},
"type": {
"once": "One-off",
"recurring": "Recurring",
"activeAgent": "Active Agent",
"workflow": "Workflow",
"unknown": "{type}"
},
"timezoneLocal": "local",
"notAvailable": "—"
},
"form": {
"title": "New Task",
"runOnce": "One-off task",
"name": "Task name",
"note": "Task description",
"cron": "Cron expression",
"cronPlaceholder": "0 9 * * *",
"runAt": "Run at",
"session": "Target session (platform_id:message_type:session_id)",
"timezone": "Timezone (optional, e.g. Asia/Shanghai)",
"enabled": "Enabled"
},
"messages": {
"loadFailed": "Failed to load tasks",
"updateFailed": "Failed to update",
"deleteSuccess": "Deleted",
"deleteFailed": "Failed to delete",
"sessionRequired": "Session is required",
"noteRequired": "Description is required",
"cronRequired": "Cron expression is required",
"runAtRequired": "Please select run time",
"createSuccess": "Created successfully",
"createFailed": "Failed to create"
}
}
@@ -0,0 +1,53 @@
{
"page": {
"title": "SubAgent Orchestration",
"beta": "Beta",
"subtitle": "The main LLM only chats and delegates; tools live on individual SubAgents."
},
"actions": {
"refresh": "Refresh",
"save": "Save",
"add": "Add SubAgent",
"delete": "Delete"
},
"switches": {
"enable": "Enable SubAgent orchestration",
"dedupe": "Deduplicate main LLM tools (hide tools duplicated by SubAgents)"
},
"description": {
"disabled": "When off: SubAgent is disabled; the main LLM mounts tools via persona rules (all by default) and calls them directly.",
"enabled": "When on: the main LLM keeps its own tools and mounts transfer_to_* delegate tools. With deduplication, tools overlapping with SubAgents are removed from the main tool set."
},
"section": {
"title": "SubAgents"
},
"cards": {
"statusEnabled": "Enabled",
"statusDisabled": "Disabled",
"unnamed": "Untitled SubAgent",
"transferPrefix": "transfer_to_{name}",
"switchLabel": "Enable",
"previewTitle": "Preview: handoff tool shown to the main LLM",
"personaChip": "Persona: {id}"
},
"form": {
"nameLabel": "Agent name (used for transfer_to_{name})",
"nameHint": "Use lowercase letters + underscores; must be globally unique.",
"providerLabel": "Chat Provider (optional)",
"providerHint": "Leave empty to follow the global default provider.",
"personaLabel": "Choose Persona",
"personaHint": "The SubAgent inherits the selected Persona's system settings and tools.",
"descriptionLabel": "Description for the main LLM (used to decide handoff)",
"descriptionHint": "Shown to the main LLM as the transfer_to_* tool description—keep it short and clear."
},
"messages": {
"loadConfigFailed": "Failed to load config",
"loadPersonaFailed": "Failed to load persona list",
"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}",
"personaMissing": "SubAgent {name} has no persona selected",
"saveSuccess": "Saved successfully",
"saveFailed": "Failed to save"
}
}
@@ -0,0 +1,64 @@
{
"page": {
"title": "未来任务管理",
"beta": "Beta",
"subtitle": "查看给 AstrBot 布置的未来任务。AstrBot 将会被自动唤醒、执行任务,然后将结果告知任务布置方。",
"proactive": {
"supported": "主动发送结果仅支持以下平台:{platforms}",
"unsupported": "暂无支持主动消息的平台,请在平台设置中开启。"
}
},
"actions": {
"create": "新建任务",
"refresh": "刷新",
"delete": "删除",
"cancel": "取消",
"submit": "创建"
},
"table": {
"title": "已注册任务",
"empty": "暂无任务。",
"headers": {
"name": "名称",
"type": "类型",
"cron": "Cron",
"nextRun": "下一次执行",
"lastRun": "最近执行",
"note": "说明",
"actions": "操作"
},
"type": {
"once": "一次性",
"recurring": "循环",
"activeAgent": "Active Agent",
"workflow": "Workflow",
"unknown": "{type}"
},
"timezoneLocal": "本地时区",
"notAvailable": "—"
},
"form": {
"title": "新建任务",
"runOnce": "一次性任务",
"name": "任务名称",
"note": "任务说明",
"cron": "Cron 表达式",
"cronPlaceholder": "0 9 * * *",
"runAt": "执行时间",
"session": "目标 session (platform_id:message_type:session_id)",
"timezone": "时区(可选,如 Asia/Shanghai",
"enabled": "启用"
},
"messages": {
"loadFailed": "获取任务失败",
"updateFailed": "更新失败",
"deleteSuccess": "已删除",
"deleteFailed": "删除失败",
"sessionRequired": "请填写 session",
"noteRequired": "请填写说明",
"cronRequired": "请填写 Cron 表达式",
"runAtRequired": "请选择执行时间",
"createSuccess": "创建成功",
"createFailed": "创建失败"
}
}
@@ -0,0 +1,53 @@
{
"page": {
"title": "SubAgent 编排",
"beta": "Beta",
"subtitle": "主 LLM 只负责聊天与分派(handoff),工具挂载在各个 SubAgent 上。"
},
"actions": {
"refresh": "刷新",
"save": "保存",
"add": "新增 SubAgent",
"delete": "删除"
},
"switches": {
"enable": "启用 SubAgent 编排",
"dedupe": "主 LLM 去重重复工具(与 SubAgent 重叠的工具将被隐藏)"
},
"description": {
"disabled": "不启动:SubAgent 关闭;主 LLM 按 persona 规则挂载工具(默认全部),并直接调用。",
"enabled": "启动:主 LLM 会保留自身工具并挂载 transfer_to_* 委派工具。若开启“去重重复工具”,与 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除。"
},
"section": {
"title": "SubAgents"
},
"cards": {
"statusEnabled": "启用",
"statusDisabled": "停用",
"unnamed": "未命名 SubAgent",
"transferPrefix": "transfer_to_{name}",
"switchLabel": "启用",
"previewTitle": "预览:主 LLM 将看到的 handoff 工具",
"personaChip": "Persona: {id}"
},
"form": {
"nameLabel": "Agent 名称(用于 transfer_to_{name}",
"nameHint": "建议使用英文小写+下划线,且全局唯一",
"providerLabel": "Chat Provider(可选)",
"providerHint": "留空表示跟随全局默认 provider。",
"personaLabel": "选择 Persona",
"personaHint": "SubAgent 将直接继承所选 Persona 的系统设定与工具。",
"descriptionLabel": "对主 LLM 的描述(用于决定是否 handoff",
"descriptionHint": "这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
},
"messages": {
"loadConfigFailed": "获取配置失败",
"loadPersonaFailed": "获取 Persona 列表失败",
"nameMissing": "存在未填写名称的 SubAgent",
"nameInvalid": "SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头",
"nameDuplicate": "SubAgent 名称重复:{name}",
"personaMissing": "SubAgent {name} 未选择 Persona",
"saveSuccess": "保存成功",
"saveFailed": "保存失败"
}
}
+10 -2
View File
@@ -25,6 +25,7 @@ import zhCNSettings from './locales/zh-CN/features/settings.json';
import zhCNAuth from './locales/zh-CN/features/auth.json';
import zhCNChart from './locales/zh-CN/features/chart.json';
import zhCNDashboard from './locales/zh-CN/features/dashboard.json';
import zhCNCron from './locales/zh-CN/features/cron.json';
import zhCNAlkaidIndex from './locales/zh-CN/features/alkaid/index.json';
import zhCNAlkaidKnowledgeBase from './locales/zh-CN/features/alkaid/knowledge-base.json';
import zhCNAlkaidMemory from './locales/zh-CN/features/alkaid/memory.json';
@@ -34,6 +35,7 @@ import zhCNKnowledgeBaseDocument from './locales/zh-CN/features/knowledge-base/d
import zhCNPersona from './locales/zh-CN/features/persona.json';
import zhCNMigration from './locales/zh-CN/features/migration.json';
import zhCNCommand from './locales/zh-CN/features/command.json';
import zhCNSubagent from './locales/zh-CN/features/subagent.json';
import zhCNErrors from './locales/zh-CN/messages/errors.json';
import zhCNSuccess from './locales/zh-CN/messages/success.json';
@@ -63,6 +65,7 @@ import enUSSettings from './locales/en-US/features/settings.json';
import enUSAuth from './locales/en-US/features/auth.json';
import enUSChart from './locales/en-US/features/chart.json';
import enUSDashboard from './locales/en-US/features/dashboard.json';
import enUSCron from './locales/en-US/features/cron.json';
import enUSAlkaidIndex from './locales/en-US/features/alkaid/index.json';
import enUSAlkaidKnowledgeBase from './locales/en-US/features/alkaid/knowledge-base.json';
import enUSAlkaidMemory from './locales/en-US/features/alkaid/memory.json';
@@ -72,6 +75,7 @@ import enUSKnowledgeBaseDocument from './locales/en-US/features/knowledge-base/d
import enUSPersona from './locales/en-US/features/persona.json';
import enUSMigration from './locales/en-US/features/migration.json';
import enUSCommand from './locales/en-US/features/command.json';
import enUSSubagent from './locales/en-US/features/subagent.json';
import enUSErrors from './locales/en-US/messages/errors.json';
import enUSSuccess from './locales/en-US/messages/success.json';
@@ -105,6 +109,7 @@ export const translations = {
auth: zhCNAuth,
chart: zhCNChart,
dashboard: zhCNDashboard,
cron: zhCNCron,
alkaid: {
index: zhCNAlkaidIndex,
'knowledge-base': zhCNAlkaidKnowledgeBase,
@@ -117,7 +122,8 @@ export const translations = {
},
persona: zhCNPersona,
migration: zhCNMigration,
command: zhCNCommand
command: zhCNCommand,
subagent: zhCNSubagent
},
messages: {
errors: zhCNErrors,
@@ -151,6 +157,7 @@ export const translations = {
auth: enUSAuth,
chart: enUSChart,
dashboard: enUSDashboard,
cron: enUSCron,
alkaid: {
index: enUSAlkaidIndex,
'knowledge-base': enUSAlkaidKnowledgeBase,
@@ -163,7 +170,8 @@ export const translations = {
},
persona: enUSPersona,
migration: enUSMigration,
command: enUSCommand
command: enUSCommand,
subagent: enUSSubagent
},
messages: {
errors: enUSErrors,
+84 -56
View File
@@ -3,58 +3,69 @@
<div class="d-flex align-center justify-space-between mb-4">
<div>
<div class="d-flex align-center" style="gap: 8px;">
<h2 class="text-h5 font-weight-bold">未来任务管理</h2>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>Beta</v-chip>
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>{{ tm('page.beta') }}</v-chip>
</div>
<div class="text-body-2 text-medium-emphasis">
查看给 AstrBot 布置的未来任务AstrBot 将会被自动唤醒执行任务然后将结果告知任务布置方
主动发送结果仅支持以下平台
{{ tm('page.subtitle') }}
<span v-if="proactivePlatforms.length">
{{ proactivePlatforms.map((p) => `${p.display_name || p.name}(${p.id})`).join('、') }}
{{ tm('page.proactive.supported', { platforms: proactivePlatformText }) }}
</span>
<span v-else>暂无支持主动消息的平台请在平台设置中开启</span>
<span v-else>{{ tm('page.proactive.unsupported') }}</span>
</div>
</div>
<div class="d-flex align-center" style="gap: 8px;">
<v-btn variant="tonal" color="primary" @click="openCreate">新建任务</v-btn>
<v-btn variant="tonal" color="primary" :loading="loading" @click="loadJobs">刷新</v-btn>
<v-btn variant="tonal" color="primary" @click="openCreate">{{ tm('actions.create') }}</v-btn>
<v-btn variant="tonal" color="primary" :loading="loading" @click="loadJobs">{{ tm('actions.refresh') }}</v-btn>
</div>
</div>
<v-card class="rounded-lg" variant="flat">
<v-card-text>
<div class="d-flex align-center justify-space-between mb-3">
<div class="text-subtitle-1 font-weight-bold">已注册任务</div>
<div class="text-subtitle-1 font-weight-bold">{{ tm('table.title') }}</div>
</div>
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">暂无任务</v-alert>
<v-alert v-if="!jobs.length && !loading" type="info" variant="tonal">{{ tm('table.empty') }}</v-alert>
<v-data-table :items="jobs" :headers="headers" :loading="loading" item-key="job_id" density="comfortable"
class="elevation-0">
<v-data-table
:items="jobs"
:headers="headers"
:loading="loading"
item-key="job_id"
density="comfortable"
class="elevation-0"
>
<template #item.name="{ item }">
<div class="font-weight-medium">{{ item.name }}</div>
<div class="text-caption text-medium-emphasis">{{ item.description }}</div>
</template>
<template #item.type="{ item }">
<v-chip size="small" :color="item.run_once ? 'orange' : 'primary'" variant="tonal">
{{ item.run_once ? '一次性' : (item.job_type || 'active_agent') }}
{{ jobTypeLabel(item) }}
</v-chip>
</template>
<template #item.cron_expression="{ item }">
<div v-if="item.run_once">{{ formatTime(item.run_at) }}</div>
<div v-else>
<div>{{ item.cron_expression || '—' }}</div>
<div class="text-caption text-medium-emphasis">{{ item.timezone || 'local' }}</div>
<div>{{ item.cron_expression || tm('table.notAvailable') }}</div>
<div class="text-caption text-medium-emphasis">{{ item.timezone || tm('table.timezoneLocal') }}</div>
</div>
</template>
<template #item.next_run_time="{ item }">{{ formatTime(item.next_run_time) }}</template>
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
<template #item.note="{ item }">{{ item.note || '—' }}</template>
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
<template #item.actions="{ item }">
<div class="d-flex" style="gap: 8px;">
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
@change="toggleJob(item)" />
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">删除</v-btn>
<v-switch
v-model="item.enabled"
inset
density="compact"
hide-details
color="primary"
@change="toggleJob(item)"
/>
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete') }}</v-btn>
</div>
</template>
</v-data-table>
@@ -67,44 +78,44 @@
<v-dialog v-model="createDialog" max-width="560">
<v-card>
<v-card-title class="text-h6">新建任务</v-card-title>
<v-card-title class="text-h6">{{ tm('form.title') }}</v-card-title>
<v-card-text>
<v-switch v-model="newJob.run_once" label="一次性任务" inset color="primary" hide-details />
<v-text-field v-model="newJob.name" label="任务名称" variant="outlined" density="comfortable" />
<v-text-field v-model="newJob.note" label="任务说明" variant="outlined" density="comfortable" />
<v-switch v-model="newJob.run_once" :label="tm('form.runOnce')" inset color="primary" hide-details />
<v-text-field v-model="newJob.name" :label="tm('form.name')" variant="outlined" density="comfortable" />
<v-text-field v-model="newJob.note" :label="tm('form.note')" variant="outlined" density="comfortable" />
<v-text-field
v-if="!newJob.run_once"
v-model="newJob.cron_expression"
label="Cron 表达式"
placeholder="0 9 * * *"
:label="tm('form.cron')"
:placeholder="tm('form.cronPlaceholder')"
variant="outlined"
density="comfortable"
/>
<v-text-field
v-else
v-model="newJob.run_at"
label="执行时间"
:label="tm('form.runAt')"
type="datetime-local"
variant="outlined"
density="comfortable"
/>
<v-text-field
v-model="newJob.session"
label="目标 session (platform_id:message_type:session_id)"
:label="tm('form.session')"
variant="outlined"
density="comfortable"
/>
<v-text-field
v-model="newJob.timezone"
label="时区(可选,如 Asia/Shanghai"
:label="tm('form.timezone')"
variant="outlined"
density="comfortable"
/>
<v-switch v-model="newJob.enabled" label="启用" inset color="primary" hide-details />
<v-switch v-model="newJob.enabled" :label="tm('form.enabled')" inset color="primary" hide-details />
</v-card-text>
<v-card-actions class="justify-end">
<v-btn variant="text" @click="createDialog = false">取消</v-btn>
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">创建</v-btn>
<v-btn variant="text" @click="createDialog = false">{{ tm('actions.cancel') }}</v-btn>
<v-btn variant="tonal" color="primary" :loading="creating" @click="createJob">{{ tm('actions.submit') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -112,8 +123,11 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
const { tm } = useModuleI18n('features/cron')
const loading = ref(false)
const jobs = ref<any[]>([])
@@ -133,22 +147,26 @@ const newJob = ref({
const snackbar = ref({ show: false, message: '', color: 'success' })
const headers = [
{ title: '名称', key: 'name', minWidth: '200px' },
{ title: '类型', key: 'type', width: 110 },
{ title: 'Cron', key: 'cron_expression', minWidth: '160px' },
{ title: '下一次执行', key: 'next_run_time', minWidth: '160px' },
{ title: '最近执行', key: 'last_run_at', minWidth: '160px' },
{ title: '说明', key: 'note', minWidth: '220px' },
{ title: '操作', key: 'actions', width: 160, sortable: false }
]
const proactivePlatformText = computed(() =>
proactivePlatforms.value.map((p) => `${p.display_name || p.name}(${p.id})`).join(' / ')
)
const headers = computed(() => [
{ title: tm('table.headers.name'), key: 'name', minWidth: '200px' },
{ title: tm('table.headers.type'), key: 'type', width: 110 },
{ title: tm('table.headers.cron'), key: 'cron_expression', minWidth: '160px' },
{ title: tm('table.headers.nextRun'), key: 'next_run_time', minWidth: '160px' },
{ title: tm('table.headers.lastRun'), key: 'last_run_at', minWidth: '160px' },
{ title: tm('table.headers.note'), key: 'note', minWidth: '220px' },
{ title: tm('table.headers.actions'), key: 'actions', width: 160, sortable: false }
])
function toast(message: string, color: 'success' | 'error' | 'warning' = 'success') {
snackbar.value = { show: true, message, color }
}
function formatTime(val: any): string {
if (!val) return '—'
if (!val) return tm('table.notAvailable')
try {
return new Date(val).toLocaleString()
} catch (e) {
@@ -156,6 +174,16 @@ function formatTime(val: any): string {
}
}
function jobTypeLabel(item: any): string {
if (item.run_once) return tm('table.type.once')
const type = item.job_type || 'active_agent'
const map: Record<string, string> = {
active_agent: tm('table.type.activeAgent'),
workflow: tm('table.type.workflow')
}
return map[type] || tm('table.type.unknown', { type })
}
async function loadJobs() {
loading.value = true
try {
@@ -163,10 +191,10 @@ async function loadJobs() {
if (res.data.status === 'ok') {
jobs.value = Array.isArray(res.data.data) ? res.data.data : []
} else {
toast(res.data.message || '获取任务失败', 'error')
toast(res.data.message || tm('messages.loadFailed'), 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '获取任务失败', 'error')
toast(e?.response?.data?.message || tm('messages.loadFailed'), 'error')
} finally {
loading.value = false
}
@@ -193,11 +221,11 @@ async function toggleJob(job: any) {
try {
const res = await axios.patch(`/api/cron/jobs/${job.job_id}`, { enabled: job.enabled })
if (res.data.status !== 'ok') {
toast(res.data.message || '更新失败', 'error')
toast(res.data.message || tm('messages.updateFailed'), 'error')
await loadJobs()
}
} catch (e: any) {
toast(e?.response?.data?.message || '更新失败', 'error')
toast(e?.response?.data?.message || tm('messages.updateFailed'), 'error')
await loadJobs()
}
}
@@ -206,13 +234,13 @@ async function deleteJob(job: any) {
try {
const res = await axios.delete(`/api/cron/jobs/${job.job_id}`)
if (res.data.status === 'ok') {
toast('已删除')
toast(tm('messages.deleteSuccess'))
jobs.value = jobs.value.filter((j) => j.job_id !== job.job_id)
} else {
toast(res.data.message || '删除失败', 'error')
toast(res.data.message || tm('messages.deleteFailed'), 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '删除失败', 'error')
toast(e?.response?.data?.message || tm('messages.deleteFailed'), 'error')
}
}
@@ -236,19 +264,19 @@ function resetNewJob() {
async function createJob() {
if (!newJob.value.session) {
toast('请填写 session', 'warning')
toast(tm('messages.sessionRequired'), 'warning')
return
}
if (!newJob.value.note) {
toast('请填写说明', 'warning')
toast(tm('messages.noteRequired'), 'warning')
return
}
if (!newJob.value.run_once && !newJob.value.cron_expression) {
toast('请填写 Cron 表达式', 'warning')
toast(tm('messages.cronRequired'), 'warning')
return
}
if (newJob.value.run_once && !newJob.value.run_at) {
toast('请选择执行时间', 'warning')
toast(tm('messages.runAtRequired'), 'warning')
return
}
creating.value = true
@@ -256,15 +284,15 @@ async function createJob() {
const payload: any = { ...newJob.value }
const res = await axios.post('/api/cron/jobs', payload)
if (res.data.status === 'ok') {
toast('创建成功')
toast(tm('messages.createSuccess'))
createDialog.value = false
resetNewJob()
await loadJobs()
} else {
toast(res.data.message || '创建失败', 'error')
toast(res.data.message || tm('messages.createFailed'), 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '创建失败', 'error')
toast(e?.response?.data?.message || tm('messages.createFailed'), 'error')
} finally {
creating.value = false
}
+103 -57
View File
@@ -3,17 +3,17 @@
<div class="d-flex align-center justify-space-between mb-4">
<div>
<div class="d-flex align-center" style="gap: 8px;">
<h2 class="text-h5 font-weight-bold">SubAgent 编排</h2>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>Beta</v-chip>
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>{{ tm('page.beta') }}</v-chip>
</div>
<div class="text-body-2 text-medium-emphasis">
LLM 只负责聊天与分派handoff工具挂载在各个 SubAgent
{{ tm('page.subtitle') }}
</div>
</div>
<div class="d-flex align-center" style="gap: 8px;">
<v-btn variant="tonal" color="primary" :loading="loading" @click="reload">刷新</v-btn>
<v-btn variant="flat" color="primary" :loading="saving" @click="save">保存</v-btn>
<v-btn variant="tonal" color="primary" :loading="loading" @click="reload">{{ tm('actions.refresh') }}</v-btn>
<v-btn variant="flat" color="primary" :loading="saving" @click="save">{{ tm('actions.save') }}</v-btn>
</div>
</div>
@@ -21,30 +21,36 @@
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-switch v-model="cfg.main_enable" label="启用 SubAgent 编排"
inset color="primary" hide-details density="comfortable" />
<v-switch
v-model="cfg.main_enable"
:label="tm('switches.enable')"
inset
color="primary"
hide-details
density="comfortable"
/>
</v-col>
<v-col cols="12" md="6">
<v-switch v-model="cfg.remove_main_duplicate_tools" :disabled="!cfg.main_enable"
label="主 LLM 去重重复工具(与 SubAgent 重叠的工具将被隐藏)"
inset color="primary" hide-details density="comfortable" />
<v-switch
v-model="cfg.remove_main_duplicate_tools"
:disabled="!cfg.main_enable"
:label="tm('switches.dedupe')"
inset
color="primary"
hide-details
density="comfortable"
/>
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-1">
<div v-if="!cfg.main_enable">
不启动SubAgent 关闭 LLM persona 规则挂载工具默认全部并直接调用
</div>
<div v-else>
启动 LLM 会保留自身工具并挂载 transfer_to_* 委派工具
若开启去重重复工具 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除
</div>
{{ mainStateDescription }}
</div>
<div class="d-flex align-center justify-space-between mt-6 mb-2">
<div class="text-subtitle-1 font-weight-bold">SubAgents</div>
<div class="text-subtitle-1 font-weight-bold">{{ tm('section.title') }}</div>
<v-btn size="small" variant="tonal" color="primary" @click="addAgent">
新增 SubAgent
{{ tm('actions.add') }}
</v-btn>
</div>
@@ -54,23 +60,31 @@
<div class="subagent-panel-title">
<div class="subagent-title-left">
<v-chip :color="agent.enabled ? 'success' : 'grey'" size="small" variant="tonal">
{{ agent.enabled ? '启用' : '停用' }}
{{ agent.enabled ? tm('cards.statusEnabled') : tm('cards.statusDisabled') }}
</v-chip>
<div class="subagent-title-text">
<div class="subagent-title-name">{{ agent.name || '未命名 SubAgent' }}</div>
<div class="subagent-title-sub">transfer_to_{{ agent.name || '...' }}</div>
<div class="subagent-title-name">{{ agent.name || tm('cards.unnamed') }}</div>
<div class="subagent-title-sub">
{{ tm('cards.transferPrefix', { name: agent.name || '...' }) }}
</div>
</div>
</div>
<div class="subagent-title-right">
<v-switch v-model="agent.enabled" inset color="primary" hide-details class="subagent-enabled-inline"
@click.stop>
<template #label>启用</template>
<v-switch
v-model="agent.enabled"
inset
color="primary"
hide-details
class="subagent-enabled-inline"
@click.stop
>
<template #label>{{ tm('cards.switchLabel') }}</template>
</v-switch>
<v-btn size="small" variant="text" color="error" @click.stop="removeAgent(idx)">
删除
{{ tm('actions.delete') }}
</v-btn>
</div>
</div>
@@ -79,42 +93,69 @@
<v-expansion-panel-text>
<v-row class="subagent-grid">
<v-col cols="12" md="5">
<v-text-field v-model="agent.name" label="Agent 名称(用于 transfer_to_{name}" variant="outlined"
density="comfortable" hint="建议使用英文小写+下划线,且全局唯一" persistent-hint />
<v-text-field
v-model="agent.name"
:label="tm('form.nameLabel')"
variant="outlined"
density="comfortable"
:hint="tm('form.nameHint')"
persistent-hint
/>
</v-col>
<v-col cols="12" md="7" class="subagent-actions">
<ProviderSelector v-model="agent.provider_id" provider-type="chat_completion"
label="Chat Provider(可选)" hint="留空表示跟随全局默认 provider。" persistent-hint clearable
class="subagent-provider" />
<ProviderSelector
v-model="agent.provider_id"
provider-type="chat_completion"
:label="tm('form.providerLabel')"
:hint="tm('form.providerHint')"
persistent-hint
clearable
class="subagent-provider"
/>
</v-col>
<v-col cols="12" md="6">
<v-autocomplete v-model="agent.persona_id" :items="personaOptions" item-title="title"
item-value="value" label="选择 Persona" variant="outlined" density="comfortable" clearable
:loading="personaLoading" :disabled="personaLoading" hint="SubAgent 将直接继承所选 Persona 的系统设定与工具。"
persistent-hint />
<v-autocomplete
v-model="agent.persona_id"
:items="personaOptions"
item-title="title"
item-value="value"
:label="tm('form.personaLabel')"
variant="outlined"
density="comfortable"
clearable
:loading="personaLoading"
:disabled="personaLoading"
:hint="tm('form.personaHint')"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="agent.public_description" label="对主 LLM 的描述(用于决定是否 handoff" variant="outlined"
density="comfortable" hint="这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。" persistent-hint />
<v-text-field
v-model="agent.public_description"
:label="tm('form.descriptionLabel')"
variant="outlined"
density="comfortable"
:hint="tm('form.descriptionHint')"
persistent-hint
/>
</v-col>
</v-row>
<div class="mt-3">
<div class="text-caption text-medium-emphasis">预览 LLM 将看到的 handoff 工具</div>
<div class="text-caption text-medium-emphasis">{{ tm('cards.previewTitle') }}</div>
<div class="d-flex align-center" style="gap: 8px; flex-wrap: wrap;">
<v-chip size="small" variant="outlined" color="primary">
transfer_to_{{ agent.name || '...' }}
{{ tm('cards.transferPrefix', { name: agent.name || '...' }) }}
</v-chip>
<v-chip size="small" variant="tonal" color="secondary" v-if="agent.persona_id">
Persona: {{ agent.persona_id }}
{{ tm('cards.personaChip', { id: agent.persona_id }) }}
</v-chip>
</div>
</div>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
@@ -125,9 +166,10 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import axios from 'axios'
import ProviderSelector from '@/components/shared/ProviderSelector.vue'
import { useModuleI18n } from '@/i18n/composables'
type SubAgentItem = {
__key: string
@@ -144,6 +186,8 @@ type SubAgentConfig = {
agents: SubAgentItem[]
}
const { tm } = useModuleI18n('features/subagent')
const loading = ref(false)
const saving = ref(false)
@@ -166,6 +210,10 @@ const cfg = ref<SubAgentConfig>({
const personaOptions = ref<{ title: string; value: string }[]>([])
const personaLoading = ref(false)
const mainStateDescription = computed(() =>
cfg.value.main_enable ? tm('description.enabled') : tm('description.disabled')
)
function normalizeConfig(raw: any): SubAgentConfig {
const main_enable = !!raw?.main_enable
const remove_main_duplicate_tools = !!raw?.remove_main_duplicate_tools
@@ -176,15 +224,14 @@ function normalizeConfig(raw: any): SubAgentConfig {
const persona_id = (a?.persona_id ?? '').toString()
const public_description = (a?.public_description ?? '').toString()
const enabled = a?.enabled !== false
const provider_id = (a?.provider_id ?? undefined) as (string | undefined)
const provider_id = (a?.provider_id ?? undefined) as string | undefined
return {
__key: `${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`,
name,
persona_id,
public_description,
enabled
,
enabled,
provider_id
}
})
@@ -199,10 +246,10 @@ async function loadConfig() {
if (res.data.status === 'ok') {
cfg.value = normalizeConfig(res.data.data)
} else {
toast(res.data.message || '获取配置失败', 'error')
toast(res.data.message || tm('messages.loadConfigFailed'), 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '获取配置失败', 'error')
toast(e?.response?.data?.message || tm('messages.loadConfigFailed'), 'error')
} finally {
loading.value = false
}
@@ -220,7 +267,7 @@ async function loadPersonas() {
}))
}
} catch (e: any) {
toast(e?.response?.data?.message || '获取 Persona 列表失败', 'error')
toast(e?.response?.data?.message || tm('messages.loadPersonaFailed'), 'error')
} finally {
personaLoading.value = false
}
@@ -247,20 +294,20 @@ function validateBeforeSave(): boolean {
for (const a of cfg.value.agents) {
const name = (a.name || '').trim()
if (!name) {
toast('存在未填写名称的 SubAgent', 'warning')
toast(tm('messages.nameMissing'), 'warning')
return false
}
if (!nameRe.test(name)) {
toast('SubAgent 名称不合法:仅允许英文小写字母/数字/下划线,且需以字母开头', 'warning')
toast(tm('messages.nameInvalid'), 'warning')
return false
}
if (seen.has(name)) {
toast(`SubAgent 名称重复:${name}`, 'warning')
toast(tm('messages.nameDuplicate', { name }), 'warning')
return false
}
seen.add(name)
if (!a.persona_id) {
toast(`SubAgent ${name} 未选择 Persona`, 'warning')
toast(tm('messages.personaMissing', { name }), 'warning')
return false
}
}
@@ -271,11 +318,10 @@ async function save() {
if (!validateBeforeSave()) return
saving.value = true
try {
// Strip UI-only fields
const payload = {
main_enable: cfg.value.main_enable,
remove_main_duplicate_tools: cfg.value.remove_main_duplicate_tools,
agents: cfg.value.agents.map(a => ({
agents: cfg.value.agents.map((a) => ({
name: a.name,
persona_id: a.persona_id,
public_description: a.public_description,
@@ -286,12 +332,12 @@ async function save() {
const res = await axios.post('/api/subagent/config', payload)
if (res.data.status === 'ok') {
toast(res.data.message || '保存成功', 'success')
toast(res.data.message || tm('messages.saveSuccess'), 'success')
} else {
toast(res.data.message || '保存失败', 'error')
toast(res.data.message || tm('messages.saveFailed'), 'error')
}
} catch (e: any) {
toast(e?.response?.data?.message || '保存失败', 'error')
toast(e?.response?.data?.message || tm('messages.saveFailed'), 'error')
} finally {
saving.value = false
}