Browse Source

feat: 添加环境变量配置

jiaxing.liao 1 week ago
parent
commit
70bdc9d07e

+ 245 - 0
apps/web/src/features/toolbar/AgentEnvDialog.vue

@@ -0,0 +1,245 @@
+<template>
+	<!-- 主弹窗:展示环境变量列表 -->
+	<el-dialog v-model="visible" title="环境变量" width="520px" append-to-body>
+		<div class="space-y-12px">
+			<div>环境变量用以配置智能体运行时所需的环境变量,例如 API_KEY、SECRET_KEY 等。</div>
+			<div class="flex items-center justify-between">
+				<el-button type="primary" link @click="onAddClick">
+					<span class="text-13px">+ 新增变量</span>
+				</el-button>
+			</div>
+
+			<div v-if="envList.length === 0" class="text-12px text-gray-400">
+				当前暂无环境变量,点击「新增变量」添加。
+			</div>
+
+			<div v-else class="space-y-8px max-h-360px overflow-y-auto pr-4px">
+				<el-card
+					v-for="(item, index) in envList"
+					:key="index"
+					shadow="never"
+					class="env-item-card"
+					body-class="p-6px!"
+					header-class="p-6px!"
+				>
+					<template #header>
+						<div class="flex items-center justify-between">
+							<div class="text-14px text-gray-700 font-medium flex items-center gap-4px">
+								<Icon icon="eos-icons:env" class="w-12px h-12px" color="#7839ee" />
+								{{ item.name || '未命名变量' }}
+								<span class="ml-2 text-12px text-gray-400">
+									{{ item.type === 'number' ? 'number' : 'string' }}
+								</span>
+							</div>
+							<div class="flex items-center gap-4px">
+								<el-button
+									type="primary"
+									link
+									class="mt-4px flex-shrink-0"
+									@click="onEditClick(index)"
+								>
+									编辑
+								</el-button>
+								<el-button
+									type="danger"
+									link
+									class="mt-4px flex-shrink-0"
+									@click="removeEnv(index)"
+								>
+									删除
+								</el-button>
+							</div>
+						</div>
+					</template>
+					<div class="text-12px text-gray-500 break-all">
+						{{ item.value }}
+					</div>
+				</el-card>
+			</div>
+		</div>
+	</el-dialog>
+
+	<!-- 编辑 / 新增 子弹窗:单个变量表单 -->
+	<el-dialog
+		v-model="editVisible"
+		:title="editingIndex === -1 ? '新增环境变量' : '编辑环境变量'"
+		width="520px"
+		append-to-body
+	>
+		<el-form
+			ref="editFormRef"
+			:model="editForm"
+			:rules="editFormRules"
+			label-width="80px"
+			label-position="top"
+		>
+			<el-form-item label="变量名" prop="name">
+				<el-input v-model="editForm.name" placeholder="例如 API_KEY" clearable />
+			</el-form-item>
+
+			<el-form-item label="变量类型" prop="type">
+				<el-select v-model="editForm.type" placeholder="请选择变量类型" class="w-full">
+					<el-option label="String" value="string" />
+					<el-option label="Number" value="number" />
+				</el-select>
+			</el-form-item>
+
+			<el-form-item label="变量值" prop="value">
+				<el-input
+					v-model="editForm.value"
+					:type="editForm.type === 'number' ? 'number' : 'text'"
+					placeholder="请输入变量值"
+					clearable
+				/>
+			</el-form-item>
+		</el-form>
+
+		<template #footer>
+			<div class="flex justify-end gap-8px">
+				<el-button @click="cancelEdit">取消</el-button>
+				<el-button type="primary" @click="saveEdit">保存</el-button>
+			</div>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, ref, watch } from 'vue'
+import { Icon } from '@repo/ui'
+import type { FormInstance } from 'element-plus'
+
+interface AgentEnvVar {
+	name: string
+	value: string
+	type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+}
+
+const props = defineProps<{
+	modelValue: boolean
+	/** 当前的环境变量列表,可选(预填用) */
+	value?: AgentEnvVar[]
+}>()
+
+const emit = defineEmits<{
+	(e: 'update:modelValue', value: boolean): void
+	(e: 'change', value: AgentEnvVar[]): void
+}>()
+
+const visible = computed({
+	get() {
+		return props.modelValue
+	},
+	set(val: boolean) {
+		emit('update:modelValue', val)
+	}
+})
+
+const defaultItem: AgentEnvVar = {
+	name: '',
+	type: 'string',
+	value: ''
+}
+
+const envList = ref<AgentEnvVar[]>([])
+
+// 子弹窗编辑状态
+const editVisible = ref(false)
+const editingIndex = ref<number>(-1)
+const editForm = reactive<AgentEnvVar>({ ...defaultItem })
+const editFormRef = ref<FormInstance>()
+const editFormRules = {
+	// 名称不能为空且不能重复
+	name: [
+		{
+			required: true,
+			trigger: 'blur',
+			validator: (_rule: any, value: string, callback: any) => {
+				if (!value) {
+					callback(new Error('请输入变量名'))
+				}
+				if (envList.value.some((item) => item.name === value)) {
+					callback(new Error('变量名不能重复'))
+				}
+				callback()
+			}
+		}
+	],
+	type: [{ required: true, message: '请选择变量类型', trigger: 'blur' }],
+	value: [{ required: true, message: '请输入变量值', trigger: 'blur' }]
+}
+
+watch(
+	() => props.value,
+	(val) => {
+		if (Array.isArray(val) && val.length > 0) {
+			envList.value = val.map((item) => ({
+				name: item.name,
+				type: item.type,
+				value: item.value ?? ''
+			}))
+		} else {
+			envList.value = []
+		}
+	},
+	{ immediate: true, deep: false }
+)
+
+function onAddClick() {
+	editingIndex.value = -1
+	editForm.name = ''
+	editForm.type = 'string'
+	editForm.value = ''
+	editVisible.value = true
+}
+
+function onEditClick(index: number) {
+	const target = envList.value[index]
+	editingIndex.value = index
+	editForm.name = target?.name ?? ''
+	editForm.type = target?.type ?? 'string'
+	editForm.value = target?.value ?? ''
+	editVisible.value = true
+}
+
+function cancelEdit() {
+	editVisible.value = false
+}
+
+async function saveEdit() {
+	if (!editFormRef.value) return
+	const isValid = await editFormRef.value.validate()
+	if (!isValid) return
+
+	const normalized: AgentEnvVar = {
+		name: editForm.name.trim(),
+		type: editForm.type,
+		value: editForm.value ?? ''
+	}
+
+	// 不允许空变量名
+	if (!normalized.name) {
+		editVisible.value = false
+		return
+	}
+
+	if (editingIndex.value === -1) {
+		envList.value.push({ ...normalized })
+	} else if (envList.value[editingIndex.value]) {
+		envList.value[editingIndex.value] = { ...normalized }
+	}
+
+	emit('change', envList.value)
+	editVisible.value = false
+}
+
+function removeEnv(index: number) {
+	envList.value.splice(index, 1)
+	emit('change', envList.value)
+}
+</script>
+
+<style scoped>
+.env-item-card:hover {
+	border-color: #e5e7eb;
+}
+</style>

+ 49 - 1
apps/web/src/features/toolbar/index.vue

@@ -16,15 +16,63 @@
 				@click="$emit('create:node', 'stickyNote')"
 			/>
 		</el-tooltip>
+
 		<el-tooltip content="运行" placement="left">
-			<IconButton icon="lucide:play" icon-color="#fff" type="success" square />
+			<IconButton
+				icon="lucide:play"
+				icon-color="#fff"
+				type="success"
+				square
+				@click="$emit('run')"
+			/>
+		</el-tooltip>
+
+		<el-tooltip content="环境变量" placement="left">
+			<IconButton icon="eos-icons:env" icon-color="#666" square @click="showEnvDialog = true" />
 		</el-tooltip>
+
+		<AgentEnvDialog v-model="showEnvDialog" @change="handleEnvChange" :value="envVars" />
 	</div>
 </template>
 
 <script setup lang="ts">
 import { IconButton } from '@repo/ui'
+import { ref } from 'vue'
 import NodeLibary from '@/features/nodeLibary/index.vue'
+import AgentEnvDialog from './AgentEnvDialog.vue'
+
+const props = defineProps<{
+	envVars: {
+		name: string
+		value: string
+		type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+	}[]
+}>()
+
+const emit = defineEmits<{
+	(
+		e: 'changeEnvVars',
+		value: {
+			name: string
+			value: string
+			type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+		}[]
+	): void
+	(e: 'run'): void
+	(e: 'create:node', value: { type: string } | string): void
+}>()
+
+const showEnvDialog = ref(false)
+
+function handleEnvChange(
+	payload: {
+		name: string
+		value: string
+		type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+	}[]
+) {
+	emit('changeEnvVars', payload)
+}
 </script>
 
 <style scoped></style>

+ 4 - 2
apps/web/src/style.css

@@ -193,7 +193,7 @@ button:focus-visible {
 
 .el-dialog__footer {
 	padding: var(--el-dialog-padding-primary) !important;
-	padding-top: 0 !important;
+	/* padding-top: 0 !important; */
 	text-align: right;
 	box-sizing: border-box;
 	background-color: var(--bg-base);
@@ -471,7 +471,9 @@ h6,
 }
 
 #nprogress .peg {
-	box-shadow: 0 0 10px var(--el-color-primary), 0 0 5px var(--el-color-primary) !important;
+	box-shadow:
+		0 0 10px var(--el-color-primary),
+		0 0 5px var(--el-color-primary) !important;
 }
 
 #nprogress .spinner-icon {

+ 25 - 1
apps/web/src/views/Editor.vue

@@ -64,7 +64,12 @@
 						@dragleave="onDragLeave"
 						class="bg-#f5f5f5"
 					>
-						<Toolbar @create:node="handleNodeCreate" @run="handleRunSelectedNode" />
+						<Toolbar
+							@create:node="handleNodeCreate"
+							@run="handleRunSelectedNode"
+							:env-vars="workflow?.env_variables || []"
+							@change-env-vars="handleChangeEnvVars"
+						/>
 					</Workflow>
 				</div>
 				<RunWorkflow v-model:visible="runVisible" @run="handleRunSelectedNode" />
@@ -729,6 +734,25 @@ const handleDeleteEdge = async (connection: Connection) => {
 	}
 }
 
+/**
+ * 修改环境变量
+ */
+const handleChangeEnvVars = async (
+	envVars: {
+		name: string
+		value: string
+		type: 'string' | 'number' | 'boolean' | 'object' | 'array'
+	}[]
+) => {
+	const response = await agent.postAgentDoSaveAgentVariables({
+		appAgentId: workflow.value.id,
+		conversation_variables: [],
+		env_variables: envVars
+	})
+	handleApiResult(response, '环境变量已保存', '保存环境变量失败')
+	await loadAgentWorkflow(workflow.value.id)
+}
+
 onBeforeUnmount(() => {
 	if (saveAgentTimer.value) window.clearTimeout(saveAgentTimer.value)
 	if (saveVarsTimer.value) window.clearTimeout(saveVarsTimer.value)