feat: Agent 环境变量功能 - 新增环境变量配置组件与工具注册优化

- 新增 AgentEnvSettingsCard.vue 环境变量配置卡片
- 新增 agent_env_api.js API 层
- 优化 McpEnvEditor.vue 和 SettingsModal.vue 集成
- 更新 toolRegistry.js 工具注册逻辑
This commit is contained in:
Wenjie Zhang 2026-05-21 22:09:12 +08:00
parent b9f0579ef2
commit 1f2bdf3158
5 changed files with 228 additions and 8 deletions

View File

@ -0,0 +1,9 @@
import { apiGet, apiPut } from './base'
const AGENT_ENV_PATH = '/api/user/agent-env'
export const agentEnvApi = {
get: () => apiGet(AGENT_ENV_PATH),
update: (env) => apiPut(AGENT_ENV_PATH, { env: env || {} })
}

View File

@ -0,0 +1,185 @@
<template>
<div class="agent-env-settings">
<div class="header-section">
<div class="header-content">
<div class="section-title">沙盒环境变量</div>
<p class="section-description">
配置当前用户的 Agent 沙盒环境变量新建沙盒时会注入这些变量并覆盖同名全局 sandbox.env
</p>
</div>
<div class="header-actions">
<a-button class="lucide-icon-btn" :loading="loading" @click="loadAgentEnv">
<template #icon><RefreshCw :size="16" :class="{ spin: loading }" /></template>
刷新
</a-button>
<a-button type="primary" :loading="saving" @click="saveAgentEnv">保存</a-button>
</div>
</div>
<div class="env-tip">
保存后仅对新建沙盒生效已运行沙盒不会热更新
</div>
<a-spin :spinning="loading">
<McpEnvEditor :modelValue="draftEnv" @update:modelValue="updateDraftEnv" />
</a-spin>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { message } from 'ant-design-vue'
import { RefreshCw } from 'lucide-vue-next'
import { agentEnvApi } from '@/apis/agent_env_api'
import McpEnvEditor from '@/components/McpEnvEditor.vue'
const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
const MAX_ENV_COUNT = 200
const MAX_ENV_KEY_LENGTH = 128
const MAX_ENV_VALUE_LENGTH = 32768
const loading = ref(false)
const saving = ref(false)
const draftEnv = ref({})
const lastSavedEnv = ref({})
const normalizeEnv = (env) => {
if (!env || typeof env !== 'object' || Array.isArray(env)) {
return {}
}
return Object.fromEntries(
Object.entries(env)
.map(([key, value]) => [key.trim(), value == null ? '' : String(value)])
.filter(([key]) => key)
)
}
const isSameEnv = (left, right) => {
const leftEntries = Object.entries(left)
const rightEntries = Object.entries(right)
if (leftEntries.length !== rightEntries.length) return false
return leftEntries.every(([key, value]) => right[key] === value)
}
const updateDraftEnv = (value) => {
const nextEnv = normalizeEnv(value)
if (!isSameEnv(draftEnv.value, nextEnv)) {
draftEnv.value = nextEnv
}
}
const validateEnv = (env) => {
const entries = Object.entries(env)
if (entries.length > MAX_ENV_COUNT) {
message.error(`环境变量数量不能超过 ${MAX_ENV_COUNT}`)
return false
}
for (const [key, value] of entries) {
if (key.length > MAX_ENV_KEY_LENGTH) {
message.error(`环境变量名长度不能超过 ${MAX_ENV_KEY_LENGTH}`)
return false
}
if (!ENV_KEY_PATTERN.test(key)) {
message.error(`环境变量名 ${key} 格式不正确`)
return false
}
if (value.length > MAX_ENV_VALUE_LENGTH) {
message.error(`环境变量 ${key} 的值过长`)
return false
}
}
return true
}
const loadAgentEnv = async () => {
loading.value = true
try {
const res = await agentEnvApi.get()
const env = normalizeEnv(res.env)
draftEnv.value = env
lastSavedEnv.value = env
} catch (error) {
message.error(error.message || '加载环境变量失败')
} finally {
loading.value = false
}
}
const saveAgentEnv = async () => {
const env = normalizeEnv(draftEnv.value)
if (!validateEnv(env)) return
if (isSameEnv(env, lastSavedEnv.value)) {
message.info('环境变量未变化')
return
}
saving.value = true
try {
await agentEnvApi.update(env)
draftEnv.value = env
lastSavedEnv.value = env
message.success('环境变量已保存')
} catch (error) {
message.error(error.message || '保存环境变量失败')
} finally {
saving.value = false
}
}
onMounted(loadAgentEnv)
</script>
<style lang="less" scoped>
.agent-env-settings {
.header-section {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 16px;
margin-bottom: 12px;
@media (max-width: 760px) {
align-items: stretch;
flex-direction: column;
}
}
.header-content {
flex: 1;
min-width: 0;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.env-tip {
margin-bottom: 14px;
padding: 10px 12px;
border-radius: 10px;
background: var(--main-10);
border: 1px solid var(--main-300);
color: var(--main-700);
font-size: 13px;
line-height: 1.5;
}
}
:deep(.spin) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -13,7 +13,7 @@
删除
</a-button>
</div>
<a-button size="small" @click="addRow">
<a-button @click="addRow" class="add-env">
<template #icon><PlusOutlined /></template>
添加变量
</a-button>
@ -141,5 +141,9 @@ watch(
flex: 1;
}
}
button.add-env {
width: fit-content;
}
}
</style>

View File

@ -55,6 +55,15 @@
<Users class="icon" :size="18" />
<span>部门管理</span>
</div>
<div
class="sider-item"
:class="{ activesec: activeTab === 'agentEnv' }"
@click="activeTab = 'agentEnv'"
v-if="userStore.isLoggedIn"
>
<SquareTerminal class="icon" :size="18" />
<span>环境变量</span>
</div>
</div>
<div v-if="showStarCard" class="settings-star-card">
@ -101,6 +110,14 @@
>
账户设置
</div>
<div
class="nav-item"
:class="{ active: activeTab === 'agentEnv' }"
@click="activeTab = 'agentEnv'"
v-if="userStore.isLoggedIn"
>
沙盒环境变量
</div>
<div
class="nav-item"
:class="{ active: activeTab === 'base' }"
@ -134,6 +151,10 @@
<AccountSettingsComponent />
</div>
<div v-if="activeTab === 'agentEnv' && userStore.isLoggedIn">
<AgentEnvSettingsCard />
</div>
<div v-show="activeTab === 'base'" v-if="userStore.isAdmin">
<BasicSettingsSection />
</div>
@ -155,8 +176,9 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useUserStore } from '@/stores/user'
import { CircleUser, ExternalLink, Settings, Star, User, Users, X } from 'lucide-vue-next'
import { CircleUser, ExternalLink, Settings, Star, SquareTerminal, User, Users, X } from 'lucide-vue-next'
import AccountSettingsComponent from '@/components/AccountSettingsComponent.vue'
import AgentEnvSettingsCard from '@/components/AgentEnvSettingsCard.vue'
import BasicSettingsSection from '@/components/BasicSettingsSection.vue'
import UserManagementComponent from '@/components/UserManagementComponent.vue'
import DepartmentManagementComponent from '@/components/DepartmentManagementComponent.vue'
@ -188,7 +210,7 @@ const visible = computed({
const availableTabs = computed(() => {
const tabs = []
if (userStore.isLoggedIn) tabs.push('account')
if (userStore.isLoggedIn) tabs.push('account', 'agentEnv')
if (userStore.isAdmin) tabs.push('base', 'user')
if (userStore.isSuperAdmin) tabs.push('department')
return tabs

View File

@ -14,16 +14,16 @@ import {
HelpCircle,
Image,
Network,
Terminal
SquareTerminal
} from 'lucide-vue-next'
export const TOOL_ICON_MAP = {
ask_user_question: HelpCircle,
bash: Terminal,
bash: SquareTerminal,
calculator: Calculator,
cmd: Terminal,
cmd: SquareTerminal,
edit_file: FilePen,
execute: Terminal,
execute: SquareTerminal,
find_kb_document: FolderSearch,
get_mindmap: Network,
glob: FolderSearch,
@ -39,7 +39,7 @@ export const TOOL_ICON_MAP = {
query_kb: BookOpen,
read_file: FileText,
replace: FilePen,
run_shell_command: Terminal,
run_shell_command: SquareTerminal,
search_file_content: FolderSearch,
task: Bot,
tavily_search: Globe,