jiaxing.liao 5 дней назад
Родитель
Сommit
edb506b13c

+ 99 - 2
apps/web/src/features/editorFooter/index.vue

@@ -8,23 +8,120 @@
 				<span class="text-12px">日志</span>
 				<IconButton :icon="open ? 'lucide:chevron-down' : 'lucide:chevron-up'" link></IconButton>
 			</div>
-			<div class="flex-1 text-12px p-12px">日志内容...</div>
+			<div class="flex-1 text-12px p-12px overflow-auto">
+				<div v-if="executions.length === 0" class="text-gray-400">
+					暂无运行日志,点击运行节点后查看。
+				</div>
+				<el-table v-else :data="executions" row-key="runnerKey" size="small" border class="w-full">
+					<el-table-column type="expand" width="48">
+						<template #default="scope">
+							<div class="p-8px">
+								<el-table
+									:data="scope.row.nodes"
+									row-key="nodeId"
+									size="small"
+									border
+									class="w-full"
+								>
+									<el-table-column prop="nodeName" label="节点名称" min-width="160" />
+									<el-table-column prop="nodeType" label="类型" width="120" />
+									<el-table-column label="状态" width="100">
+										<template #default="{ row }">
+											<el-tag :type="statusTagType(row.status)" size="small">
+												{{ statusText(row.status) }}
+											</el-tag>
+										</template>
+									</el-table-column>
+									<el-table-column prop="lastUpdateTime" label="最后时间" width="180" />
+									<el-table-column label="详情" min-width="260">
+										<template #default="{ row }">
+											<el-tabs type="border-card" class="w-full">
+												<el-tab-pane label="输入">
+													<pre
+														class="bg-#f7f7f7 rounded p-6px whitespace-pre-wrap break-all max-h-160px overflow-auto"
+														>{{ formatJson(row.track?.input_variable) }}
+													</pre
+													>
+												</el-tab-pane>
+												<el-tab-pane label="输出">
+													<pre
+														class="bg-#f7f7f7 rounded p-6px whitespace-pre-wrap break-all max-h-160px overflow-auto"
+														>{{ formatJson(row.track?.output_variable) }}
+													</pre
+													>
+												</el-tab-pane>
+											</el-tabs>
+										</template>
+									</el-table-column>
+								</el-table>
+							</div>
+						</template>
+					</el-table-column>
+					<el-table-column type="index" label="#" width="48" />
+					<el-table-column prop="runnerKey" label="运行ID" min-width="220" />
+					<el-table-column label="状态" width="100">
+						<template #default="{ row }">
+							<el-tag :type="statusTagType(row.status)" size="small">
+								{{ statusText(row.status) }}
+							</el-tag>
+						</template>
+					</el-table-column>
+					<el-table-column prop="startedAt" label="开始时间" width="180" />
+					<el-table-column prop="finishedAt" label="结束时间" width="180" />
+					<el-table-column label="节点数" width="80">
+						<template #default="{ row }">
+							{{ row.nodes?.length || 0 }}
+						</template>
+					</el-table-column>
+				</el-table>
+			</div>
 		</div>
 	</div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { IconButton } from '@repo/ui'
+import { useRunnerStore, type NodeStatus, type RunnerStatus } from '@/store/modules/runner.store'
 
 const emit = defineEmits<{
 	toggle: [open: boolean]
 }>()
 
 const open = ref(false)
+const runnerStore = useRunnerStore()
+
+const executions = computed(() => runnerStore.executions)
 
 const onClick = () => {
 	open.value = !open.value
 	emit('toggle', open.value)
 }
+
+const formatJson = (value: unknown) => {
+	if (value === null || value === undefined) return '-'
+	try {
+		return JSON.stringify(value, null, 2)
+	} catch {
+		return String(value)
+	}
+}
+
+const statusText = (status: NodeStatus | RunnerStatus) => {
+	if (status === 'running') return '运行中'
+	if (status === 'success') return '运行成功'
+	if (status === 'finished') return '运行完成'
+	if (status === 'failed') return '运行失败'
+	if (status === 'error') return '运行异常'
+	return '就绪'
+}
+
+const statusTagType = (
+	status: NodeStatus | RunnerStatus
+): 'info' | 'success' | 'warning' | 'danger' => {
+	if (status === 'running') return 'warning'
+	if (status === 'success' || status === 'finished') return 'success'
+	if (status === 'failed' || status === 'error') return 'danger'
+	return 'info'
+}
 </script>

+ 190 - 0
apps/web/src/features/setter/NodeLog.vue

@@ -0,0 +1,190 @@
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { useRunnerStore, type RunnerNodeState } from '@/store/modules/runner.store'
+import type { IWorkflowNode } from '@repo/workflow'
+
+interface Props {
+	node: IWorkflowNode | undefined
+}
+
+const props = defineProps<Props>()
+
+const runnerStore = useRunnerStore()
+
+const nodeState = computed<RunnerNodeState | null>(() => {
+	if (!props.node) return null
+	return runnerStore.nodes.find((item) => item.nodeId === props.node!.id) || null
+})
+
+const hasLog = computed(
+	() => !!nodeState.value && (!!nodeState.value.track || !!nodeState.value.lastUpdateTime)
+)
+
+const statusText = computed(() => {
+	if (!nodeState.value) return '未运行'
+	switch (nodeState.value.status) {
+		case 'running':
+			return '运行中'
+		case 'success':
+			return '运行成功'
+		case 'failed':
+			return '运行失败'
+		default:
+			return '未运行'
+	}
+})
+
+const statusType = computed<'info' | 'success' | 'warning' | 'danger'>(() => {
+	if (!nodeState.value) return 'info'
+	switch (nodeState.value.status) {
+		case 'running':
+			return 'warning'
+		case 'success':
+			return 'success'
+		case 'failed':
+			return 'danger'
+		default:
+			return 'info'
+	}
+})
+
+const inputData = computed(() => {
+	const track = nodeState.value?.track
+	if (!track) return null
+	return track?.input_variable || null
+})
+
+const outputData = computed(() => {
+	const track = nodeState.value?.track
+	if (!track) return null
+	return track?.output_variable || null
+})
+
+const toPrettyJson = (value: unknown) => {
+	if (value === null || value === undefined) return ''
+	try {
+		return typeof value === 'string' ? value : JSON.stringify(value, null, 2)
+	} catch {
+		return String(value)
+	}
+}
+
+const prettyTrack = computed(() => {
+	if (!nodeState.value?.track) return ''
+	return toPrettyJson(nodeState.value.track)
+})
+</script>
+
+<template>
+	<div class="node-log">
+		<div v-if="!props.node" class="node-log__empty">当前没有选中的节点。</div>
+		<div v-else-if="!hasLog" class="node-log__empty">当前节点还没有运行记录。</div>
+		<div v-else class="node-log__content">
+			<div class="node-log__header">
+				<div class="node-log__title">
+					<span class="node-log__name">{{ props.node?.name || '未命名节点' }}</span>
+					<el-tag :type="statusType" size="small" class="node-log__status-tag">
+						{{ statusText }}
+					</el-tag>
+				</div>
+				<div v-if="nodeState?.lastUpdateTime" class="node-log__time">
+					最后更新时间:{{ nodeState.lastUpdateTime }}
+				</div>
+			</div>
+
+			<!-- 输入日志 -->
+			<div class="node-log__body">
+				<div class="node-log__section-title">输入</div>
+				<pre class="node-log__pre">{{ toPrettyJson(inputData) }}</pre>
+			</div>
+
+			<!-- 输出日志 -->
+			<div class="node-log__body">
+				<div class="node-log__section-title">输出</div>
+				<pre class="node-log__pre">{{ toPrettyJson(outputData) }}</pre>
+			</div>
+
+			<div v-if="prettyTrack" class="node-log__body">
+				<div class="node-log__section-title">运行详情(原始数据)</div>
+				<pre class="node-log__pre">{{ prettyTrack }}</pre>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style lang="less" scoped>
+.node-log {
+	padding: 16px 20px;
+	font-size: 13px;
+	color: #111827;
+
+	&__empty {
+		color: #9ca3af;
+		text-align: center;
+		padding: 40px 0;
+	}
+
+	&__content {
+		display: flex;
+		flex-direction: column;
+		gap: 12px;
+	}
+
+	&__header {
+		display: flex;
+		flex-direction: column;
+		gap: 4px;
+	}
+
+	&__title {
+		display: flex;
+		align-items: center;
+		gap: 8px;
+		font-weight: 500;
+	}
+
+	&__name {
+		color: #111827;
+	}
+
+	&__status-tag {
+		border-radius: 999px;
+	}
+
+	&__time {
+		font-size: 12px;
+		color: #6b7280;
+	}
+
+	&__body {
+		border-radius: 8px;
+		border: 1px solid #e5e7eb;
+		background: #f9fafb;
+		padding: 12px;
+	}
+
+	&__section-title {
+		font-size: 12px;
+		color: #6b7280;
+		margin-bottom: 8px;
+	}
+
+	&__pre {
+		margin: 0;
+		padding: 8px 10px;
+		border-radius: 6px;
+		background: #111827;
+		color: #e5e7eb;
+		font-family:
+			ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
+			monospace;
+		font-size: 12px;
+		line-height: 1.5;
+		white-space: pre-wrap;
+		word-break: break-all;
+		max-height: 320px;
+		min-height: 120px;
+		overflow: auto;
+	}
+}
+</style>

+ 4 - 1
apps/web/src/features/setter/index.vue

@@ -9,6 +9,7 @@
 import { computed, provide } from 'vue'
 import { Icon, Input } from '@repo/ui'
 import { useDebounceFn } from '@vueuse/core'
+import NodeLog from './NodeLog.vue'
 
 import { nodeMap } from '@/nodes'
 
@@ -146,7 +147,9 @@ provide('nodeVars', nodeVars)
 							@update="onUpdate"
 						></component>
 					</el-tab-pane>
-					<el-tab-pane label="上次运行"> 运行结果 </el-tab-pane>
+					<el-tab-pane label="上次运行">
+						<NodeLog :node="node" />
+					</el-tab-pane>
 				</el-tabs>
 			</div>
 		</div>

+ 1 - 1
apps/web/src/nodes/src/http/index.ts

@@ -54,7 +54,7 @@ export const httpNode: INodeType = {
 			: [NodeConnectionTypes.main]
 	},
 	validate: (data: HttpRequestData) => {
-		return !!data?.url.trim() ? false : '请填写URL'
+		return !!data?.url ? false : '请填写URL'
 	},
 	// 业务数据
 	schema: {

+ 424 - 0
apps/web/src/store/modules/runner.store.ts

@@ -0,0 +1,424 @@
+import { defineStore } from 'pinia'
+import { computed, reactive, readonly, ref } from 'vue'
+import { dayjs } from 'element-plus'
+
+export type RunnerStatus = 'idle' | 'connecting' | 'running' | 'finished' | 'error'
+
+export type NodeStatus = 'idle' | 'running' | 'success' | 'failed'
+
+export interface RunnerNodeInfo {
+	nodeId: string
+	nodeName: string
+	nodeType: string
+}
+
+export interface RunnerNodeState extends RunnerNodeInfo {
+	status: NodeStatus
+	lastUpdateTime?: string
+	track?: any
+}
+
+export interface RunnerExecution {
+	runnerKey: string
+	status: RunnerStatus
+	startedAt: string | null
+	finishedAt: string | null
+	nodes: RunnerNodeState[]
+	result?: any
+}
+
+interface AgentRunnerMessageBase {
+	cmd: string
+	time?: string
+}
+
+interface ConnectErrorMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_CONNECT_ERROR_MSG'
+	errorMsg: string
+}
+
+interface WelcomeMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_WELCOME_MSG'
+}
+
+interface HeartbeatMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_HEARTBEAT_MSG'
+}
+
+interface AgentRunningMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_AGENT_RUNNING_MSG'
+}
+
+interface NodeRunningMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_NODE_RUNNING_MSG'
+	node: RunnerNodeInfo
+}
+
+interface NodeFinishMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_NODE_FINISH_MSG'
+	node: RunnerNodeInfo
+	track: any
+}
+
+interface NodeIterationRunningMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_NODE_ITERATION_RUNNING_MSG'
+	count: number
+	node: RunnerNodeInfo
+}
+
+interface NodeIterationStepMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_NODE_ITERATION_STEP_MSG'
+	count: number
+	index: number
+	node: RunnerNodeInfo
+}
+
+interface NodeIterationFinishMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_NODE_ITERATION_FINISH_MSG'
+	node: RunnerNodeInfo
+}
+
+interface AgentFinishMessage extends AgentRunnerMessageBase {
+	cmd: 'CMD_AGENT_FINISH_MSG'
+	result: any
+}
+
+export type AgentRunnerMessage =
+	| ConnectErrorMessage
+	| WelcomeMessage
+	| HeartbeatMessage
+	| AgentRunningMessage
+	| NodeRunningMessage
+	| NodeFinishMessage
+	| NodeIterationRunningMessage
+	| NodeIterationStepMessage
+	| NodeIterationFinishMessage
+	| AgentFinishMessage
+
+const AGENT_RUNNER_WS_BASE = `wss://${import.meta.env.VITE_BASE_URL}/api/ws/agentRunner`
+
+export const useRunnerStore = defineStore('runner', () => {
+	const currentRunnerKey = ref<string | null>(null)
+	const status = ref<RunnerStatus>('idle')
+	const errorMsg = ref<string | null>(null)
+	const connected = ref(false)
+	const lastHeartbeatAt = ref<number | null>(null)
+	const agentResult = ref<any>(null)
+
+	const nodesMap = reactive<Record<string, RunnerNodeState>>({})
+
+	const executions = ref<RunnerExecution[]>([])
+
+	const socket = ref<WebSocket | null>(null)
+	const heartbeatTimer = ref<number | null>(null)
+
+	const nodes = computed(() => Object.values(nodesMap))
+
+	const getCurrentExecution = () => {
+		if (!currentRunnerKey.value) return null
+		return executions.value.find((item) => item.runnerKey === currentRunnerKey.value) || null
+	}
+
+	const clearHeartbeat = () => {
+		if (heartbeatTimer.value) {
+			window.clearInterval(heartbeatTimer.value)
+			heartbeatTimer.value = null
+		}
+	}
+
+	const resetState = () => {
+		status.value = 'idle'
+		errorMsg.value = null
+		connected.value = false
+		lastHeartbeatAt.value = null
+		agentResult.value = null
+		Object.keys(nodesMap).forEach((key) => {
+			delete nodesMap[key]
+		})
+	}
+
+	const closeSocket = () => {
+		clearHeartbeat()
+		if (socket.value) {
+			try {
+				socket.value.close()
+			} catch {
+				// ignore
+			}
+			socket.value = null
+		}
+	}
+
+	const updateNodeState = (info: RunnerNodeInfo, partial: Partial<RunnerNodeState>) => {
+		const existing = nodesMap[info.nodeId]
+		const nextState: RunnerNodeState = {
+			nodeId: info.nodeId,
+			nodeName: info.nodeName,
+			nodeType: info.nodeType,
+			status: existing?.status ?? 'idle',
+			...existing,
+			...partial
+		}
+
+		nodesMap[info.nodeId] = nextState
+
+		const execution = getCurrentExecution()
+		if (execution) {
+			const index = execution.nodes.findIndex((item) => item.nodeId === info.nodeId)
+			if (index === -1) {
+				execution.nodes.push({ ...nextState })
+			} else {
+				execution.nodes[index] = { ...nextState }
+			}
+		}
+	}
+
+	const handleMessage = (raw: MessageEvent<string>) => {
+		let data: AgentRunnerMessage | null = null
+		try {
+			data = JSON.parse(raw.data)
+		} catch {
+			return
+		}
+
+		if (!data || typeof data !== 'object' || !('cmd' in data)) return
+
+		switch (data.cmd) {
+			/**
+			 * 连接运行器失败
+			 */
+			case 'CMD_CONNECT_ERROR_MSG': {
+				const msg = data as ConnectErrorMessage
+				status.value = 'error'
+				errorMsg.value = msg.errorMsg || '连接运行器失败'
+				connected.value = false
+
+				const execution = getCurrentExecution()
+				if (execution) {
+					execution.status = 'error'
+					execution.finishedAt = msg.time || new Date().toISOString()
+				}
+
+				closeSocket()
+				break
+			}
+			/**
+			 * 连接运行器成功
+			 */
+			case 'CMD_WELCOME_MSG': {
+				status.value = 'running'
+				connected.value = true
+
+				const execution = getCurrentExecution()
+				if (execution) {
+					execution.status = 'running'
+				}
+
+				break
+			}
+			/**
+			 * 心跳消息
+			 */
+			case 'CMD_HEARTBEAT_MSG': {
+				lastHeartbeatAt.value = Date.now()
+				break
+			}
+			/**
+			 * 智能体运行消息
+			 */
+			case 'CMD_AGENT_RUNNING_MSG': {
+				status.value = 'running'
+
+				const execution = getCurrentExecution()
+				if (execution) {
+					execution.status = 'running'
+				}
+
+				break
+			}
+			/**
+			 * 节点运行消息
+			 */
+			case 'CMD_NODE_RUNNING_MSG': {
+				const msg = data as NodeRunningMessage
+				updateNodeState(msg.node, {
+					status: 'running',
+					lastUpdateTime: msg.time
+				})
+				break
+			}
+			/**
+			 * 节点运行完成消息
+			 */
+			case 'CMD_NODE_FINISH_MSG': {
+				const msg = data as NodeFinishMessage
+				const isSuccess = !!msg.track?.is_success
+				updateNodeState(msg.node, {
+					status: isSuccess ? 'success' : 'failed',
+					lastUpdateTime: msg.time,
+					track: msg.track
+				})
+				break
+			}
+			/**
+			 * 节点迭代运行消息
+			 */
+			case 'CMD_NODE_ITERATION_RUNNING_MSG': {
+				const msg = data as NodeIterationRunningMessage
+				updateNodeState(msg.node, {
+					status: 'running',
+					lastUpdateTime: msg.time
+				})
+				break
+			}
+			/**
+			 * 节点迭代运行步骤消息
+			 */
+			case 'CMD_NODE_ITERATION_STEP_MSG': {
+				const msg = data as NodeIterationStepMessage
+				updateNodeState(msg.node, {
+					status: 'running',
+					lastUpdateTime: msg.time
+				})
+				break
+			}
+
+			/**
+			 * 节点迭代运行完成消息
+			 */
+			case 'CMD_NODE_ITERATION_FINISH_MSG': {
+				const msg = data as NodeIterationFinishMessage
+				updateNodeState(msg.node, {
+					status: 'success',
+					lastUpdateTime: msg.time
+				})
+				break
+			}
+
+			/**
+			 * 智能体运行完成消息
+			 */
+			case 'CMD_AGENT_FINISH_MSG': {
+				const msg = data as AgentFinishMessage
+				status.value = 'finished'
+				agentResult.value = msg.result
+
+				const execution = getCurrentExecution()
+				if (execution) {
+					execution.status = 'finished'
+					execution.finishedAt = msg.time || new Date().toISOString()
+					execution.result = msg.result
+				}
+
+				closeSocket()
+				break
+			}
+			default:
+				break
+		}
+	}
+
+	const startHeartbeat = () => {
+		clearHeartbeat()
+		heartbeatTimer.value = window.setInterval(() => {
+			if (!socket.value || socket.value.readyState !== WebSocket.OPEN) return
+			socket.value.send(JSON.stringify({ cmd: 'CMD_HEARTBEAT_MSG' }))
+		}, 10000)
+	}
+
+	const startRunner = (runnerKey: string) => {
+		if (!runnerKey) return
+
+		closeSocket()
+		resetState()
+
+		currentRunnerKey.value = runnerKey
+		status.value = 'connecting'
+
+		executions.value.unshift({
+			runnerKey,
+			status: 'connecting',
+			startedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+			finishedAt: null,
+			nodes: [],
+			result: undefined
+		})
+
+		const url = `${AGENT_RUNNER_WS_BASE}?agentRunnerKey=${encodeURIComponent(runnerKey)}`
+		const ws = new WebSocket(url)
+		socket.value = ws
+
+		ws.onopen = () => {
+			connected.value = true
+			status.value = 'running'
+			startHeartbeat()
+
+			const execution = getCurrentExecution()
+			if (execution) {
+				execution.status = 'running'
+			}
+		}
+
+		ws.onmessage = (event) => {
+			handleMessage(event as MessageEvent<string>)
+		}
+
+		ws.onerror = () => {
+			status.value = 'error'
+			errorMsg.value = errorMsg.value || '运行器连接异常'
+
+			const execution = getCurrentExecution()
+			if (execution) {
+				execution.status = 'error'
+				if (!execution.finishedAt) {
+					execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
+				}
+			}
+		}
+
+		ws.onclose = () => {
+			connected.value = false
+			clearHeartbeat()
+			if (status.value === 'running' || status.value === 'connecting') {
+				status.value = 'finished'
+
+				const execution = getCurrentExecution()
+				if (execution && !execution.finishedAt) {
+					execution.status = 'finished'
+					execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
+				}
+			}
+		}
+	}
+
+	const stopRunner = () => {
+		closeSocket()
+		status.value = 'finished'
+
+		const execution = getCurrentExecution()
+		if (execution && !execution.finishedAt) {
+			execution.status = 'finished'
+			execution.finishedAt = dayjs().format('YYYY-MM-DD HH:mm:ss')
+		}
+	}
+
+	const resetRunner = () => {
+		closeSocket()
+		currentRunnerKey.value = null
+		resetState()
+	}
+
+	return {
+		currentRunnerKey: readonly(currentRunnerKey),
+		status,
+		errorMsg,
+		connected,
+		lastHeartbeatAt,
+		nodes,
+		agentResult,
+		executions,
+		startRunner,
+		stopRunner,
+		resetRunner
+	}
+})

+ 29 - 12
apps/web/src/views/Editor.vue

@@ -52,7 +52,8 @@
 					<Workflow
 						:workflow="workflow"
 						:nodeMap="nodeMap"
-						@click:node="handleNodeClick"
+						@click:node="handleSelectNode"
+						@dblclick:node="handleNodeClick"
 						@create:node="handleNodeCreate"
 						@create:connection:end="onCreateConnection"
 						@drag-and-drop="handleDrop"
@@ -106,6 +107,7 @@ import { Workflow, useDragAndDrop } from '@repo/workflow'
 import { dayjs, ElMessage, ElMessageBox } from 'element-plus'
 
 import type { IWorkflow, XYPosition, Connection } from '@repo/workflow'
+import { useRunnerStore } from '@/store/modules/runner.store'
 
 const layout = inject<{ setMainStyle: (style: CSSProperties) => void }>('layout')
 
@@ -123,6 +125,7 @@ const footerHeight = ref(32)
 const route = useRoute()
 const router = useRouter()
 const id = route.params?.id as string
+const runnerStore = useRunnerStore()
 
 const projectMap = JSON.parse(localStorage.getItem(`workflow-map`) || '{}') as Record<
 	string,
@@ -267,14 +270,6 @@ const isPendingCreate = (node: any) => {
 	return !!(node as any)?.__pendingCreate
 }
 
-const isEqualNodeData = (current: any, next: any) => {
-	try {
-		return JSON.stringify(current ?? {}) === JSON.stringify(next ?? {})
-	} catch {
-		return current === next
-	}
-}
-
 const loadAgentWorkflow = async (agentId: string) => {
 	if (!agentId) return
 	isHydrating.value = true
@@ -440,6 +435,10 @@ const handleRunSelectedNode = async () => {
 			start_node_id: nodeID.value,
 			is_debugger: true
 		})
+		const agentRunnerKey = response?.result
+		if (agentRunnerKey) {
+			runnerStore.startRunner(agentRunnerKey)
+		}
 		runVisible.value = false
 		handleApiResult(response, '已提交节点测试', '节点测试失败')
 	} catch (error) {
@@ -460,10 +459,14 @@ const handleRunNode = async (id: string) => {
 			start_node_id: id,
 			is_debugger: true
 		})
-		handleApiResult(response, '已提交节点测试', '节点测试失败')
+		const agentRunnerKey = response?.result
+		if (agentRunnerKey) {
+			runnerStore.startRunner(agentRunnerKey)
+		}
+		handleApiResult(response, '已提交运行节点', '运行节点失败')
 	} catch (error) {
 		console.error('postDoTestNodeRunner error', error)
-		ElMessage.error('节点测试失败')
+		ElMessage.error('运行节点失败')
 	}
 }
 const handleNodeCreate = (value: { type: string; position?: XYPosition } | string) => {
@@ -615,6 +618,21 @@ const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[
 	})
 }
 
+/**
+ * 点击选中节点
+ * @param id
+ * @param position
+ */
+const handleSelectNode = (id: string) => {
+	workflow.value?.nodes.forEach((node) => {
+		node.selected = false
+	})
+	const node = workflow.value?.nodes.find((node) => node.id === id)
+	if (node) {
+		node.selected = true
+	}
+}
+
 /**
  * 修改节点数据
  */
@@ -669,7 +687,6 @@ const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
  * 删除节点
  */
 const handleDeleteNode = async (id: string) => {
-	console.log('del node', id)
 	const index = workflow.value.nodes.findIndex((node) => node.id === id)
 	if (index != -1) {
 		await agent.postAgentDoDeleteAgentNode({

+ 54 - 50
apps/web/vite.config.ts

@@ -1,4 +1,4 @@
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import path from 'path'
 import UnoCss from 'unocss/vite'
@@ -11,55 +11,59 @@ import monacoEditorPlugin from 'vite-plugin-monaco-editor'
 import vueJsx from '@vitejs/plugin-vue-jsx'
 
 // https://vite.dev/config/
-export default defineConfig({
-	plugins: [
-		vue(),
-		vueJsx(),
-		UnoCss(),
-		createSvgIconsPlugin({
-			// 指定存放 SVG 的文件夹
-			iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
-			symbolId: 'svg-icon-[dir]-[name]'
-		}),
-		// 代码编辑器
-		(monacoEditorPlugin as any).default({
-			languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html', 'css']
-		}),
-		// 按需求加载(模板)
-		AutoImport({
-			imports: ['vue'],
-			resolvers: [
-				IconsResolver({
-					prefix: 'Icon'
-				}),
-				ElementPlusResolver()
-			],
-			dts: 'auto-imports.d.ts'
-		}),
-		Components({
-			resolvers: [
-				// 自动注册图标组件
-				IconsResolver({
-					enabledCollections: ['ep']
-				}),
-				ElementPlusResolver()
-			],
-			dts: 'components.d.ts'
-		})
-	],
-	resolve: {
-		alias: {
-			'@': path.resolve(__dirname, 'src')
-		}
-	},
-	server: {
-		host: true,
-		port: 5174,
-		proxy: {
-			'/api': {
-				target: 'http://shalu-componenttesting-admin-dev.shalu.com',
-				changeOrigin: true,
-				rewrite: (path) => path.replace(/^\/api/, '/api')
+export default defineConfig(({ mode }) => {
+	const env = loadEnv(mode, process.cwd())
+
+	return {
+		plugins: [
+			vue(),
+			vueJsx(),
+			UnoCss(),
+			createSvgIconsPlugin({
+				// 指定存放 SVG 的文件夹
+				iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
+				symbolId: 'svg-icon-[dir]-[name]'
+			}),
+			// 代码编辑器
+			(monacoEditorPlugin as any).default({
+				languageWorkers: ['editorWorkerService', 'typescript', 'json', 'html', 'css']
+			}),
+			// 按需求加载(模板)
+			AutoImport({
+				imports: ['vue'],
+				resolvers: [
+					IconsResolver({
+						prefix: 'Icon'
+					}),
+					ElementPlusResolver()
+				],
+				dts: 'auto-imports.d.ts'
+			}),
+			Components({
+				resolvers: [
+					// 自动注册图标组件
+					IconsResolver({
+						enabledCollections: ['ep']
+					}),
+					ElementPlusResolver()
+				],
+				dts: 'components.d.ts'
+			})
+		],
+		resolve: {
+			alias: {
+				'@': path.resolve(__dirname, 'src')
+			}
+		},
+		server: {
+			host: true,
+			port: 5174,
+			proxy: {
+				'/api': {
+					target: `http://${env.VITE_BASE_URL}`,
+					changeOrigin: true,
+					rewrite: (path) => path.replace(/^\/api/, '/api')
+				}
 			}
 		}
 	}

+ 1 - 0
packages/workflow/src/Interface.ts

@@ -26,6 +26,7 @@ export interface IWorkflowNode extends Node {
 	remark?: string
 	nodeType: string
 	parentId?: string
+	selected?: boolean
 	data: {
 		id: string
 		// 位置

+ 1 - 1
packages/workflow/src/components/elements/nodes/render-types/NodeDefault.vue

@@ -37,7 +37,7 @@ const nodeType = computed(() => nodeMap?.[nodeData.value?.nodeType!])
 
 const warningInfo = computed(() => {
 	const validate = nodeType.value?.validate
-	return validate && validate(nodeData.value?.data)
+	return validate && validate(nodeData.value)
 })
 </script>