feat: Agent 环境变量功能 - 新增环境变量配置组件与工具注册优化
- 新增 AgentEnvSettingsCard.vue 环境变量配置卡片 - 新增 agent_env_api.js API 层 - 优化 McpEnvEditor.vue 和 SettingsModal.vue 集成 - 更新 toolRegistry.js 工具注册逻辑
This commit is contained in:
parent
b9f0579ef2
commit
1f2bdf3158
9
web/src/apis/agent_env_api.js
Normal file
9
web/src/apis/agent_env_api.js
Normal 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 || {} })
|
||||
}
|
||||
185
web/src/components/AgentEnvSettingsCard.vue
Normal file
185
web/src/components/AgentEnvSettingsCard.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user