| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- <template>
- <div class="workflow-editor-page">
- <header class="workflow-editor-topbar">
- <div class="workflow-editor-title">
- <el-button @click="$emit('back')">返回</el-button>
- <el-input v-model="workflowName" placeholder="工作流名称" class="workflow-name-input" />
- <el-input v-model="workflowKey" placeholder="workflow key" class="workflow-key-input" />
- <el-input v-model="workflowDescription" placeholder="描述" class="workflow-desc-input" />
- </div>
- <div class="filters">
- <el-button type="primary" :loading="saving" @click="save">保存</el-button>
- <el-button type="success" :disabled="!workflowId" :loading="running" @click="runWorkflow">执行</el-button>
- <el-button @click="fitView()">适配画布</el-button>
- </div>
- </header>
- <div class="workflow-editor-body">
- <aside class="workflow-node-palette">
- <div class="section-title">节点库</div>
- <el-collapse v-model="openedCategories">
- <el-collapse-item v-for="group in groupedNodeDefinitions" :key="group.category" :title="categoryLabel(group.category)" :name="group.category">
- <button v-for="definition in group.items" :key="definition.type" class="palette-node" type="button" @click="addNode(definition)">
- <span>{{ definition.label }}</span>
- <small>{{ definition.type }}</small>
- <em>{{ definition.description }}</em>
- </button>
- </el-collapse-item>
- </el-collapse>
- </aside>
- <main class="workflow-flow-wrap">
- <VueFlow
- v-model:nodes="flowNodes"
- v-model:edges="flowEdges"
- class="workflow-flow"
- :default-viewport="{ zoom: 0.9 }"
- :min-zoom="0.2"
- :max-zoom="2"
- fit-view-on-init
- @connect="onConnect"
- @node-click="onNodeClick"
- @pane-click="selectedNodeId = null"
- >
- <Background pattern-color="#cbd5e1" :gap="24" />
- <Controls />
- <template #node-default="{ id, data, selected }">
- <div class="workflow-card-node" :class="{ selected, collapsed: data.collapsed }">
- <div class="workflow-card-head">
- <div class="workflow-card-title-wrap">
- <div class="workflow-card-type">{{ nodeDefinition(data.nodeType)?.label || data.nodeType }}</div>
- <div class="workflow-card-title">{{ data.title }}</div>
- </div>
- <button class="workflow-card-toggle" type="button" @click.stop="toggleNodeCollapsed(id)">
- {{ data.collapsed ? '展开' : '折叠' }}
- </button>
- </div>
- <div class="workflow-card-desc">{{ nodeDefinition(data.nodeType)?.description || data.nodeType }}</div>
- <div v-if="!data.collapsed" class="workflow-card-body">
- <div class="workflow-card-section">
- <strong>参数</strong>
- <span v-if="!nodeParamEntries(data).length">无</span>
- <span v-for="item in nodeParamEntries(data)" :key="item.key" class="workflow-card-chip">
- {{ item.label }}={{ shortValue(item.value) }}
- </span>
- </div>
- <div class="workflow-card-section">
- <strong>输入</strong>
- <span v-if="!nodeInputEntries(data).length">无</span>
- <span v-for="item in nodeInputEntries(data)" :key="item.key" class="workflow-card-chip input">
- {{ item.label }}←{{ bindingSummary(item.binding) }}
- </span>
- </div>
- <div class="workflow-card-section">
- <strong>输出</strong>
- <span v-if="!nodeOutputEntries(data.nodeType).length">无</span>
- <span v-for="item in nodeOutputEntries(data.nodeType).slice(0, 4)" :key="item.key" class="workflow-card-chip output">
- {{ item.key }}:{{ item.type }}
- </span>
- <span v-if="nodeOutputEntries(data.nodeType).length > 4" class="workflow-card-more">
- +{{ nodeOutputEntries(data.nodeType).length - 4 }}
- </span>
- </div>
- </div>
- </div>
- </template>
- </VueFlow>
- </main>
- <aside class="workflow-inspector-panel">
- <div class="section-title">工作流变量</div>
- <div class="workflow-variable-list">
- <div v-for="(variable, name) in workflowVariables" :key="name" class="workflow-variable-row">
- <div class="workflow-variable-head">
- <strong>{{ name }}</strong>
- <el-button size="small" type="danger" @click="deleteVariable(name)">删除</el-button>
- </div>
- <div class="workflow-variable-fields">
- <el-select v-model="ensureVariableObject(name).type" size="small">
- <el-option label="文本" value="string" />
- <el-option label="数字" value="number" />
- <el-option label="布尔" value="boolean" />
- <el-option label="对象" value="object" />
- <el-option label="数组" value="array" />
- </el-select>
- <el-input
- v-if="['object', 'array'].includes(ensureVariableObject(name).type)"
- :model-value="variableJsonText(ensureVariableObject(name).default)"
- type="textarea"
- :rows="5"
- placeholder="JSON"
- @change="updateVariableJson(name, $event)"
- />
- <component
- v-else
- :is="variableDefaultComponent(ensureVariableObject(name))"
- v-model="ensureVariableObject(name).default"
- v-bind="variableDefaultProps(ensureVariableObject(name))"
- placeholder="默认值"
- />
- </div>
- <el-input v-model="ensureVariableObject(name).description" size="small" placeholder="说明" />
- </div>
- <el-button class="add-variable-button" @click="addVariable">新增变量</el-button>
- </div>
- <template v-if="selectedNode">
- <div class="section-title">节点属性</div>
- <el-form label-width="86px" class="workflow-inspector-form">
- <el-form-item label="标题">
- <el-input v-model="selectedNode.data.title" @input="syncNodeLabel" />
- </el-form-item>
- <el-form-item label="类型">
- <el-tag>{{ selectedNode.data.nodeType }}</el-tag>
- </el-form-item>
- <el-form-item label="说明">
- <div class="node-description">{{ selectedDefinition?.description || '暂无说明' }}</div>
- </el-form-item>
- </el-form>
- <div class="section-title">参数</div>
- <el-form label-width="92px" class="workflow-inspector-form">
- <el-form-item v-for="(field, key) in selectedDefinition?.params || {}" :key="key" :label="field.label || key">
- <component
- :is="fieldComponent(field)"
- v-model="selectedNode.data.params[key]"
- v-bind="fieldProps(field)"
- class="workflow-field"
- >
- <el-option v-for="option in field.options || []" :key="option" :label="option" :value="option" />
- </component>
- </el-form-item>
- <div v-if="!Object.keys(selectedDefinition?.params || {}).length" class="muted">此节点没有固定参数。</div>
- </el-form>
- <div class="section-title">输入</div>
- <div v-for="(field, key) in selectedDefinition?.inputs || {}" :key="key" class="input-binding-row">
- <div class="input-binding-name">{{ field.label || key }}</div>
- <el-select v-model="inputBinding(key).source" class="input-source">
- <el-option label="固定值" value="literal" />
- <el-option label="变量" value="variable" />
- <el-option label="节点输出" value="node_output" />
- <el-option label="运行时" value="runtime" />
- </el-select>
- <el-input v-if="inputBinding(key).source === 'literal'" v-model="inputBinding(key).value" placeholder="值" />
- <el-input v-if="inputBinding(key).source === 'variable'" v-model="inputBinding(key).name" placeholder="变量名" />
- <template v-if="inputBinding(key).source === 'node_output'">
- <el-select v-model="inputBinding(key).node_id" placeholder="节点">
- <el-option v-for="node in flowNodes.filter((item) => item.id !== selectedNode.id)" :key="node.id" :label="node.data.title" :value="node.id" />
- </el-select>
- <el-select v-model="inputBinding(key).output" placeholder="输出">
- <el-option v-for="output in outputEntriesForNode(inputBinding(key).node_id)" :key="output.key" :label="`${output.label} (${output.key})`" :value="output.key" />
- </el-select>
- </template>
- <el-input v-if="inputBinding(key).source === 'runtime'" v-model="inputBinding(key).name" placeholder="运行时键名" />
- <div v-if="field.description" class="field-help">{{ field.description }}</div>
- </div>
- <div v-if="!Object.keys(selectedDefinition?.inputs || {}).length" class="muted">此节点不需要输入。</div>
- <div class="section-title">输出数据结构</div>
- <el-table :data="nodeOutputEntries(selectedNode.data.nodeType)" size="small" border empty-text="此节点没有声明输出">
- <el-table-column prop="key" label="字段" width="110" />
- <el-table-column prop="label" label="含义" min-width="130" />
- <el-table-column prop="type" label="类型" width="90" />
- <el-table-column prop="description" label="说明" min-width="150" show-overflow-tooltip />
- </el-table>
- <div class="section-title">相关连线</div>
- <el-table :data="selectedEdges" size="small" border>
- <el-table-column label="来源" min-width="150">
- <template #default="{ row }">
- {{ edgeEndpointLabel(row.source, row.sourceHandle || edgeSourcePort(row)) }}
- </template>
- </el-table-column>
- <el-table-column label="目标" min-width="150">
- <template #default="{ row }">
- {{ edgeEndpointLabel(row.target, row.targetHandle || edgeTargetPort(row)) }}
- </template>
- </el-table-column>
- <el-table-column label="类型" width="100">
- <template #default="{ row }">
- <el-select v-model="row.data.kind" size="small" @change="syncEdgeLabel(row)">
- <el-option label="控制流" value="control" />
- <el-option label="数据流" value="data" />
- </el-select>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="74">
- <template #default="{ row }">
- <el-button size="small" type="danger" @click="deleteEdge(row.id)">删</el-button>
- </template>
- </el-table-column>
- </el-table>
- <div class="filters inspector-actions">
- <el-button type="danger" @click="deleteSelectedNode">删除节点</el-button>
- </div>
- </template>
- <div v-else class="muted">从左侧添加节点,或点击画布中的节点编辑参数和输入绑定。</div>
- <div class="section-title">全部连线</div>
- <el-table :data="flowEdges" size="small" border empty-text="暂无连线">
- <el-table-column label="连线" min-width="230">
- <template #default="{ row }">
- {{ readableEdgeLabel(row) }}
- </template>
- </el-table-column>
- <el-table-column label="类型" width="82">
- <template #default="{ row }">
- <el-tag size="small" :type="row.data?.kind === 'data' ? 'success' : 'primary'">
- {{ edgeKindLabel(row.data?.kind) }}
- </el-tag>
- </template>
- </el-table-column>
- </el-table>
- <div class="section-title">执行结果</div>
- <pre class="workflow-run-output">{{ runOutput || '暂无执行结果' }}</pre>
- </aside>
- </div>
- </div>
- </template>
- <script setup>
- import { computed, nextTick, onMounted, ref } from 'vue'
- import { VueFlow, useVueFlow } from '@vue-flow/core'
- import { Background } from '@vue-flow/background'
- import { Controls } from '@vue-flow/controls'
- import '@vue-flow/core/dist/style.css'
- import '@vue-flow/core/dist/theme-default.css'
- import '@vue-flow/controls/dist/style.css'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { api } from '../api'
- const props = defineProps({
- workflowId: { type: Number, default: null },
- })
- const emit = defineEmits(['back', 'saved', 'task-created'])
- const { fitView } = useVueFlow()
- const nodeDefinitions = ref([])
- const flowNodes = ref([])
- const flowEdges = ref([])
- const workflowName = ref('')
- const workflowKey = ref('')
- const workflowDescription = ref('')
- const workflowVariables = ref({})
- const workflowSettings = ref({ max_steps: 100, default_timeout_ms: 30000, on_unhandled_error: 'pause_for_user' })
- const workflowId = ref(props.workflowId)
- const selectedNodeId = ref(null)
- const openedCategories = ref(['flow', 'browser', 'vision', 'media', 'mouse', 'keyboard'])
- const saving = ref(false)
- const running = ref(false)
- const runOutput = ref('')
- const selectedNode = computed(() => flowNodes.value.find((node) => node.id === selectedNodeId.value))
- const selectedDefinition = computed(() => nodeDefinitions.value.find((item) => item.type === selectedNode.value?.data?.nodeType))
- const selectedEdges = computed(() => flowEdges.value.filter((edge) => edge.source === selectedNodeId.value || edge.target === selectedNodeId.value))
- const groupedNodeDefinitions = computed(() => {
- const groups = new Map()
- for (const item of nodeDefinitions.value) {
- if (!groups.has(item.category)) groups.set(item.category, [])
- groups.get(item.category).push(item)
- }
- return [...groups.entries()].map(([category, items]) => ({ category, items }))
- })
- function categoryLabel(category) {
- return {
- browser: '浏览器',
- flow: '流程',
- human: '人工交互',
- keyboard: '键盘',
- media: '媒体',
- mouse: '鼠标',
- program: '程序',
- research: '研究',
- screen: '屏幕',
- text: '文本',
- vision: '视觉 AI',
- wait: '等待',
- }[category] || category
- }
- function nodeDefinition(nodeType) {
- return nodeDefinitions.value.find((item) => item.type === nodeType)
- }
- async function loadDefinitions() {
- const { data } = await api.get('/api/automation/workflow-nodes')
- nodeDefinitions.value = data.items || []
- }
- async function loadWorkflow() {
- if (!workflowId.value) {
- workflowName.value = `新工作流 ${new Date().toLocaleString()}`
- workflowKey.value = ''
- workflowDescription.value = ''
- workflowVariables.value = {}
- workflowSettings.value = { max_steps: 100, default_timeout_ms: 30000, on_unhandled_error: 'pause_for_user' }
- flowNodes.value = []
- flowEdges.value = []
- return
- }
- const { data } = await api.get(`/api/automation/workflows/${workflowId.value}`)
- workflowName.value = data.name
- workflowKey.value = data.workflow_key || ''
- workflowDescription.value = data.description || ''
- workflowVariables.value = data.variables || {}
- workflowSettings.value = data.settings || {}
- flowNodes.value = (data.nodes || []).map(workflowNodeToFlow)
- flowEdges.value = (data.edges || []).map(workflowEdgeToFlow)
- refreshEdgeLabels()
- }
- function workflowNodeToFlow(node) {
- return {
- id: node.id,
- type: 'default',
- label: node.title || node.type,
- position: node.position || { x: 80, y: 80 },
- data: {
- title: node.title || node.type,
- nodeType: node.type,
- params: structuredClone(node.params || {}),
- inputs: structuredClone(node.inputs || {}),
- collapsed: false,
- },
- }
- }
- function workflowEdgeToFlow(edge) {
- const kind = edge.kind || 'control'
- return {
- id: edge.id || `${edge.source}-${edge.target}-${Date.now()}`,
- source: edge.source,
- target: edge.target,
- sourceHandle: edge.source_port || null,
- targetHandle: edge.target_port || null,
- label: '',
- data: { kind },
- animated: kind === 'control',
- style: { stroke: kind === 'data' ? '#10b981' : '#2563eb' },
- }
- }
- function addNode(definition) {
- const index = flowNodes.value.length
- const node = {
- id: `node_${Date.now()}_${index}`,
- type: 'default',
- label: definition.label,
- position: { x: 120 + index * 40, y: 120 + index * 36 },
- data: {
- title: definition.label,
- nodeType: definition.type,
- params: defaultValues(definition.params || {}),
- inputs: {},
- collapsed: false,
- },
- }
- flowNodes.value.push(node)
- selectedNodeId.value = node.id
- nextTick(() => fitView())
- }
- function defaultValues(fields) {
- const values = {}
- for (const [key, field] of Object.entries(fields)) {
- values[key] = structuredClone(field.default ?? defaultValueForType(field.type))
- }
- return values
- }
- function defaultValueForType(type) {
- if (type === 'number') return 0
- if (type === 'boolean') return false
- if (type === 'array') return []
- return ''
- }
- function onConnect(connection) {
- const kind = 'control'
- flowEdges.value.push({
- id: `edge_${Date.now()}`,
- ...connection,
- label: '',
- data: { kind },
- animated: true,
- style: { stroke: '#2563eb' },
- })
- refreshEdgeLabels()
- }
- function onNodeClick(event) {
- selectedNodeId.value = event.node.id
- }
- function syncNodeLabel() {
- if (!selectedNode.value) return
- selectedNode.value.label = selectedNode.value.data.title
- refreshEdgeLabels()
- }
- function syncEdgeLabel(edge) {
- edge.label = readableEdgeLabel(edge)
- edge.animated = edge.data.kind === 'control'
- edge.style = { stroke: edge.data.kind === 'data' ? '#10b981' : '#2563eb' }
- }
- function edgeLabel(kind) {
- return kind === 'data' ? '数据' : '控制'
- }
- function edgeKindLabel(kind) {
- return kind === 'data' ? '数据' : '控制'
- }
- function edgeSourcePort(edge) {
- return edge.sourceHandle || (edge.data?.kind === 'data' ? 'value' : 'success')
- }
- function edgeTargetPort(edge) {
- return edge.targetHandle || (edge.data?.kind === 'data' ? 'value' : 'run')
- }
- function edgeEndpointLabel(nodeId, port) {
- const node = flowNodes.value.find((item) => item.id === nodeId)
- const title = node?.data?.title || nodeId || '未知节点'
- return port ? `${title}.${port}` : title
- }
- function readableEdgeLabel(edge) {
- return `${edgeEndpointLabel(edge.source, edgeSourcePort(edge))} → ${edgeEndpointLabel(edge.target, edgeTargetPort(edge))}`
- }
- function refreshEdgeLabels() {
- flowEdges.value.forEach((edge) => {
- edge.label = readableEdgeLabel(edge)
- })
- }
- function toggleNodeCollapsed(nodeId) {
- const node = flowNodes.value.find((item) => item.id === nodeId)
- if (node) node.data.collapsed = !node.data.collapsed
- }
- function fieldEntries(fields = {}) {
- return Object.entries(fields).map(([key, field]) => ({
- key,
- label: field.label || key,
- type: field.type || 'any',
- description: field.description || '',
- }))
- }
- function nodeOutputEntries(nodeType) {
- return fieldEntries(nodeDefinition(nodeType)?.outputs || {})
- }
- function outputEntriesForNode(nodeId) {
- const node = flowNodes.value.find((item) => item.id === nodeId)
- return node ? nodeOutputEntries(node.data.nodeType) : []
- }
- function nodeParamEntries(data) {
- const definition = nodeDefinition(data.nodeType)
- const params = data.params || {}
- return fieldEntries(definition?.params || {})
- .filter((item) => params[item.key] !== undefined && params[item.key] !== '')
- .map((item) => ({ ...item, value: params[item.key] }))
- }
- function nodeInputEntries(data) {
- const definition = nodeDefinition(data.nodeType)
- const inputs = data.inputs || {}
- return fieldEntries(definition?.inputs || {})
- .filter((item) => inputs[item.key])
- .map((item) => ({ ...item, binding: inputs[item.key] }))
- }
- function shortValue(value) {
- if (value === null || value === undefined || value === '') return '空'
- if (typeof value === 'boolean') return value ? '是' : '否'
- if (typeof value === 'object') {
- const text = JSON.stringify(value)
- return text.length > 28 ? `${text.slice(0, 28)}...` : text
- }
- const text = String(value)
- return text.length > 28 ? `${text.slice(0, 28)}...` : text
- }
- function bindingSummary(binding) {
- if (!binding) return '未设置'
- if (binding.source === 'variable') return `变量:${binding.name || '-'}`
- if (binding.source === 'node_output') return `${edgeEndpointLabel(binding.node_id, binding.output || '-')}`
- if (binding.source === 'runtime') return `运行时:${binding.name || '-'}`
- return shortValue(binding.value)
- }
- function fieldComponent(field) {
- if (field.type === 'boolean') return 'el-switch'
- if (field.type === 'select') return 'el-select'
- if (field.type === 'number') return 'el-input-number'
- if (field.type === 'textarea') return 'el-input'
- return 'el-input'
- }
- function fieldProps(field) {
- if (field.type === 'textarea') return { type: 'textarea', rows: 4 }
- if (field.type === 'number') return { min: field.min, max: field.max }
- if (field.type === 'array') return { type: 'textarea', rows: 3, placeholder: 'JSON 数组或逗号分隔文本' }
- return {}
- }
- function inputBinding(key) {
- if (!selectedNode.value.data.inputs[key]) {
- selectedNode.value.data.inputs[key] = { source: 'literal', value: '' }
- }
- return selectedNode.value.data.inputs[key]
- }
- function ensureVariableObject(name) {
- if (!workflowVariables.value[name] || typeof workflowVariables.value[name] !== 'object') {
- workflowVariables.value[name] = {
- type: typeof workflowVariables.value[name],
- default: workflowVariables.value[name],
- description: '',
- }
- }
- workflowVariables.value[name].type ||= 'string'
- return workflowVariables.value[name]
- }
- function variableDefaultComponent(variable) {
- if (variable.type === 'number') return 'el-input-number'
- if (variable.type === 'boolean') return 'el-switch'
- return 'el-input'
- }
- function variableDefaultProps(variable) {
- if (variable.type === 'number') return { controlsPosition: 'right' }
- return {}
- }
- function variableJsonText(value) {
- return JSON.stringify(value ?? {}, null, 2)
- }
- function updateVariableJson(name, value) {
- try {
- ensureVariableObject(name).default = JSON.parse(value)
- } catch {
- ElMessage.error(`变量 ${name} 的默认值不是有效 JSON`)
- }
- }
- async function addVariable() {
- const { value } = await ElMessageBox.prompt('请输入变量名', '新增变量', {
- inputPattern: /^[A-Za-z_][A-Za-z0-9_]*$/,
- inputErrorMessage: '变量名只能使用字母、数字和下划线,且不能以数字开头',
- })
- if (workflowVariables.value[value]) {
- ElMessage.warning('变量已存在')
- return
- }
- workflowVariables.value[value] = { type: 'string', default: '', description: '' }
- }
- function deleteVariable(name) {
- delete workflowVariables.value[name]
- }
- function deleteEdge(edgeId) {
- flowEdges.value = flowEdges.value.filter((edge) => edge.id !== edgeId)
- }
- function deleteSelectedNode() {
- if (!selectedNodeId.value) return
- flowNodes.value = flowNodes.value.filter((node) => node.id !== selectedNodeId.value)
- flowEdges.value = flowEdges.value.filter((edge) => edge.source !== selectedNodeId.value && edge.target !== selectedNodeId.value)
- selectedNodeId.value = null
- }
- function buildPayload() {
- return {
- schema_version: 'workflow/v1',
- workflow_key: workflowKey.value.trim() || null,
- name: workflowName.value.trim(),
- description: workflowDescription.value,
- variables: workflowVariables.value,
- settings: workflowSettings.value,
- nodes: flowNodes.value.map((node) => ({
- id: node.id,
- type: node.data.nodeType,
- title: node.data.title,
- position: node.position,
- params: node.data.params || {},
- inputs: node.data.inputs || {},
- })),
- edges: flowEdges.value.map((edge) => ({
- id: edge.id,
- kind: edge.data?.kind || 'control',
- source: edge.source,
- source_port: edge.sourceHandle || (edge.data?.kind === 'data' ? 'value' : 'success'),
- target: edge.target,
- target_port: edge.targetHandle || (edge.data?.kind === 'data' ? 'value' : 'run'),
- })),
- }
- }
- async function save() {
- if (!workflowName.value.trim()) {
- ElMessage.warning('请输入工作流名称')
- return
- }
- saving.value = true
- try {
- const payload = buildPayload()
- const { data } = workflowId.value
- ? await api.put(`/api/automation/workflows/${workflowId.value}`, payload)
- : await api.post('/api/automation/workflows', payload)
- workflowId.value = data.id
- ElMessage.success('已保存')
- emit('saved', data.id)
- } finally {
- saving.value = false
- }
- }
- async function runWorkflow() {
- if (!workflowId.value || !workflowKey.value.trim()) {
- ElMessage.warning('请先保存并设置 workflow key')
- return
- }
- running.value = true
- try {
- const { data } = await api.post(`/api/automation/workflows/by-key/${encodeURIComponent(workflowKey.value.trim())}/run`, {})
- runOutput.value = JSON.stringify(data, null, 2)
- ElMessage.success('任务已加入执行队列')
- emit('task-created', data)
- } catch (error) {
- ElMessage.error(error.response?.data?.detail || '执行工作流失败')
- } finally {
- running.value = false
- }
- }
- onMounted(async () => {
- await loadDefinitions()
- await loadWorkflow()
- })
- </script>
|