|
@@ -6,6 +6,7 @@ import { cloneDeep } from 'lodash-es'
|
|
|
import { agent } from '@repo/api-service'
|
|
import { agent } from '@repo/api-service'
|
|
|
|
|
|
|
|
import InputTab from './components/InputTab.vue'
|
|
import InputTab from './components/InputTab.vue'
|
|
|
|
|
+import TriggerTab from './components/TriggerTab.vue'
|
|
|
import ResultTab from './components/ResultTab.vue'
|
|
import ResultTab from './components/ResultTab.vue'
|
|
|
import DetailTab from './components/DetailTab.vue'
|
|
import DetailTab from './components/DetailTab.vue'
|
|
|
import TraceTab from './components/TraceTab.vue'
|
|
import TraceTab from './components/TraceTab.vue'
|
|
@@ -24,12 +25,16 @@ interface Props {
|
|
|
visible: boolean
|
|
visible: boolean
|
|
|
closeOnRun?: boolean
|
|
closeOnRun?: boolean
|
|
|
inputOnly?: boolean
|
|
inputOnly?: boolean
|
|
|
|
|
+ activeNodeId?: string
|
|
|
|
|
+ initialTab?: 'input' | 'trigger' | 'result' | 'detail' | 'trace'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
visible: false,
|
|
visible: false,
|
|
|
closeOnRun: false,
|
|
closeOnRun: false,
|
|
|
- inputOnly: false
|
|
|
|
|
|
|
+ inputOnly: false,
|
|
|
|
|
+ activeNodeId: '',
|
|
|
|
|
+ initialTab: 'input'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
@@ -55,6 +60,25 @@ const startNode = computed<IWorkflowNode | null>(() => {
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const activeNode = computed<IWorkflowNode | null>(() => {
|
|
|
|
|
+ if (props.activeNodeId) {
|
|
|
|
|
+ return props.workflow?.nodes?.find((node) => node.id === props.activeNodeId) || null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return startNode.value
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const activeNodeType = computed(() => {
|
|
|
|
|
+ return ((activeNode.value as any)?.nodeType ||
|
|
|
|
|
+ (activeNode.value as any)?.data?.nodeType ||
|
|
|
|
|
+ '') as string
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const showInputTab = computed(() => activeNodeType.value === 'start')
|
|
|
|
|
+const showTriggerTab = computed(() =>
|
|
|
|
|
+ ['trigger-schedule', 'trigger-webhook'].includes(activeNodeType.value)
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
const startVariables = computed<StartVariable[]>(() => {
|
|
const startVariables = computed<StartVariable[]>(() => {
|
|
|
const variables = (startNode.value as any)?.data?.variables
|
|
const variables = (startNode.value as any)?.data?.variables
|
|
|
return Array.isArray(variables) ? variables : []
|
|
return Array.isArray(variables) ? variables : []
|
|
@@ -72,27 +96,6 @@ const currentExecution = computed<RunnerExecution | null>(() => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const traceNodes = computed(() => currentExecution.value?.nodes || runnerStore.nodes || [])
|
|
const traceNodes = computed(() => currentExecution.value?.nodes || runnerStore.nodes || [])
|
|
|
-const outputNodeIds = computed(() => {
|
|
|
|
|
- return new Set(
|
|
|
|
|
- (props.workflow?.nodes || [])
|
|
|
|
|
- .filter((node) => {
|
|
|
|
|
- const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
|
|
|
|
|
- return nodeType === 'end'
|
|
|
|
|
- })
|
|
|
|
|
- .map((node) => node.id)
|
|
|
|
|
- )
|
|
|
|
|
-})
|
|
|
|
|
-const hasOutputNode = computed(() => outputNodeIds.value.size > 0)
|
|
|
|
|
-const outputNodeState = computed(() => {
|
|
|
|
|
- const nodes = [...traceNodes.value]
|
|
|
|
|
- return (
|
|
|
|
|
- nodes.reverse().find((node) => {
|
|
|
|
|
- return outputNodeIds.value.has(node.nodeId) || node.nodeType === 'end'
|
|
|
|
|
- }) || null
|
|
|
|
|
- )
|
|
|
|
|
-})
|
|
|
|
|
-const outputNodeResultValue = computed(() => outputNodeState.value?.track?.output_variable ?? null)
|
|
|
|
|
-const outputNodeResultText = computed(() => formatDisplayValue(outputNodeResultValue.value))
|
|
|
|
|
|
|
|
|
|
const hasExecutionData = computed(
|
|
const hasExecutionData = computed(
|
|
|
() => !!currentExecution.value || !!submittedParams.value || !!runnerStore.agentResult
|
|
() => !!currentExecution.value || !!submittedParams.value || !!runnerStore.agentResult
|
|
@@ -127,7 +130,7 @@ const detailMeta = computed(() => [
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
const finalResultValue = computed(() =>
|
|
const finalResultValue = computed(() =>
|
|
|
- extractFinalResult(runnerStore.agentResult || currentExecution.value?.result)
|
|
|
|
|
|
|
+ extractFinalResult(runnerStore.agentResult?.data?.result || currentExecution.value?.result)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const finalResultText = computed(() => formatDisplayValue(finalResultValue.value))
|
|
const finalResultText = computed(() => formatDisplayValue(finalResultValue.value))
|
|
@@ -136,6 +139,111 @@ const detailOutputText = computed(() => formatDisplayValue(finalResultValue.valu
|
|
|
const detailStatusClassName = computed(() =>
|
|
const detailStatusClassName = computed(() =>
|
|
|
statusClass(currentExecution.value?.status || runnerStore.status)
|
|
statusClass(currentExecution.value?.status || runnerStore.status)
|
|
|
)
|
|
)
|
|
|
|
|
+const triggerWebhookDebugUrl = computed(() => {
|
|
|
|
|
+ return `${(activeNode.value as any)?.data?.webhook_debug_url || ''}`.trim()
|
|
|
|
|
+})
|
|
|
|
|
+const triggerScheduleNextRunText = computed(() => {
|
|
|
|
|
+ if (activeNodeType.value !== 'trigger-schedule') return ''
|
|
|
|
|
+
|
|
|
|
|
+ const data = (activeNode.value as any)?.data || {}
|
|
|
|
|
+ const visualConfig = data.visual_config || {}
|
|
|
|
|
+ const frequency = `${data.frequency || 'daily'}`
|
|
|
|
|
+ if (data.mode === 'cron' && `${data.cron_expression || ''}`.trim()) {
|
|
|
|
|
+ return `${data.cron_expression || ''}`.trim()
|
|
|
|
|
+ }
|
|
|
|
|
+ const now = new Date()
|
|
|
|
|
+ const createCandidate = (base: Date, hour: number, minute: number) => {
|
|
|
|
|
+ const candidate = new Date(base)
|
|
|
|
|
+ candidate.setHours(hour, minute, 0, 0)
|
|
|
|
|
+ return candidate
|
|
|
|
|
+ }
|
|
|
|
|
+ const parseTime = (value?: string) => {
|
|
|
|
|
+ const [hourText = '0', minuteText = '0'] = `${value || '00:00'}`.split(':')
|
|
|
|
|
+ return {
|
|
|
|
|
+ hour: Math.min(23, Math.max(0, Number(hourText))),
|
|
|
|
|
+ minute: Math.min(59, Math.max(0, Number(minuteText)))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const getLastDayOfMonth = (date: Date) => {
|
|
|
|
|
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let nextRun: Date | null = null
|
|
|
|
|
+ if (frequency === 'hourly') {
|
|
|
|
|
+ const minute = Math.min(59, Math.max(0, Number(visualConfig.minute ?? 0)))
|
|
|
|
|
+ const cursor = new Date(now)
|
|
|
|
|
+ cursor.setSeconds(0, 0)
|
|
|
|
|
+ cursor.setMinutes(minute, 0, 0)
|
|
|
|
|
+ if (cursor <= now) {
|
|
|
|
|
+ cursor.setHours(cursor.getHours() + 1)
|
|
|
|
|
+ cursor.setMinutes(minute, 0, 0)
|
|
|
|
|
+ }
|
|
|
|
|
+ nextRun = cursor
|
|
|
|
|
+ } else if (frequency === 'weekly') {
|
|
|
|
|
+ const { hour, minute } = parseTime(visualConfig.time)
|
|
|
|
|
+ const weekdayMap: Record<string, number> = {
|
|
|
|
|
+ sun: 0,
|
|
|
|
|
+ mon: 1,
|
|
|
|
|
+ tue: 2,
|
|
|
|
|
+ wed: 3,
|
|
|
|
|
+ thu: 4,
|
|
|
|
|
+ fri: 5,
|
|
|
|
|
+ sat: 6
|
|
|
|
|
+ }
|
|
|
|
|
+ const weekdays = new Set(
|
|
|
|
|
+ (Array.isArray(visualConfig.weekdays) ? visualConfig.weekdays : ['sun'])
|
|
|
|
|
+ .map((item: string) => weekdayMap[item])
|
|
|
|
|
+ .filter((item: number) => Number.isInteger(item))
|
|
|
|
|
+ )
|
|
|
|
|
+ const cursor = new Date(now)
|
|
|
|
|
+ cursor.setHours(0, 0, 0, 0)
|
|
|
|
|
+ for (let dayOffset = 0; dayOffset < 370; dayOffset += 1) {
|
|
|
|
|
+ const candidate = createCandidate(cursor, hour, minute)
|
|
|
|
|
+ if (weekdays.has(candidate.getDay()) && candidate > now) {
|
|
|
|
|
+ nextRun = candidate
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ cursor.setDate(cursor.getDate() + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (frequency === 'monthly') {
|
|
|
|
|
+ const { hour, minute } = parseTime(visualConfig.time)
|
|
|
|
|
+ const monthlyDays = Array.isArray(visualConfig.monthly_days) ? visualConfig.monthly_days : [1]
|
|
|
|
|
+ const cursor = new Date(now)
|
|
|
|
|
+ cursor.setHours(0, 0, 0, 0)
|
|
|
|
|
+ for (let dayOffset = 0; dayOffset < 740; dayOffset += 1) {
|
|
|
|
|
+ const candidate = createCandidate(cursor, hour, minute)
|
|
|
|
|
+ const currentDate = candidate.getDate()
|
|
|
|
|
+ const isLastDay = currentDate === getLastDayOfMonth(candidate)
|
|
|
|
|
+ const matched = monthlyDays.some((item: number | 'last') =>
|
|
|
|
|
+ item === 'last' ? isLastDay : Number(item) === currentDate
|
|
|
|
|
+ )
|
|
|
|
|
+ if (matched && candidate > now) {
|
|
|
|
|
+ nextRun = candidate
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ cursor.setDate(cursor.getDate() + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const { hour, minute } = parseTime(visualConfig.time)
|
|
|
|
|
+ const candidate = createCandidate(now, hour, minute)
|
|
|
|
|
+ if (candidate <= now) {
|
|
|
|
|
+ candidate.setDate(candidate.getDate() + 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ nextRun = candidate
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!nextRun) return `${data.cron_expression || ''}`.trim()
|
|
|
|
|
+
|
|
|
|
|
+ return new Intl.DateTimeFormat('en-US', {
|
|
|
|
|
+ weekday: 'long',
|
|
|
|
|
+ month: 'long',
|
|
|
|
|
+ day: 'numeric',
|
|
|
|
|
+ year: 'numeric',
|
|
|
|
|
+ hour: 'numeric',
|
|
|
|
|
+ minute: '2-digit',
|
|
|
|
|
+ hour12: true
|
|
|
|
|
+ }).format(nextRun)
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
const initializeInputValues = () => {
|
|
const initializeInputValues = () => {
|
|
|
startVariables.value.forEach((variable) => {
|
|
startVariables.value.forEach((variable) => {
|
|
@@ -166,10 +274,20 @@ const resetValidation = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
|
- () => props.visible,
|
|
|
|
|
- (visible) => {
|
|
|
|
|
|
|
+ () => [props.visible, props.initialTab, showInputTab.value, showTriggerTab.value],
|
|
|
|
|
+ ([visible]) => {
|
|
|
if (visible) {
|
|
if (visible) {
|
|
|
- activeTab.value = 'input'
|
|
|
|
|
|
|
+ if (props.initialTab === 'trigger' && showTriggerTab.value) {
|
|
|
|
|
+ activeTab.value = 'trigger'
|
|
|
|
|
+ } else if (props.initialTab === 'input' && showInputTab.value) {
|
|
|
|
|
+ activeTab.value = 'input'
|
|
|
|
|
+ } else if (showTriggerTab.value) {
|
|
|
|
|
+ activeTab.value = 'trigger'
|
|
|
|
|
+ } else if (showInputTab.value) {
|
|
|
|
|
+ activeTab.value = 'input'
|
|
|
|
|
+ } else {
|
|
|
|
|
+ activeTab.value = 'detail'
|
|
|
|
|
+ }
|
|
|
resetValidation()
|
|
resetValidation()
|
|
|
initializeInputValues()
|
|
initializeInputValues()
|
|
|
return
|
|
return
|
|
@@ -184,6 +302,10 @@ const closeDrawer = () => {
|
|
|
emit('update:visible', false)
|
|
emit('update:visible', false)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const handleStopTriggerListening = () => {
|
|
|
|
|
+ runnerStore.stopRunner()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const buildExecuteParams = () => {
|
|
const buildExecuteParams = () => {
|
|
|
resetValidation()
|
|
resetValidation()
|
|
|
const params: Record<string, any> = {}
|
|
const params: Record<string, any> = {}
|
|
@@ -428,7 +550,7 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
|
|
|
|
|
<div class="content">
|
|
<div class="content">
|
|
|
<el-tabs v-model="activeTab" class="runner-tabs">
|
|
<el-tabs v-model="activeTab" class="runner-tabs">
|
|
|
- <el-tab-pane label="输入" name="input">
|
|
|
|
|
|
|
+ <el-tab-pane v-if="showInputTab" label="输入" name="input">
|
|
|
<InputTab
|
|
<InputTab
|
|
|
:start-node="startNode"
|
|
:start-node="startNode"
|
|
|
:visible-variables="visibleVariables"
|
|
:visible-variables="visibleVariables"
|
|
@@ -439,16 +561,34 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
@run="handleRunWorkflow"
|
|
@run="handleRunWorkflow"
|
|
|
/>
|
|
/>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane v-if="!props.inputOnly" label="结果" name="result">
|
|
|
|
|
|
|
+ <el-tab-pane v-if="showTriggerTab" label="触发" name="trigger">
|
|
|
|
|
+ <TriggerTab
|
|
|
|
|
+ :node-type="activeNodeType"
|
|
|
|
|
+ :is-listening="isRunning"
|
|
|
|
|
+ :webhook-debug-url="triggerWebhookDebugUrl"
|
|
|
|
|
+ :next-schedule-run-text="triggerScheduleNextRunText"
|
|
|
|
|
+ @stop="handleStopTriggerListening"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ <el-tab-pane
|
|
|
|
|
+ v-if="!props.inputOnly"
|
|
|
|
|
+ label="结果"
|
|
|
|
|
+ name="result"
|
|
|
|
|
+ :disabled="runnerStore.status === 'idle'"
|
|
|
|
|
+ >
|
|
|
<ResultTab
|
|
<ResultTab
|
|
|
- :has-output-node="hasOutputNode"
|
|
|
|
|
:has-execution-data="hasExecutionData"
|
|
:has-execution-data="hasExecutionData"
|
|
|
:is-running="isRunning"
|
|
:is-running="isRunning"
|
|
|
- :final-result-text="outputNodeResultText"
|
|
|
|
|
- :final-result-value="outputNodeResultValue"
|
|
|
|
|
|
|
+ :final-result-text="finalResultText"
|
|
|
|
|
+ :final-result-value="finalResultValue"
|
|
|
/>
|
|
/>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane v-if="!props.inputOnly" label="详情" name="detail">
|
|
|
|
|
|
|
+ <el-tab-pane
|
|
|
|
|
+ v-if="!props.inputOnly"
|
|
|
|
|
+ label="详情"
|
|
|
|
|
+ name="detail"
|
|
|
|
|
+ :disabled="runnerStore.status === 'idle'"
|
|
|
|
|
+ >
|
|
|
<DetailTab
|
|
<DetailTab
|
|
|
:has-execution-data="hasExecutionData"
|
|
:has-execution-data="hasExecutionData"
|
|
|
:status-class-name="detailStatusClassName"
|
|
:status-class-name="detailStatusClassName"
|
|
@@ -462,7 +602,12 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
:detail-output-text="detailOutputText"
|
|
:detail-output-text="detailOutputText"
|
|
|
/>
|
|
/>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
- <el-tab-pane v-if="!props.inputOnly" label="追踪" name="trace">
|
|
|
|
|
|
|
+ <el-tab-pane
|
|
|
|
|
+ v-if="!props.inputOnly"
|
|
|
|
|
+ label="追踪"
|
|
|
|
|
+ name="trace"
|
|
|
|
|
+ :disabled="runnerStore.status === 'idle'"
|
|
|
|
|
+ >
|
|
|
<TraceTab :trace-nodes="traceNodes" />
|
|
<TraceTab :trace-nodes="traceNodes" />
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
</el-tabs>
|
|
</el-tabs>
|
|
@@ -473,6 +618,8 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
<style lang="less" scoped>
|
|
|
.runner {
|
|
.runner {
|
|
|
|
|
+ z-index: 999;
|
|
|
|
|
+ font-size: 13px;
|
|
|
.drawer {
|
|
.drawer {
|
|
|
position: fixed;
|
|
position: fixed;
|
|
|
top: 60px;
|
|
top: 60px;
|
|
@@ -480,7 +627,7 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
bottom: 10px;
|
|
bottom: 10px;
|
|
|
width: 420px;
|
|
width: 420px;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
- z-index: 1000;
|
|
|
|
|
|
|
+ z-index: 1001;
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
@@ -499,6 +646,8 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
|
|
|
|
|
h4 {
|
|
h4 {
|
|
|
margin: 0;
|
|
margin: 0;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -518,6 +667,10 @@ function statusClass(status?: NodeStatus | RunnerStatus | null) {
|
|
|
margin-bottom: 0;
|
|
margin-bottom: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ :deep(.el-tabs__item) {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
:deep(.el-tabs__nav-scroll) {
|
|
:deep(.el-tabs__nav-scroll) {
|
|
|
padding-left: 20px;
|
|
padding-left: 20px;
|
|
|
}
|
|
}
|