新增: 工具工作流补全(diff_files工具 + 技能清单注入 + 工作流进度共享 + human端到端测试 + 知识库MCP工具)

This commit is contained in:
lxy
2026-08-08 21:24:53 +08:00
parent 6ba6daf188
commit 9eb2995a74
14 changed files with 807 additions and 93 deletions
+3
View File
@@ -288,6 +288,9 @@ export interface WorkflowEventPayload {
}
}
/** 工作流 DAG 节点实时状态(WorkflowDagDisplay 高亮 + workflow store 共享推导) */
export type NodeStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
// ============================================================
// 通用
// ============================================================
@@ -65,6 +65,8 @@
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
import type { NodeStatus } from '@/api/types'
export type { NodeStatus }
interface DagNode {
id: string
@@ -76,7 +78,6 @@ interface EdgeDef {
target: string
condition?: string | null
}
export type NodeStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
const props = withDefaults(defineProps<{
dagJson: string
+2
View File
@@ -35,6 +35,8 @@ export default {
emptyTasks: 'No tasks yet',
// Workflow log panel
workflowLogTitle: '📋 Workflow Log',
// B-41 workflow progress panel
workflowProgressTitle: '📈 Workflow Progress',
runDemoWorkflow: '▶ Run Demo Workflow',
emptyWorkflowLog: 'Click "Run Demo Workflow" to view live logs',
// Approval dialog
+2
View File
@@ -35,6 +35,8 @@ export default {
emptyTasks: '暂无任务',
// 工作流日志面板
workflowLogTitle: '📋 工作流日志',
// B-41 工作流实时进度面板
workflowProgressTitle: '📈 工作流实时进度',
runDemoWorkflow: '▶ 运行测试工作流',
emptyWorkflowLog: '点击"运行测试工作流"查看实时日志',
// 审批对话框
+2
View File
@@ -168,6 +168,8 @@ function createStore() {
startEventListener: workflowStore.startEventListener,
stopEventListener: workflowStore.stopEventListener,
clearLiveEvents: workflowStore.clearLiveEvents,
// B-41: 工作流进度共享推导(按 execId 派生节点状态/进度)
workflowProgress: workflowStore.workflowProgress,
approveHumanApproval: workflowStore.approveHumanApproval,
cancelHumanApproval: workflowStore.cancelHumanApproval,
// AR-11(方案A):数据变更联动刷新监听(后端工具执行后 emit df-data-changed)
+81
View File
@@ -1,7 +1,79 @@
import { workflowApi } from '@/api'
import { t } from '@/i18n/i18n-helpers'
import type { NodeStatus, WorkflowEventPayload } from '@/api/types'
import { state, _eventUnlisten, setEventUnlisten } from './state'
// B-41: 工作流进度共享推导。liveEvents 已全局收集所有 workflow-event,
// 此处按 execution_id 重放派生节点状态/进度,供 TaskDetail/ProjectDetail 同源使用,
// 替代各自维护一份 onEvent 累加状态(单事实源,并发工作流互不干扰)。
export interface WorkflowProgress {
nodeStatuses: Record<string, NodeStatus>
runningNode: string
doneCount: number
totalNodes: number
result: 'completed' | 'failed' | null
}
function deriveWorkflowProgress(
execId: string | null,
events: WorkflowEventPayload[],
): WorkflowProgress {
const empty: WorkflowProgress = {
nodeStatuses: {},
runningNode: '',
doneCount: 0,
totalNodes: 0,
result: null,
}
if (!execId) return empty
const nodeStatuses: Record<string, NodeStatus> = {}
let runningNode = ''
let doneCount = 0
let totalNodes = 0
let result: 'completed' | 'failed' | null = null
for (const payload of events) {
if (payload.execution_id !== execId) continue
const evt = payload.event
switch (evt.type) {
case 'node_started': {
const node = String(evt.node_id ?? '')
if (node) {
nodeStatuses[node] = 'running'
runningNode = node
}
// 后端模板节点数前端无法预知,用「已启动 + 已完成」近似 total
totalNodes = Math.max(totalNodes, doneCount + 1)
break
}
case 'node_completed': {
if (runningNode) nodeStatuses[runningNode] = 'completed'
runningNode = ''
doneCount += 1
break
}
case 'node_failed': {
const node = String(evt.node_id ?? '')
if (node) nodeStatuses[node] = 'failed'
break
}
case 'node_cancelled': {
const node = String(evt.node_id ?? '')
if (node) nodeStatuses[node] = 'skipped'
break
}
case 'workflow_completed':
result = 'completed'
break
case 'workflow_failed':
result = 'failed'
break
default:
break
}
}
return { nodeStatuses, runningNode, doneCount, totalNodes, result }
}
// B-03b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。
// 防双击/连点在同一轮 IPC 未返回前发起第二次(后端无幂等键,第二次会重复消费 Request 触发
// "node already completed" 类报错)。store 层守卫优于按钮 disabled: 共享单例 state,跨调用方统一。
@@ -77,6 +149,14 @@ export function createWorkflowStore() {
state.liveEvents = []
}
/**
* B-41: 按 execution_id 派生工作流进度(读全局 liveEvents,响应式)。
* 供 TaskDetail(自身推进的 execId)/ ProjectDetail(最近一次执行)共用同源状态。
*/
function workflowProgress(execId: string | null): WorkflowProgress {
return deriveWorkflowProgress(execId, state.liveEvents)
}
/**
* 发送审批响应。
*
@@ -165,6 +245,7 @@ export function createWorkflowStore() {
startEventListener,
stopEventListener,
clearLiveEvents,
workflowProgress,
approveHumanApproval,
cancelHumanApproval,
}
+56 -7
View File
@@ -310,6 +310,22 @@
</section>
</div>
<!-- B-41 工作流实时进度:最近一次执行的 DAG + 节点状态高亮(共享 workflow store 派生) -->
<section v-if="hasWfProgress" class="panel wf-progress-panel">
<div class="panel-header">
<h2>{{ $t('projectDetail.workflowProgressTitle') }}</h2>
</div>
<div class="wf-progress">
<span v-if="wfProgress.runningNode">{{ $t('taskDetail.workflowStepRunning', { node: wfProgress.runningNode }) }}</span>
<span v-else-if="wfProgress.doneCount > 0" class="wf-progress-count">
{{ $t('taskDetail.workflowStepsProgress', { done: wfProgress.doneCount, total: wfProgress.totalNodes }) }}
</span>
<span v-if="wfProgress.result === 'completed'" class="wf-progress-hint">{{ $t('taskDetail.workflowCompletedHint') }}</span>
<span v-if="wfProgress.result === 'failed'" class="wf-progress-hint wf-progress-hint-fail">{{ $t('taskDetail.workflowFailedHint') }}</span>
</div>
<WorkflowDagDisplay v-if="wfDagJson" :dag-json="wfDagJson" :node-statuses="wfProgress.nodeStatuses" />
</section>
<!-- 工作流日志:无记录时折叠为摘要(标题 + 计数),有记录才展开列表 -->
<section v-if="formattedEvents.length > 0 || !workflowLogCollapsed" class="panel workflow-log-panel">
<div class="panel-header">
@@ -396,7 +412,7 @@ import { useI18n } from 'vue-i18n'
import { Message } from '@arco-design/web-vue'
import { open } from '@tauri-apps/plugin-dialog'
import { useProjectStore } from '@/stores/project'
import { projectApi, taskApi } from '@/api'
import { projectApi, taskApi, workflowApi } from '@/api'
import { moduleApi, type ProjectModuleRecord } from '@/api/module'
import { formatDate } from '@/utils/time'
import { parseStack } from '@/utils/project'
@@ -406,6 +422,7 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue'
import ApprovalDialog from '@/components/project/ApprovalDialog.vue'
import FileExplorer from '@/components/project/FileExplorer.vue'
import DependencyGraph from '@/components/project/DependencyGraph.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
import { useConfirm } from '@/composables/useConfirm'
import { useRendered } from '@/composables/useMarkdown'
import type { ProjectId, TaskRecord } from '@/api/types'
@@ -416,7 +433,6 @@ const store = useProjectStore()
const { t, locale } = useI18n()
const logListRef = ref<HTMLElement | null>(null)
const showApprovalDialog = ref(false)
let unlisten: (() => void) | null = null
// Tab 导航:概览(项目信息/任务/工作流日志) vs 文件浏览器(Batch 10)。
// P1-g+ 问题10④:activeTab 持久化 localStorage(按项目隔离,切换项目不串扰)。
@@ -672,6 +688,26 @@ const formattedEvents = computed<LogEntry[]>(() => {
})
})
// ── B-41 工作流实时进度 ──
// ProjectDetail 无工作流推进入口,但审批弹窗/日志面板已接 liveEvents;此处展示最近一次
// 工作流的 DAG + 节点状态高亮(共享 workflow store 派生,与 TaskDetail 同源),
// 手动跑工作流时也能看到节点实时进度。无事件(本次会话没跑过)时整个面板不渲染。
const wfDagJson = ref('')
const latestWfExecId = computed(() => {
const evs = store.liveEvents
return evs.length > 0 ? evs[evs.length - 1].execution_id : null
})
const wfProgress = computed(() => store.workflowProgress(latestWfExecId.value))
const hasWfProgress = computed(() => Object.keys(wfProgress.value.nodeStatuses).length > 0)
watch(latestWfExecId, async (id) => {
wfDagJson.value = ''
if (!id) return
try {
const record = await workflowApi.getExecution(id)
if (record?.dag_json) wfDagJson.value = record.dag_json
} catch { /* 静默 */ }
}, { immediate: true })
// ── 新建任务 ──
const showNewTaskModal = ref(false)
const newTaskTitle = ref('')
@@ -822,7 +858,7 @@ onMounted(async () => {
// 来源灵感回溯:项目有 idea_id 但 store.ideas 为空(本页未 load 过)时补拉,
// 否则 sourceIdea computed 永远找不到对应灵感记录
await store.loadIdeas()
unlisten = await store.startEventListener()
await store.startEventListener()
await checkPath()
})
@@ -838,10 +874,9 @@ watch(projectId, async (newId) => {
watch(() => currentProject.value?.path, () => { checkPath() })
onUnmounted(() => {
if (unlisten) {
unlisten()
unlisten = null
}
// 用 store.stopEventListener 统一停全局监听并复位句柄(直接调 unlisten() 停监听但
// 不复位 _eventUnlisten,后续 startEventListener 幂等会误返回已死句柄,共享化后必查)
store.stopEventListener()
})
</script>
@@ -1205,6 +1240,20 @@ onUnmounted(() => {
}
.workflow-log-panel { flex: 0 0 auto; }
/* B-41 工作流实时进度面板(与 TaskDetail wf-progress 同款视觉) */
.wf-progress-panel { flex: 0 0 auto; }
.wf-progress {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 6px;
font-size: 12px;
color: var(--df-text-secondary);
}
.wf-progress-count { color: var(--df-text-dim); }
.wf-progress-hint { color: var(--df-success); }
.wf-progress-hint-fail { color: var(--df-danger); }
/* ===== 面板 ===== */
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619),
此处仅保留本组件特有 .task-count。 */
+33 -78
View File
@@ -237,6 +237,7 @@ import { useRoute, useRouter } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { taskApi, projectApi, ideaApi } from '@/api'
import { workflowApi } from '@/api'
import { useProjectStore } from '@/stores/project'
import { formatDate } from '@/utils/time'
import { useRendered } from '@/composables/useMarkdown'
import {
@@ -245,14 +246,15 @@ import {
priorityLabel,
priorityClass,
} from '../constants/project'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload } from '@/api/types'
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
import type { NodeStatus } from '@/components/workflow/WorkflowDagDisplay.vue'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
// B-41: 共享 workflow store(liveEvents 全局收集 + workflowProgress 派生进度)
const store = useProjectStore()
const loading = ref(true)
const errorMsg = ref('')
@@ -386,32 +388,47 @@ async function confirmSubtask() {
// F-260616-06 ①-1 / B-41 工作流推进状态(与手动 advance 的 advancing 独立,互不干扰)
// ------------------------------------------------------------
// wfAdvancing:推进中态;wfExecId:当前监听的工作流执行 ID(过滤 workflow-event);
// wfTotalNodes/wfDoneCount/wfRunningNode:轻量进度(听 NodeStarted/NodeCompleted);
// wfTotalNodes/wfDoneCount/wfRunningNode:轻量进度(由共享 store 按 execId 派生);
// wfResult: 'completed' | 'failed' | null —— 终态提示,完成/失败后自动清空(由新一次推进重置)。
const wfAdvancing = ref(false)
const wfExecId = ref<string | null>(null)
const wfTotalNodes = ref(0)
const wfDoneCount = ref(0)
const wfRunningNode = ref<string>('')
const wfResult = ref<'completed' | 'failed' | null>(null)
// SW-260618-21: 终态提示 timer 引用,卸载时清理防写已销毁 ref(对齐 Projects.vue _toastTimer 模式)
let _wfResultTimer: ReturnType<typeof setTimeout> | null = null
// B-41: 共享工作流进度推导。workflow store 已全局收集 liveEvents,此处按 execId
// 派生节点状态/进度,与 ProjectDetail 同源(store.workflowProgress),替代原本地 onEvent 逐事件累加。
const wfProgress = computed(() => store.workflowProgress(wfExecId.value))
const wfNodeStatuses = computed(() => wfProgress.value.nodeStatuses)
const wfDoneCount = computed(() => wfProgress.value.doneCount)
const wfTotalNodes = computed(() => wfProgress.value.totalNodes)
const wfRunningNode = computed(() => wfProgress.value.runningNode)
const wfDoneTotal = computed(() => wfTotalNodes.value)
const wfProgressHint = computed(() => wfResult.value !== null)
const wfCompletedHint = computed(() => wfResult.value === 'completed')
const wfFailedHint = computed(() => wfResult.value === 'failed')
// 工作流 DAG 结构展示
// 终态 → 瞬态提示:派生 result 置位时收 wfAdvancing + 3s 后清 wfResult(仅 UI hint,数据不改)
watch(() => wfProgress.value.result, (r) => {
if (!r) return
wfResult.value = r
wfAdvancing.value = false
if (_wfResultTimer) clearTimeout(_wfResultTimer)
_wfResultTimer = setTimeout(() => {
if (wfResult.value === r) wfResult.value = null
_wfResultTimer = null
}, 3000)
})
// 工作流 DAG 结构展示(执行记录读回,节点状态由共享派生)
const wfDagJson = ref('')
const wfNodeStatuses = ref<Record<string, NodeStatus>>({})
async function refreshWorkflowDag(execId: string) {
wfNodeStatuses.value = {}
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)
@@ -568,9 +585,6 @@ async function handleWorkflowAdvance(target: string) {
if (!task.value || wfAdvancing.value || advancing.value) return
wfAdvancing.value = true
wfResult.value = null
wfRunningNode.value = ''
wfDoneCount.value = 0
wfTotalNodes.value = 0
try {
const execId = await workflowApi.run(
`task-advance:${target}`,
@@ -589,64 +603,8 @@ async function handleWorkflowAdvance(target: string) {
}
}
// B-41: 处理单个工作流事件(由 onEvent 回调 dispatch)。按 execution_id 过滤当前推进
function handleWorkflowEvent(payload: WorkflowEventPayload) {
if (!wfExecId.value || payload.execution_id !== wfExecId.value) return
const evt = payload.event
switch (evt?.type) {
case 'node_started': {
// 第一次 NodeStarted 时初始化 total(后端模板节点数,无法前端预知,
// 用「已启动 + 已完成」近似 total,显示已完成/已启动进度)
const node = String(evt.node_id ?? '')
wfRunningNode.value = node
wfNodeStatuses.value = { ...wfNodeStatuses.value, [node]: 'running' }
if (wfTotalNodes.value === 0) wfTotalNodes.value = 1
else wfTotalNodes.value = Math.max(wfTotalNodes.value, wfDoneCount.value + 1)
break
}
case 'node_completed': {
wfDoneCount.value += 1
if (wfRunningNode.value) {
wfNodeStatuses.value = { ...wfNodeStatuses.value, [wfRunningNode.value]: 'completed' }
}
wfRunningNode.value = ''
break
}
case 'workflow_completed': {
wfAdvancing.value = false
wfRunningNode.value = ''
wfResult.value = 'completed'
// 终态提示保留 3s 后清空(任务本体由 df-data-changed 刷新,提示不影响数据)
if (_wfResultTimer) clearTimeout(_wfResultTimer)
_wfResultTimer = setTimeout(() => {
if (wfResult.value === 'completed') wfResult.value = null
_wfResultTimer = null
}, 3000)
break
}
case 'workflow_failed':
case 'node_failed': {
if (evt?.node_id) {
const node = String(evt.node_id)
wfNodeStatuses.value = { ...wfNodeStatuses.value, [node]: 'failed' }
}
if (evt?.type === 'workflow_failed') {
wfAdvancing.value = false
wfRunningNode.value = ''
wfResult.value = 'failed'
if (_wfResultTimer) clearTimeout(_wfResultTimer)
_wfResultTimer = setTimeout(() => {
if (wfResult.value === 'failed') wfResult.value = null
_wfResultTimer = null
}, 3000)
}
// node_failed: 单节点失败,工作流后续会发 workflow_failed,此处不提前改终态
break
}
default:
break
}
}
// B-41: 进度状态已由共享 store 按 execId 派生(store.workflowProgress),不再本地逐事件累加
// 原有 handleWorkflowEvent 已移除 —— 事件仍由 workflow store 全局收集,TaskDetail 只读派生结果。
async function load() {
loading.value = true
@@ -689,8 +647,6 @@ watch(renderedDesc, () => { measureDescHeight() })
// 不享受 store 全局 df-data-changed 监听(该监听只刷 store.tasks 列表,不含本视图的当前 task 单体),
// 故此本地监听 entity=task/project 时重载当前 task。
let _unlistenDataChanged: (() => void) | null = null
// B-41: 工作流推进事件 unlistener(听 NodeStarted/NodeCompleted/WorkflowFailed 显示轻量进度)
let _unlistenWorkflowEvent: (() => void) | null = null
onMounted(async () => {
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
@@ -709,18 +665,17 @@ onMounted(async () => {
} catch (e) {
console.error('[TaskDetail] 启动 df-data-changed 监听失败:', e)
}
// B-41: 工作流推进事件监听(复用 workflowApi.onEvent, 与 workflow store 的全局监听并存无冲突 —
// 多个 listen 各自接收全量事件,本视图按 execution_id 过滤只处理自己发起的推进)
// B-41: 工作流进度由 workflow store 全局 liveEvents 推导(store.workflowProgress),
// 此处只需确保 store 事件监听已启动(幂等,共享单例;按 execution_id 过滤由派生层完成)。
try {
_unlistenWorkflowEvent = await workflowApi.onEvent(handleWorkflowEvent)
await store.startEventListener()
} catch (e) {
console.error('[TaskDetail] 启动 workflow-event 监听失败:', e)
console.error('[TaskDetail] 启动 workflow 事件监听失败:', e)
}
})
onBeforeUnmount(() => {
if (_unlistenDataChanged) { _unlistenDataChanged(); _unlistenDataChanged = null }
if (_unlistenWorkflowEvent) { _unlistenWorkflowEvent(); _unlistenWorkflowEvent = null }
// F-260805:移除子任务快捷菜单关闭监听
document.removeEventListener('click', closeChildMenu)
// SW-260618-21: 清终态提示 timer 防卸载后写已销毁 ref