新增: Conditions 启用 + 工作流 DAG 展示 + 决策记录

- 开启 conditions-eval feature flag(df-workflow 默认启用)
- 新增 WorkflowDagDisplay.vue 展示工作流节点/边/条件
- TaskDetail 工作流推进后自动获取并显示 DAG 结构
- 记录 2026-06-27 讨论决策到待决策.md
- 记录 Inbox 设计想法到 todo.md
This commit is contained in:
2026-06-28 01:17:36 +08:00
parent 22362a77b8
commit e4ceb0015b
5 changed files with 200 additions and 5 deletions

View File

@@ -0,0 +1,131 @@
<template>
<div v-if="dag" class="wf-dag">
<h4 class="wf-dag-title">{{ $t('taskDetail.workflowDagTitle') || '工作流结构' }}</h4>
<!-- 节点列表 -->
<div class="wf-dag-nodes">
<div v-for="node in dag.nodes" :key="node.id" class="wf-dag-node">
<span class="wf-dag-node-icon"></span>
<span class="wf-dag-node-name">{{ node.id }}</span>
<span class="wf-dag-node-type">{{ nodeTypeLabel[node.node_type] || node.node_type }}</span>
</div>
</div>
<!-- 边列表带条件 -->
<div class="wf-dag-edges">
<div v-for="(edge, i) in dag.edges" :key="i" class="wf-dag-edge">
<span class="wf-dag-edge-arrow">{{ edge.source }} {{ edge.target }}</span>
<span v-if="edge.condition" class="wf-dag-edge-cond" :title="edge.condition">
{{ edge.condition }}
</span>
<span v-else class="wf-dag-edge-cond wf-dag-edge-cond--uncond">
无条件
</span>
</div>
</div>
</div>
<div v-else class="wf-dag wf-dag--empty">
{{ $t('taskDetail.workflowDagEmpty') || '暂无工作流数据' }}
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface DagNode {
id: string
node_type: string
config?: Record<string, unknown>
}
interface EdgeDef {
source: string
target: string
condition?: string | null
}
const props = defineProps<{
dagJson: string
}>()
const dag = computed<{ nodes: DagNode[]; edges: EdgeDef[] } | null>(() => {
try {
return JSON.parse(props.dagJson)
} catch {
return null
}
})
const nodeTypeLabel: Record<string, string> = {
'AiNode': 'AI',
'HumanNode': '人工审批',
'ScriptNode': '脚本',
'AiSelfReviewNode': 'AI 自审',
}
</script>
<style scoped>
.wf-dag {
margin-top: 8px;
padding: 8px 12px;
background: var(--df-bg-card, #fafafa);
border-radius: var(--df-radius-sm, 4px);
font-size: 12px;
}
.wf-dag--empty {
color: var(--df-text-dim, #888);
}
.wf-dag-title {
margin: 0 0 6px;
font-size: 13px;
font-weight: 600;
}
.wf-dag-nodes {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.wf-dag-node {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: var(--df-bg, #fff);
border: 0.5px solid var(--df-border, #ddd);
border-radius: var(--df-radius-sm, 4px);
}
.wf-dag-node-icon { color: var(--df-text-dim, #888); }
.wf-dag-node-name { font-weight: 500; }
.wf-dag-node-type {
font-size: 10px;
color: var(--df-text-dim, #888);
background: var(--df-bg-card, #f0f0f0);
padding: 0 4px;
border-radius: 3px;
}
.wf-dag-edges {
display: flex;
flex-direction: column;
gap: 3px;
}
.wf-dag-edge {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 0;
}
.wf-dag-edge-arrow { color: var(--df-text-primary); }
.wf-dag-edge-cond {
font-family: var(--df-font-mono);
font-size: 11px;
padding: 1px 6px;
background: rgba(74, 144, 226, 0.08);
border-radius: 3px;
color: var(--df-info, #4a90e2);
}
.wf-dag-edge-cond--uncond {
background: transparent;
color: var(--df-text-dim, #aaa);
}
</style>

View File

@@ -168,6 +168,7 @@ import {
} from '../constants/project'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
const { t } = useI18n()
const route = useRoute()
@@ -198,6 +199,15 @@ let _wfResultTimer: ReturnType<typeof setTimeout> | null = null
const wfProgressHint = computed(() => wfResult.value !== null)
const wfCompletedHint = computed(() => wfResult.value === 'completed')
const wfFailedHint = computed(() => wfResult.value === 'failed')
// 工作流 DAG 结构展示
const wfDagJson = ref('')
async function refreshWorkflowDag(execId: string) {
try {
const record = await workflowApi.getExecution(execId)
if (record?.dag_json) wfDagJson.value = record.dag_json
} catch { /* 静默 */ }
}
const wfDoneTotal = computed(() => wfTotalNodes.value)
const taskId = computed(() => route.params.id as string)
@@ -337,6 +347,7 @@ async function handleWorkflowAdvance(target: string) {
)
wfExecId.value = execId
errorMsg.value = ''
await refreshWorkflowDag(execId)
} catch (e: any) {
wfAdvancing.value = false
wfExecId.value = null