mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 16:56:40 +08:00
feat: 添加工作流执行配置和并行数设置,优化工作流管理功能
This commit is contained in:
@@ -1489,6 +1489,10 @@ export interface Workflow {
|
|||||||
actions?: any[]
|
actions?: any[]
|
||||||
// 动作流
|
// 动作流
|
||||||
flows?: any[]
|
flows?: any[]
|
||||||
|
// 工作流执行配置
|
||||||
|
execution_config?: { [key: string]: any }
|
||||||
|
// 工作流结构化执行状态
|
||||||
|
execution_state?: { [key: string]: any }
|
||||||
// 创建时间
|
// 创建时间
|
||||||
add_time?: string
|
add_time?: string
|
||||||
// 最后执行时间
|
// 最后执行时间
|
||||||
|
|||||||
@@ -25,9 +25,67 @@ onConnect((connection: Connection) => {
|
|||||||
$toast.warning(t('dialog.workflowActions.invalidConnection'))
|
$toast.warning(t('dialog.workflowActions.invalidConnection'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addEdges(connection)
|
addEdges(
|
||||||
|
normalizeWorkflowEdge({
|
||||||
|
...connection,
|
||||||
|
id: `edge_${connection.source}_${connection.target}_${Date.now()}`,
|
||||||
|
type: 'animation',
|
||||||
|
animated: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 当前选中的流程边ID
|
||||||
|
const selectedEdgeId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 当前选中的动作节点ID
|
||||||
|
const selectedNodeId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 流程边配置表单
|
||||||
|
const edgeForm = ref({
|
||||||
|
condition: '',
|
||||||
|
join_policy: '',
|
||||||
|
branch_policy: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 动作节点执行配置表单
|
||||||
|
const nodeForm = ref({
|
||||||
|
inputs_text: '',
|
||||||
|
outputs_text: '',
|
||||||
|
join_policy: '',
|
||||||
|
fail_policy: '',
|
||||||
|
branch_policy: '',
|
||||||
|
concurrency_key: '',
|
||||||
|
timeout: null as number | null,
|
||||||
|
retry_max_attempts: null as number | null,
|
||||||
|
retry_interval: null as number | null,
|
||||||
|
retry_backoff: null as number | null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 汇合策略选项
|
||||||
|
const joinPolicyOptions = computed(() => [
|
||||||
|
{ title: t('dialog.workflowActions.joinPolicyDefault'), value: '' },
|
||||||
|
{ title: t('dialog.workflowActions.joinPolicyAllSuccess'), value: 'all_success' },
|
||||||
|
{ title: t('dialog.workflowActions.joinPolicyAnySuccess'), value: 'any_success' },
|
||||||
|
{ title: t('dialog.workflowActions.joinPolicyAllDone'), value: 'all_done' },
|
||||||
|
{ title: t('dialog.workflowActions.joinPolicyFailFast'), value: 'fail_fast' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 分支策略选项
|
||||||
|
const branchPolicyOptions = computed(() => [
|
||||||
|
{ title: t('dialog.workflowActions.branchPolicyDefault'), value: '' },
|
||||||
|
{ title: t('dialog.workflowActions.branchPolicyParallel'), value: 'parallel' },
|
||||||
|
{ title: t('dialog.workflowActions.branchPolicyExclusive'), value: 'exclusive' },
|
||||||
|
])
|
||||||
|
|
||||||
|
// 失败策略选项
|
||||||
|
const failPolicyOptions = computed(() => [
|
||||||
|
{ title: t('dialog.workflowActions.failPolicyDefault'), value: '' },
|
||||||
|
{ title: t('dialog.workflowActions.failPolicyStop'), value: 'stop' },
|
||||||
|
{ title: t('dialog.workflowActions.failPolicyContinue'), value: 'continue' },
|
||||||
|
{ title: t('dialog.workflowActions.failPolicyIgnore'), value: 'ignore' },
|
||||||
|
])
|
||||||
|
|
||||||
// 获取指定节点端口的类型(输入/输出)
|
// 获取指定节点端口的类型(输入/输出)
|
||||||
const getPortType = (node: GraphNode, handleId: string) => {
|
const getPortType = (node: GraphNode, handleId: string) => {
|
||||||
// 检查是否是输入端口(对应 handleBounds.target)
|
// 检查是否是输入端口(对应 handleBounds.target)
|
||||||
@@ -59,6 +117,341 @@ const isValidConnection = (connection: Connection) => {
|
|||||||
return sourcePortType === 'output' && targetPortType === 'input' && connection.source !== connection.target
|
return sourcePortType === 'output' && targetPortType === 'input' && connection.source !== connection.target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 读取流程边扩展配置,兼容后端支持的顶层字段与 data 字段
|
||||||
|
const getEdgeConfigValue = (edge: any, key: string) => {
|
||||||
|
return edge?.[key] ?? edge?.data?.[key] ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一流程边数据结构,确保条件和汇合策略能被后端读取
|
||||||
|
const normalizeWorkflowEdge = (edge: any) => {
|
||||||
|
const condition = String(getEdgeConfigValue(edge, 'condition') || '').trim()
|
||||||
|
const joinPolicy = String(getEdgeConfigValue(edge, 'join_policy') || '').trim()
|
||||||
|
const branchPolicy = String(getEdgeConfigValue(edge, 'branch_policy') || '').trim()
|
||||||
|
const edgeClass = String(edge?.class || '')
|
||||||
|
.replace(/\bworkflow-conditional-edge\b/g, '')
|
||||||
|
.trim()
|
||||||
|
const data = {
|
||||||
|
...(edge?.data || {}),
|
||||||
|
condition: condition || undefined,
|
||||||
|
join_policy: joinPolicy || undefined,
|
||||||
|
branch_policy: branchPolicy || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
animated: edge?.animated ?? true,
|
||||||
|
type: edge?.type || 'animation',
|
||||||
|
label: condition ? t('dialog.workflowActions.edgeConditionalLabel') : undefined,
|
||||||
|
class: [edgeClass, condition ? 'workflow-conditional-edge' : ''].filter(Boolean).join(' ') || undefined,
|
||||||
|
condition: condition || undefined,
|
||||||
|
join_policy: joinPolicy || undefined,
|
||||||
|
branch_policy: branchPolicy || undefined,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标准化所有流程边,导入和保存前都会调用
|
||||||
|
const normalizeWorkflowEdges = () => {
|
||||||
|
edges.value = (edges.value || []).map(edge => normalizeWorkflowEdge(edge))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断扩展配置是否为空,避免旧 data 中的空值覆盖顶层字段
|
||||||
|
const isEmptyConfigValue = (value: any) => {
|
||||||
|
if (value === undefined || value === null || value === '') return true
|
||||||
|
if (Array.isArray(value)) return value.length === 0
|
||||||
|
if (typeof value === 'object') return Object.keys(value).length === 0
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取动作节点扩展配置,兼容顶层字段与 data 字段
|
||||||
|
const getNodeConfigValue = (node: any, key: string) => {
|
||||||
|
const nodeValue = node?.[key]
|
||||||
|
if (!isEmptyConfigValue(nodeValue)) return nodeValue
|
||||||
|
const dataValue = node?.data?.[key]
|
||||||
|
return isEmptyConfigValue(dataValue) ? undefined : dataValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将输入声明统一为路径数组
|
||||||
|
const normalizeInputPaths = (value: any) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(item => String(item).trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value
|
||||||
|
.split(/[\n,]+/)
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JSON 形式的结构化配置
|
||||||
|
const parseStructuredConfig = (value: string, label: string) => {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return undefined
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
if (parsed && (Array.isArray(parsed) || typeof parsed === 'object')) {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
throw new Error(t('dialog.workflowActions.invalidJsonConfig', { label }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试把存量结构化配置标准化为对象或数组
|
||||||
|
const normalizeStructuredConfig = (value: any) => {
|
||||||
|
if (isEmptyConfigValue(value)) return undefined
|
||||||
|
if (Array.isArray(value) || typeof value === 'object') return value
|
||||||
|
if (typeof value !== 'string') return undefined
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
return parsed && (Array.isArray(parsed) || typeof parsed === 'object') ? parsed : undefined
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将结构化配置格式化为面板中的 JSON 文本
|
||||||
|
const stringifyStructuredConfig = (value: any) => {
|
||||||
|
const normalizedValue = normalizeStructuredConfig(value)
|
||||||
|
return normalizedValue ? JSON.stringify(normalizedValue, null, 2) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数值字段统一清洗,空值会在保存时被移除
|
||||||
|
const normalizeNumber = (value: any, minValue = 0, integer = false) => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined
|
||||||
|
const numberValue = Number(value)
|
||||||
|
if (!Number.isFinite(numberValue) || numberValue < minValue) return undefined
|
||||||
|
return integer ? Math.floor(numberValue) : numberValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取节点重试策略
|
||||||
|
const normalizeRetryConfig = (value: any) => {
|
||||||
|
const retryConfig = normalizeStructuredConfig(value)
|
||||||
|
return retryConfig && !Array.isArray(retryConfig) ? retryConfig : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据面板表单构造重试策略
|
||||||
|
const buildRetryConfigFromForm = () => {
|
||||||
|
const retryConfig: Record<string, number> = {}
|
||||||
|
const maxAttempts = normalizeNumber(nodeForm.value.retry_max_attempts, 1, true)
|
||||||
|
const interval = normalizeNumber(nodeForm.value.retry_interval, 0)
|
||||||
|
const backoff = normalizeNumber(nodeForm.value.retry_backoff, 1)
|
||||||
|
if (maxAttempts !== undefined) retryConfig.max_attempts = maxAttempts
|
||||||
|
if (interval !== undefined) retryConfig.interval = interval
|
||||||
|
if (backoff !== undefined) retryConfig.backoff = backoff
|
||||||
|
return Object.keys(retryConfig).length ? retryConfig : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一动作节点数据结构,确保执行策略能被后端读取
|
||||||
|
const normalizeWorkflowNode = (node: any) => {
|
||||||
|
const inputPaths = normalizeInputPaths(getNodeConfigValue(node, 'inputs'))
|
||||||
|
const outputs = normalizeStructuredConfig(getNodeConfigValue(node, 'outputs'))
|
||||||
|
const joinPolicy = String(getNodeConfigValue(node, 'join_policy') || '').trim()
|
||||||
|
const failPolicy = String(getNodeConfigValue(node, 'fail_policy') || '').trim()
|
||||||
|
const branchPolicy = String(getNodeConfigValue(node, 'branch_policy') || '').trim()
|
||||||
|
const concurrencyKey = String(getNodeConfigValue(node, 'concurrency_key') || '').trim()
|
||||||
|
const timeout = normalizeNumber(getNodeConfigValue(node, 'timeout'), 1, true)
|
||||||
|
const retryConfig = normalizeRetryConfig(getNodeConfigValue(node, 'retry'))
|
||||||
|
const retry = Object.keys(retryConfig).length ? retryConfig : undefined
|
||||||
|
const data = {
|
||||||
|
...(node?.data || {}),
|
||||||
|
inputs: inputPaths.length ? inputPaths : undefined,
|
||||||
|
outputs: outputs || undefined,
|
||||||
|
join_policy: joinPolicy || undefined,
|
||||||
|
fail_policy: failPolicy || undefined,
|
||||||
|
branch_policy: branchPolicy || undefined,
|
||||||
|
concurrency_key: concurrencyKey || undefined,
|
||||||
|
timeout: timeout || undefined,
|
||||||
|
retry,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
inputs: inputPaths.length ? inputPaths : undefined,
|
||||||
|
outputs: outputs || undefined,
|
||||||
|
join_policy: joinPolicy || undefined,
|
||||||
|
fail_policy: failPolicy || undefined,
|
||||||
|
branch_policy: branchPolicy || undefined,
|
||||||
|
concurrency_key: concurrencyKey || undefined,
|
||||||
|
timeout: timeout || undefined,
|
||||||
|
retry,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标准化所有动作节点,导入和保存前都会调用
|
||||||
|
const normalizeWorkflowNodes = () => {
|
||||||
|
nodes.value = (nodes.value || []).map(node => normalizeWorkflowNode(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点名称,便于在边设置面板展示流转关系
|
||||||
|
const getNodeName = (nodeId?: string) => {
|
||||||
|
const node = nodes.value.find(item => item.id === nodeId)
|
||||||
|
return (node as any)?.name || node?.data?.label || nodeId || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中流程边时打开设置面板
|
||||||
|
function handleEdgeClick(params: any) {
|
||||||
|
const edge = params?.edge
|
||||||
|
if (!edge) return
|
||||||
|
selectedNodeId.value = null
|
||||||
|
selectedEdgeId.value = edge.id
|
||||||
|
edgeForm.value = {
|
||||||
|
condition: String(getEdgeConfigValue(edge, 'condition') || ''),
|
||||||
|
join_policy: String(getEdgeConfigValue(edge, 'join_policy') || ''),
|
||||||
|
branch_policy: String(getEdgeConfigValue(edge, 'branch_policy') || ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭流程边设置面板
|
||||||
|
function closeEdgeSettings() {
|
||||||
|
selectedEdgeId.value = null
|
||||||
|
edgeForm.value = {
|
||||||
|
condition: '',
|
||||||
|
join_policy: '',
|
||||||
|
branch_policy: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存流程边设置
|
||||||
|
function saveEdgeSettings() {
|
||||||
|
if (!selectedEdgeId.value) return
|
||||||
|
edges.value = edges.value.map(edge => {
|
||||||
|
if (edge.id !== selectedEdgeId.value) return edge
|
||||||
|
return normalizeWorkflowEdge({
|
||||||
|
...edge,
|
||||||
|
condition: edgeForm.value.condition,
|
||||||
|
join_policy: edgeForm.value.join_policy,
|
||||||
|
data: {
|
||||||
|
...(edge.data || {}),
|
||||||
|
condition: edgeForm.value.condition,
|
||||||
|
join_policy: edgeForm.value.join_policy,
|
||||||
|
branch_policy: edgeForm.value.branch_policy,
|
||||||
|
},
|
||||||
|
branch_policy: edgeForm.value.branch_policy,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
$toast.success(t('dialog.workflowActions.edgeSaveSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除当前选中的流程边
|
||||||
|
function deleteSelectedEdge() {
|
||||||
|
if (!selectedEdgeId.value) return
|
||||||
|
edges.value = edges.value.filter(edge => edge.id !== selectedEdgeId.value)
|
||||||
|
closeEdgeSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前选中的流程边
|
||||||
|
const selectedEdge = computed(() => {
|
||||||
|
if (!selectedEdgeId.value) return null
|
||||||
|
return edges.value.find(edge => edge.id === selectedEdgeId.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前选中的动作节点
|
||||||
|
const selectedNode = computed(() => {
|
||||||
|
if (!selectedNodeId.value) return null
|
||||||
|
return nodes.value.find(node => node.id === selectedNodeId.value) || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 将节点数据填入右侧执行配置面板
|
||||||
|
function fillNodeForm(node: any) {
|
||||||
|
const retryConfig = normalizeRetryConfig(getNodeConfigValue(node, 'retry'))
|
||||||
|
nodeForm.value = {
|
||||||
|
inputs_text: normalizeInputPaths(getNodeConfigValue(node, 'inputs')).join('\n'),
|
||||||
|
outputs_text: stringifyStructuredConfig(getNodeConfigValue(node, 'outputs')),
|
||||||
|
join_policy: String(getNodeConfigValue(node, 'join_policy') || ''),
|
||||||
|
fail_policy: String(getNodeConfigValue(node, 'fail_policy') || ''),
|
||||||
|
branch_policy: String(getNodeConfigValue(node, 'branch_policy') || ''),
|
||||||
|
concurrency_key: String(getNodeConfigValue(node, 'concurrency_key') || ''),
|
||||||
|
timeout: normalizeNumber(getNodeConfigValue(node, 'timeout'), 1, true) ?? null,
|
||||||
|
retry_max_attempts: normalizeNumber(retryConfig.max_attempts, 1, true) ?? null,
|
||||||
|
retry_interval: normalizeNumber(retryConfig.interval, 0) ?? null,
|
||||||
|
retry_backoff: normalizeNumber(retryConfig.backoff, 1) ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中动作节点时打开执行配置面板
|
||||||
|
function handleNodeClick(params: any) {
|
||||||
|
const node = params?.node
|
||||||
|
if (!node) return
|
||||||
|
if (node.name == '备注') return
|
||||||
|
selectedEdgeId.value = null
|
||||||
|
selectedNodeId.value = node.id
|
||||||
|
fillNodeForm(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭动作节点执行配置面板
|
||||||
|
function closeNodeSettings() {
|
||||||
|
selectedNodeId.value = null
|
||||||
|
nodeForm.value = {
|
||||||
|
inputs_text: '',
|
||||||
|
outputs_text: '',
|
||||||
|
join_policy: '',
|
||||||
|
fail_policy: '',
|
||||||
|
branch_policy: '',
|
||||||
|
concurrency_key: '',
|
||||||
|
timeout: null,
|
||||||
|
retry_max_attempts: null,
|
||||||
|
retry_interval: null,
|
||||||
|
retry_backoff: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据面板表单构造动作节点执行配置
|
||||||
|
function buildNodeConfigFromForm() {
|
||||||
|
return {
|
||||||
|
inputs: normalizeInputPaths(nodeForm.value.inputs_text),
|
||||||
|
outputs: parseStructuredConfig(nodeForm.value.outputs_text, t('dialog.workflowActions.nodeOutputsLabel')),
|
||||||
|
join_policy: nodeForm.value.join_policy,
|
||||||
|
fail_policy: nodeForm.value.fail_policy,
|
||||||
|
branch_policy: nodeForm.value.branch_policy,
|
||||||
|
concurrency_key: nodeForm.value.concurrency_key,
|
||||||
|
timeout: normalizeNumber(nodeForm.value.timeout, 1, true),
|
||||||
|
retry: buildRetryConfigFromForm(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存动作节点执行配置
|
||||||
|
function saveNodeSettings() {
|
||||||
|
if (!selectedNodeId.value) return
|
||||||
|
let nodeConfig: any
|
||||||
|
try {
|
||||||
|
nodeConfig = buildNodeConfigFromForm()
|
||||||
|
} catch (error: any) {
|
||||||
|
$toast.error(error.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nodes.value = nodes.value.map(node => {
|
||||||
|
if (node.id !== selectedNodeId.value) return node
|
||||||
|
return normalizeWorkflowNode({
|
||||||
|
...node,
|
||||||
|
inputs: nodeConfig.inputs,
|
||||||
|
outputs: nodeConfig.outputs,
|
||||||
|
join_policy: nodeConfig.join_policy,
|
||||||
|
fail_policy: nodeConfig.fail_policy,
|
||||||
|
branch_policy: nodeConfig.branch_policy,
|
||||||
|
concurrency_key: nodeConfig.concurrency_key,
|
||||||
|
timeout: nodeConfig.timeout,
|
||||||
|
retry: nodeConfig.retry,
|
||||||
|
data: {
|
||||||
|
...(node.data || {}),
|
||||||
|
inputs: nodeConfig.inputs,
|
||||||
|
outputs: nodeConfig.outputs,
|
||||||
|
join_policy: nodeConfig.join_policy,
|
||||||
|
fail_policy: nodeConfig.fail_policy,
|
||||||
|
branch_policy: nodeConfig.branch_policy,
|
||||||
|
concurrency_key: nodeConfig.concurrency_key,
|
||||||
|
timeout: nodeConfig.timeout,
|
||||||
|
retry: nodeConfig.retry,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
$toast.success(t('dialog.workflowActions.nodeSaveSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
// 自定义节点类型
|
// 自定义节点类型
|
||||||
const nodeTypes: Record<string, any> = ref({})
|
const nodeTypes: Record<string, any> = ref({})
|
||||||
|
|
||||||
@@ -142,8 +535,10 @@ function handleComponentClick(action: any) {
|
|||||||
// 调用API 编辑任务
|
// 调用API 编辑任务
|
||||||
async function updateWorkflow() {
|
async function updateWorkflow() {
|
||||||
// 更新节点和流程
|
// 更新节点和流程
|
||||||
workflowForm.value.actions = nodes
|
normalizeWorkflowNodes()
|
||||||
workflowForm.value.flows = edges
|
normalizeWorkflowEdges()
|
||||||
|
workflowForm.value.actions = nodes.value
|
||||||
|
workflowForm.value.flows = edges.value
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||||
@@ -166,6 +561,11 @@ function saveCodeString(type: string, code: any) {
|
|||||||
if (type === 'workflow') {
|
if (type === 'workflow') {
|
||||||
nodes.value = codeObject.actions || []
|
nodes.value = codeObject.actions || []
|
||||||
edges.value = codeObject.flows || []
|
edges.value = codeObject.flows || []
|
||||||
|
if (codeObject.execution_config) {
|
||||||
|
workflowForm.value.execution_config = codeObject.execution_config
|
||||||
|
}
|
||||||
|
normalizeWorkflowNodes()
|
||||||
|
normalizeWorkflowEdges()
|
||||||
}
|
}
|
||||||
importCodeDialog.value = false
|
importCodeDialog.value = false
|
||||||
$toast.success(t('dialog.workflowActions.importSuccess'))
|
$toast.success(t('dialog.workflowActions.importSuccess'))
|
||||||
@@ -178,7 +578,13 @@ function saveCodeString(type: string, code: any) {
|
|||||||
|
|
||||||
// 分享工作流程
|
// 分享工作流程
|
||||||
function shareWorkflow() {
|
function shareWorkflow() {
|
||||||
const codeString = JSON.stringify({ actions: nodes.value, flows: edges.value })
|
normalizeWorkflowNodes()
|
||||||
|
normalizeWorkflowEdges()
|
||||||
|
const codeString = JSON.stringify({
|
||||||
|
actions: nodes.value,
|
||||||
|
flows: edges.value,
|
||||||
|
execution_config: workflowForm.value.execution_config,
|
||||||
|
})
|
||||||
navigator.clipboard.writeText(codeString)
|
navigator.clipboard.writeText(codeString)
|
||||||
$toast.success(t('dialog.workflowActions.codeCopied'))
|
$toast.success(t('dialog.workflowActions.codeCopied'))
|
||||||
}
|
}
|
||||||
@@ -187,9 +593,31 @@ onMounted(() => {
|
|||||||
if (props.workflow) {
|
if (props.workflow) {
|
||||||
nodes.value = props.workflow.actions ?? []
|
nodes.value = props.workflow.actions ?? []
|
||||||
edges.value = props.workflow.flows ?? []
|
edges.value = props.workflow.flows ?? []
|
||||||
|
normalizeWorkflowNodes()
|
||||||
|
normalizeWorkflowEdges()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
edges,
|
||||||
|
() => {
|
||||||
|
if (selectedEdgeId.value && !selectedEdge.value) {
|
||||||
|
closeEdgeSettings()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
nodes,
|
||||||
|
() => {
|
||||||
|
if (selectedNodeId.value && !selectedNode.value) {
|
||||||
|
closeNodeSettings()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
// 判断是不是MACOS
|
// 判断是不是MACOS
|
||||||
const isMacOS = computed(() => {
|
const isMacOS = computed(() => {
|
||||||
return /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent)
|
return /Macintosh|MacIntel|MacPPC|Mac68K/.test(navigator.userAgent)
|
||||||
@@ -231,6 +659,8 @@ const isMacOS = computed(() => {
|
|||||||
:edge-updater-radius="10"
|
:edge-updater-radius="10"
|
||||||
@dragover="onDragOver"
|
@dragover="onDragOver"
|
||||||
@dragleave="onDragLeave"
|
@dragleave="onDragLeave"
|
||||||
|
@node-click="handleNodeClick"
|
||||||
|
@edge-click="handleEdgeClick"
|
||||||
:delete-key-code="isMacOS ? 'Backspace' : 'Delete'"
|
:delete-key-code="isMacOS ? 'Backspace' : 'Delete'"
|
||||||
auto-connect
|
auto-connect
|
||||||
>
|
>
|
||||||
@@ -243,6 +673,187 @@ const isMacOS = computed(() => {
|
|||||||
>
|
>
|
||||||
</DropzoneBackground>
|
</DropzoneBackground>
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
|
|
||||||
|
<div v-if="selectedEdge" class="workflow-edge-panel">
|
||||||
|
<div class="edge-panel-header">
|
||||||
|
<div class="edge-panel-title">
|
||||||
|
<VIcon icon="mdi-source-branch" size="20" />
|
||||||
|
<span>{{ t('dialog.workflowActions.edgeSettingsTitle') }}</span>
|
||||||
|
</div>
|
||||||
|
<VBtn icon variant="text" size="small" @click="closeEdgeSettings">
|
||||||
|
<VIcon icon="mdi-close" />
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edge-route">
|
||||||
|
<span>{{ getNodeName(selectedEdge.source) }}</span>
|
||||||
|
<VIcon icon="mdi-arrow-right" size="18" />
|
||||||
|
<span>{{ getNodeName(selectedEdge.target) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VTextarea
|
||||||
|
v-model="edgeForm.condition"
|
||||||
|
:label="t('dialog.workflowActions.edgeConditionLabel')"
|
||||||
|
:placeholder="t('dialog.workflowActions.edgeConditionPlaceholder')"
|
||||||
|
rows="3"
|
||||||
|
auto-grow
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VSelect
|
||||||
|
v-model="edgeForm.join_policy"
|
||||||
|
:items="joinPolicyOptions"
|
||||||
|
:label="t('dialog.workflowActions.edgeJoinPolicyLabel')"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VSelect
|
||||||
|
v-model="edgeForm.branch_policy"
|
||||||
|
:items="branchPolicyOptions"
|
||||||
|
:label="t('dialog.workflowActions.edgeBranchPolicyLabel')"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="edge-panel-actions">
|
||||||
|
<VBtn icon variant="text" color="error" @click="deleteSelectedEdge">
|
||||||
|
<VIcon icon="mdi-delete" />
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn variant="text" @click="closeEdgeSettings">
|
||||||
|
{{ t('dialog.workflowActions.edgeCancel') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn color="primary" @click="saveEdgeSettings">
|
||||||
|
{{ t('dialog.workflowActions.edgeSave') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedNode" class="workflow-edge-panel workflow-node-panel">
|
||||||
|
<div class="edge-panel-header">
|
||||||
|
<div class="edge-panel-title">
|
||||||
|
<VIcon icon="mdi-tune-variant" size="20" />
|
||||||
|
<span>{{ t('dialog.workflowActions.nodeSettingsTitle') }}</span>
|
||||||
|
</div>
|
||||||
|
<VBtn icon variant="text" size="small" @click="closeNodeSettings">
|
||||||
|
<VIcon icon="mdi-close" />
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edge-route">
|
||||||
|
<VIcon icon="mdi-checkbox-blank-circle-outline" size="16" />
|
||||||
|
<span>{{ getNodeName(selectedNode.id) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VSelect
|
||||||
|
v-model="nodeForm.join_policy"
|
||||||
|
:items="joinPolicyOptions"
|
||||||
|
:label="t('dialog.workflowActions.nodeJoinPolicyLabel')"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VSelect
|
||||||
|
v-model="nodeForm.fail_policy"
|
||||||
|
:items="failPolicyOptions"
|
||||||
|
:label="t('dialog.workflowActions.nodeFailPolicyLabel')"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VSelect
|
||||||
|
v-model="nodeForm.branch_policy"
|
||||||
|
:items="branchPolicyOptions"
|
||||||
|
:label="t('dialog.workflowActions.nodeBranchPolicyLabel')"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VTextField
|
||||||
|
v-model="nodeForm.concurrency_key"
|
||||||
|
:label="t('dialog.workflowActions.nodeConcurrencyKeyLabel')"
|
||||||
|
:placeholder="t('dialog.workflowActions.nodeConcurrencyKeyPlaceholder')"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="workflow-number-grid">
|
||||||
|
<VTextField
|
||||||
|
v-model.number="nodeForm.timeout"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:label="t('dialog.workflowActions.nodeTimeoutLabel')"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
<VTextField
|
||||||
|
v-model.number="nodeForm.retry_max_attempts"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:label="t('dialog.workflowActions.nodeRetryAttemptsLabel')"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
<VTextField
|
||||||
|
v-model.number="nodeForm.retry_interval"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.1"
|
||||||
|
:label="t('dialog.workflowActions.nodeRetryIntervalLabel')"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
<VTextField
|
||||||
|
v-model.number="nodeForm.retry_backoff"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="0.1"
|
||||||
|
:label="t('dialog.workflowActions.nodeRetryBackoffLabel')"
|
||||||
|
clearable
|
||||||
|
variant="outlined"
|
||||||
|
density="comfortable"
|
||||||
|
hide-details="auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="edge-panel-actions">
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn variant="text" @click="closeNodeSettings">
|
||||||
|
{{ t('dialog.workflowActions.edgeCancel') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn color="primary" @click="saveNodeSettings">
|
||||||
|
{{ t('dialog.workflowActions.edgeSave') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<WorkflowSidebar @component-click="handleComponentClick" />
|
<WorkflowSidebar @component-click="handleComponentClick" />
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -285,6 +896,74 @@ const isMacOS = computed(() => {
|
|||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-edge-panel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 120;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgb(var(--v-theme-primary));
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgb(var(--v-theme-surface));
|
||||||
|
box-shadow: 0 8px 24px rgba(var(--v-shadow-key-umbra-color), 0.32);
|
||||||
|
gap: 14px;
|
||||||
|
inline-size: min(360px, calc(100vw - 32px));
|
||||||
|
inset-block-start: 20px;
|
||||||
|
inset-inline-end: 20px;
|
||||||
|
max-block-size: calc(100% - 40px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-node-panel {
|
||||||
|
inline-size: min(420px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-panel-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-route {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font-size: 13px;
|
||||||
|
gap: 8px;
|
||||||
|
padding-block: 8px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-number-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.vue-flow__minimap {
|
.vue-flow__minimap {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
@@ -345,9 +1024,26 @@ const isMacOS = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-flow__edge.workflow-conditional-edge {
|
||||||
|
.vue-flow__edge-path {
|
||||||
|
stroke: rgb(var(--v-theme-warning));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (width <= 600px) {
|
@media screen and (width <= 600px) {
|
||||||
.vue-flow__minimap {
|
.vue-flow__minimap {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-edge-panel {
|
||||||
|
inline-size: auto;
|
||||||
|
inset-block: auto 88px;
|
||||||
|
inset-inline: 16px;
|
||||||
|
max-block-size: min(72vh, calc(100% - 112px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-number-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,9 +37,35 @@ const workflowForm = ref<Workflow>(
|
|||||||
event_type: undefined,
|
event_type: undefined,
|
||||||
state: 'P',
|
state: 'P',
|
||||||
run_count: 0,
|
run_count: 0,
|
||||||
|
execution_config: {},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 将并发数清洗为正整数,空值表示使用后端默认值
|
||||||
|
const normalizePositiveInteger = (value: any) => {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined
|
||||||
|
const numberValue = Number(value)
|
||||||
|
if (!Number.isFinite(numberValue) || numberValue < 1) return undefined
|
||||||
|
return Math.floor(numberValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工作流级执行配置中的最大并行数
|
||||||
|
const workflowMaxWorkers = computed<number | null>({
|
||||||
|
get() {
|
||||||
|
return normalizePositiveInteger(workflowForm.value.execution_config?.max_workers) ?? null
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const executionConfig = { ...(workflowForm.value.execution_config || {}) }
|
||||||
|
const maxWorkers = normalizePositiveInteger(value)
|
||||||
|
if (maxWorkers) {
|
||||||
|
executionConfig.max_workers = maxWorkers
|
||||||
|
} else {
|
||||||
|
delete executionConfig.max_workers
|
||||||
|
}
|
||||||
|
workflowForm.value.execution_config = Object.keys(executionConfig).length ? executionConfig : undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// 监听props变化,处理存量数据
|
// 监听props变化,处理存量数据
|
||||||
watch(
|
watch(
|
||||||
() => props.workflow,
|
() => props.workflow,
|
||||||
@@ -49,7 +75,10 @@ watch(
|
|||||||
if (!newWorkflow.trigger_type) {
|
if (!newWorkflow.trigger_type) {
|
||||||
newWorkflow.trigger_type = 'timer'
|
newWorkflow.trigger_type = 'timer'
|
||||||
}
|
}
|
||||||
workflowForm.value = { ...newWorkflow }
|
workflowForm.value = {
|
||||||
|
...newWorkflow,
|
||||||
|
execution_config: { ...(newWorkflow.execution_config || {}) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -99,6 +128,18 @@ watch(
|
|||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
// 保存前统一清洗工作流执行配置
|
||||||
|
function normalizeWorkflowExecutionConfig() {
|
||||||
|
const executionConfig = { ...(workflowForm.value.execution_config || {}) }
|
||||||
|
const maxWorkers = normalizePositiveInteger(executionConfig.max_workers)
|
||||||
|
if (maxWorkers) {
|
||||||
|
executionConfig.max_workers = maxWorkers
|
||||||
|
} else {
|
||||||
|
delete executionConfig.max_workers
|
||||||
|
}
|
||||||
|
workflowForm.value.execution_config = Object.keys(executionConfig).length ? executionConfig : undefined
|
||||||
|
}
|
||||||
|
|
||||||
// 调用API 新增任务
|
// 调用API 新增任务
|
||||||
async function addWorkflow() {
|
async function addWorkflow() {
|
||||||
if (!workflowForm.value.name) {
|
if (!workflowForm.value.name) {
|
||||||
@@ -122,6 +163,7 @@ async function addWorkflow() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeWorkflowExecutionConfig()
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
|
const result: { [key: string]: string } = await api.post('workflow/', workflowForm.value)
|
||||||
@@ -160,6 +202,7 @@ async function editWorkflow() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeWorkflowExecutionConfig()
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
const result: { [key: string]: string } = await api.put(`workflow/${workflowForm.value.id}`, workflowForm.value)
|
||||||
@@ -256,6 +299,16 @@ onMounted(() => {
|
|||||||
prepend-inner-icon="mdi-text-box-outline"
|
prepend-inner-icon="mdi-text-box-outline"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12">
|
||||||
|
<VTextField
|
||||||
|
v-model.number="workflowMaxWorkers"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
clearable
|
||||||
|
:label="t('dialog.workflowAddEdit.maxWorkers')"
|
||||||
|
prepend-inner-icon="mdi-call-split"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|||||||
@@ -2415,6 +2415,7 @@ export default {
|
|||||||
namePlaceholder: 'Workflow name',
|
namePlaceholder: 'Workflow name',
|
||||||
desc: 'Description',
|
desc: 'Description',
|
||||||
descPlaceholder: 'Workflow description',
|
descPlaceholder: 'Workflow description',
|
||||||
|
maxWorkers: 'Max Parallel Actions',
|
||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
triggerType: 'Trigger Type',
|
triggerType: 'Trigger Type',
|
||||||
triggerTypeTimer: 'Timer Trigger',
|
triggerTypeTimer: 'Timer Trigger',
|
||||||
@@ -2465,6 +2466,42 @@ export default {
|
|||||||
importSuccess: 'Import successful!',
|
importSuccess: 'Import successful!',
|
||||||
importFailed: 'Import failed!',
|
importFailed: 'Import failed!',
|
||||||
codeCopied: 'Task workflow code copied to clipboard!',
|
codeCopied: 'Task workflow code copied to clipboard!',
|
||||||
|
edgeSettingsTitle: 'Flow Condition',
|
||||||
|
edgeConditionLabel: 'Condition',
|
||||||
|
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
||||||
|
edgeJoinPolicyLabel: 'Target Join Policy',
|
||||||
|
edgeBranchPolicyLabel: 'Source Branch Policy',
|
||||||
|
joinPolicyDefault: 'Default',
|
||||||
|
joinPolicyAllSuccess: 'All Success',
|
||||||
|
joinPolicyAnySuccess: 'Any Success',
|
||||||
|
joinPolicyAllDone: 'All Done',
|
||||||
|
joinPolicyFailFast: 'Fail Fast',
|
||||||
|
branchPolicyDefault: 'Default',
|
||||||
|
branchPolicyParallel: 'Parallel',
|
||||||
|
branchPolicyExclusive: 'Exclusive First',
|
||||||
|
failPolicyDefault: 'Default',
|
||||||
|
failPolicyStop: 'Stop on Failure',
|
||||||
|
failPolicyContinue: 'Continue on Failure',
|
||||||
|
failPolicyIgnore: 'Ignore Failure',
|
||||||
|
edgeConditionalLabel: 'Condition',
|
||||||
|
edgeSave: 'Save',
|
||||||
|
edgeCancel: 'Cancel',
|
||||||
|
edgeSaveSuccess: 'Flow condition saved',
|
||||||
|
nodeSettingsTitle: 'Action Execution Policy',
|
||||||
|
nodeInputsLabel: 'Input Declarations',
|
||||||
|
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
||||||
|
nodeOutputsLabel: 'Output Declarations',
|
||||||
|
nodeJoinPolicyLabel: 'Node Join Policy',
|
||||||
|
nodeFailPolicyLabel: 'Failure Policy',
|
||||||
|
nodeBranchPolicyLabel: 'Branch Policy',
|
||||||
|
nodeConcurrencyKeyLabel: 'Concurrency Key',
|
||||||
|
nodeConcurrencyKeyPlaceholder: 'downloader',
|
||||||
|
nodeTimeoutLabel: 'Timeout Seconds',
|
||||||
|
nodeRetryAttemptsLabel: 'Retry Attempts',
|
||||||
|
nodeRetryIntervalLabel: 'Retry Interval',
|
||||||
|
nodeRetryBackoffLabel: 'Backoff Factor',
|
||||||
|
nodeSaveSuccess: 'Action execution policy saved',
|
||||||
|
invalidJsonConfig: '{label} is not a valid JSON object or array',
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: 'Update Site Cookie & UA',
|
title: 'Update Site Cookie & UA',
|
||||||
|
|||||||
@@ -2367,6 +2367,7 @@ export default {
|
|||||||
namePlaceholder: '工作流名称',
|
namePlaceholder: '工作流名称',
|
||||||
desc: '描述',
|
desc: '描述',
|
||||||
descPlaceholder: '工作流描述',
|
descPlaceholder: '工作流描述',
|
||||||
|
maxWorkers: '最大并行数',
|
||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
triggerType: '触发类型',
|
triggerType: '触发类型',
|
||||||
triggerTypeTimer: '定时触发',
|
triggerTypeTimer: '定时触发',
|
||||||
@@ -2417,6 +2418,42 @@ export default {
|
|||||||
importSuccess: '导入成功!',
|
importSuccess: '导入成功!',
|
||||||
importFailed: '导入失败!',
|
importFailed: '导入失败!',
|
||||||
codeCopied: '任务流程代码已复制到剪贴板!',
|
codeCopied: '任务流程代码已复制到剪贴板!',
|
||||||
|
edgeSettingsTitle: '流程条件',
|
||||||
|
edgeConditionLabel: '流转条件',
|
||||||
|
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
||||||
|
edgeJoinPolicyLabel: '目标汇合策略',
|
||||||
|
edgeBranchPolicyLabel: '源分支策略',
|
||||||
|
joinPolicyDefault: '默认',
|
||||||
|
joinPolicyAllSuccess: '全部成功',
|
||||||
|
joinPolicyAnySuccess: '任一成功',
|
||||||
|
joinPolicyAllDone: '全部完成',
|
||||||
|
joinPolicyFailFast: '失败即停',
|
||||||
|
branchPolicyDefault: '默认',
|
||||||
|
branchPolicyParallel: '并行',
|
||||||
|
branchPolicyExclusive: '互斥首选',
|
||||||
|
failPolicyDefault: '默认',
|
||||||
|
failPolicyStop: '失败停止',
|
||||||
|
failPolicyContinue: '失败继续',
|
||||||
|
failPolicyIgnore: '忽略失败',
|
||||||
|
edgeConditionalLabel: '条件',
|
||||||
|
edgeSave: '保存',
|
||||||
|
edgeCancel: '取消',
|
||||||
|
edgeSaveSuccess: '流程条件已保存',
|
||||||
|
nodeSettingsTitle: '动作执行策略',
|
||||||
|
nodeInputsLabel: '输入声明',
|
||||||
|
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
||||||
|
nodeOutputsLabel: '输出声明',
|
||||||
|
nodeJoinPolicyLabel: '节点汇合策略',
|
||||||
|
nodeFailPolicyLabel: '失败策略',
|
||||||
|
nodeBranchPolicyLabel: '分支策略',
|
||||||
|
nodeConcurrencyKeyLabel: '并发互斥键',
|
||||||
|
nodeConcurrencyKeyPlaceholder: 'downloader',
|
||||||
|
nodeTimeoutLabel: '超时秒数',
|
||||||
|
nodeRetryAttemptsLabel: '重试次数',
|
||||||
|
nodeRetryIntervalLabel: '重试间隔',
|
||||||
|
nodeRetryBackoffLabel: '退避倍数',
|
||||||
|
nodeSaveSuccess: '动作执行策略已保存',
|
||||||
|
invalidJsonConfig: '{label} 不是有效的 JSON 对象或数组',
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: '更新站点Cookie & UA',
|
title: '更新站点Cookie & UA',
|
||||||
|
|||||||
@@ -2368,6 +2368,7 @@ export default {
|
|||||||
namePlaceholder: '工作流名稱',
|
namePlaceholder: '工作流名稱',
|
||||||
desc: '描述',
|
desc: '描述',
|
||||||
descPlaceholder: '工作流描述',
|
descPlaceholder: '工作流描述',
|
||||||
|
maxWorkers: '最大並行數',
|
||||||
enabled: '啟用',
|
enabled: '啟用',
|
||||||
triggerType: '觸發類型',
|
triggerType: '觸發類型',
|
||||||
triggerTypeTimer: '定時觸發',
|
triggerTypeTimer: '定時觸發',
|
||||||
@@ -2418,6 +2419,42 @@ export default {
|
|||||||
importSuccess: '匯入成功!',
|
importSuccess: '匯入成功!',
|
||||||
importFailed: '匯入失敗!',
|
importFailed: '匯入失敗!',
|
||||||
codeCopied: '任務流程代碼已複製到剪貼簿!',
|
codeCopied: '任務流程代碼已複製到剪貼簿!',
|
||||||
|
edgeSettingsTitle: '流程條件',
|
||||||
|
edgeConditionLabel: '流轉條件',
|
||||||
|
edgeConditionPlaceholder: 'outputs.A.items.count > 0',
|
||||||
|
edgeJoinPolicyLabel: '目標匯合策略',
|
||||||
|
edgeBranchPolicyLabel: '來源分支策略',
|
||||||
|
joinPolicyDefault: '預設',
|
||||||
|
joinPolicyAllSuccess: '全部成功',
|
||||||
|
joinPolicyAnySuccess: '任一成功',
|
||||||
|
joinPolicyAllDone: '全部完成',
|
||||||
|
joinPolicyFailFast: '失敗即停',
|
||||||
|
branchPolicyDefault: '預設',
|
||||||
|
branchPolicyParallel: '並行',
|
||||||
|
branchPolicyExclusive: '互斥首選',
|
||||||
|
failPolicyDefault: '預設',
|
||||||
|
failPolicyStop: '失敗停止',
|
||||||
|
failPolicyContinue: '失敗繼續',
|
||||||
|
failPolicyIgnore: '忽略失敗',
|
||||||
|
edgeConditionalLabel: '條件',
|
||||||
|
edgeSave: '儲存',
|
||||||
|
edgeCancel: '取消',
|
||||||
|
edgeSaveSuccess: '流程條件已儲存',
|
||||||
|
nodeSettingsTitle: '動作執行策略',
|
||||||
|
nodeInputsLabel: '輸入宣告',
|
||||||
|
nodeInputsPlaceholder: 'outputs.FetchTorrentsAction.torrents',
|
||||||
|
nodeOutputsLabel: '輸出宣告',
|
||||||
|
nodeJoinPolicyLabel: '節點匯合策略',
|
||||||
|
nodeFailPolicyLabel: '失敗策略',
|
||||||
|
nodeBranchPolicyLabel: '分支策略',
|
||||||
|
nodeConcurrencyKeyLabel: '並發互斥鍵',
|
||||||
|
nodeConcurrencyKeyPlaceholder: 'downloader',
|
||||||
|
nodeTimeoutLabel: '超時秒數',
|
||||||
|
nodeRetryAttemptsLabel: '重試次數',
|
||||||
|
nodeRetryIntervalLabel: '重試間隔',
|
||||||
|
nodeRetryBackoffLabel: '退避倍數',
|
||||||
|
nodeSaveSuccess: '動作執行策略已儲存',
|
||||||
|
invalidJsonConfig: '{label} 不是有效的 JSON 物件或陣列',
|
||||||
},
|
},
|
||||||
siteCookieUpdate: {
|
siteCookieUpdate: {
|
||||||
title: '更新站點Cookie & UA',
|
title: '更新站點Cookie & UA',
|
||||||
|
|||||||
Reference in New Issue
Block a user