Files
DevFlow/src/stores/project/workflow.ts

175 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { workflowApi } from '@/api'
import i18n from '@/i18n'
import { state, _eventUnlisten, setEventUnlisten } from './state'
// composable 外全局 i18n 实例(非 setup 上下文)
const t = ((i18n as any).global.t as (k: string) => string).bind((i18n as any).global)
// B-03b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。
// 防双击/连点在同一轮 IPC 未返回前发起第二次(后端无幂等键,第二次会重复消费 Request 触发
// "node already completed" 类报错)。store 层守卫优于按钮 disabled: 共享单例 state,跨调用方统一。
let _approvalInFlight = false
/**
* 工作流 + 审批子 store(共享全局 state)。
*
* 越层 invoke(approve_human_approval / cancel_workflow_node) 已下沉到
* src/api/workflow.ts,本子 store 不再直接 invoke IPC。
*/
export function createWorkflowStore() {
/**
* 触发工作流执行。
* F-260616-06 阶段2(②-2): taskId/targetStatus 可选,同时传时工作流完成/失败联动推进任务。
*/
async function runWorkflow(
name: string,
dag: unknown,
config?: Record<string, unknown>,
taskId?: string,
targetStatus?: string,
) {
return await workflowApi.run(name, dag, config, taskId, targetStatus)
}
async function loadWorkflowExecutions() {
try {
state.workflowExecutions = await workflowApi.listExecutions()
} catch (e: any) {
state.error = e?.toString() ?? t('projects.err.loadWorkflowFailed')
}
}
async function startEventListener() {
if (_eventUnlisten) return _eventUnlisten
const unlisten = await workflowApi.onEvent((payload) => {
try {
// 固时间戳(FR-C1):formattedEvents computed 重算时用事件发生时刻,非当前时刻
state.liveEvents.push({ ...payload, _ts: Date.now() })
// 限长(FR-R3):防 liveEvents 无限增长撑爆内存,保留最近 200 条
if (state.liveEvents.length > 200) {
state.liveEvents.splice(0, state.liveEvents.length - 200)
}
// 处理人工审批请求
// WorkflowEvent serde tag=type rename_all=snake_case → type='human_approval_request',字段扁平(无 data 包装)(B-03b-R7)
if (payload.event?.type === 'human_approval_request') {
state.pendingApproval = payload.event as unknown as typeof state.pendingApproval
}
// B-03b-R10(⑤ 终态不清): 工作流走到终态(completed/failed)或审批节点被取消时,
// 若 pendingApproval 仍属于本次 execution,说明该审批未经 approve/cancel 正常路径关闭
// (上游节点失败/工作流被整体取消/用户「稍后」关闭弹窗但 state 仍残留),清掉陈旧态
// 避免下次进 ProjectDetail 时 watch immediate 误判仍有待审批。
// 按 execution_id 限定: 并发工作流场景不清掉他人家的 pending(④ 单槽根因另议,见审查报告)。
else if (
state.pendingApproval &&
state.pendingApproval.execution_id === payload.execution_id &&
(payload.event?.type === 'workflow_completed' ||
payload.event?.type === 'workflow_failed' ||
payload.event?.type === 'node_cancelled')
) {
state.pendingApproval = null
}
} catch (e) {
console.error('处理工作流事件失败:', e, payload)
}
})
setEventUnlisten(unlisten)
return unlisten
}
function clearLiveEvents() {
state.liveEvents = []
}
/**
* 发送审批响应。
*
* 统一以 decisions 数组表达选择,按 pendingApproval.select_type 判定单/多选:
* - 单选(select_type='single' 或缺省):取 decisions[0] 作为后端 decision 单值;
* - 多选(select_type='multiple'):decisions 透传全量。
*
* CR-11⑪:签名保持 approveHumanApproval(decisions: string[], comment?)。
* F-260615-01: decisions/select_type 透传,与后端 IPC 签名对齐。
*/
async function approveHumanApproval(
decisions: string[],
comment?: string,
) {
if (!state.pendingApproval) return
// B-03b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击(连点/双击)
if (_approvalInFlight) return
_approvalInFlight = true
const selectType = state.pendingApproval.select_type ?? 'single'
// CR-260615-18:删死逻辑三元(原 selectType 多/单分支返回值相同)
// 单选取首项(后端 decision 单值);多选 decision 取首项占位(后端按 decisions 走)
const decision = decisions[0] ?? ''
try {
// R-PD-5: 透传 options 闭环后端校验(options 非空时 decision 必须 ∈ options,否则 IPC 报错)。
// options 来自已收到的 HumanApprovalRequest 事件 payload(IPC 无法访问节点 config)。
// B-260615-34:snake_case 由 api 层对齐后端 workflow.rs:211
await workflowApi.approveHumanApproval({
execution_id: state.pendingApproval.execution_id,
node_id: state.pendingApproval.node_id,
decision,
comment,
options: state.pendingApproval.options ?? [],
decisions,
select_type: selectType,
})
// 清除待审批状态
state.pendingApproval = null
} catch (error) {
console.error('审批失败:', error)
state.error = error?.toString() ?? t('projects.err.approveFailed')
} finally {
// B-03b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
_approvalInFlight = false
}
}
/** 取消当前待审批节点(调 cancel_workflow_node IPC 置节点 Cancelled */
async function cancelHumanApproval() {
if (!state.pendingApproval) return
// B-03b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击
if (_approvalInFlight) return
_approvalInFlight = true
try {
await workflowApi.cancelHumanApproval({
execution_id: state.pendingApproval.execution_id,
node_id: state.pendingApproval.node_id,
})
// 清除待审批状态
state.pendingApproval = null
} catch (error) {
console.error('取消审批失败:', error)
state.error = error?.toString() ?? t('projects.err.cancelFailed')
} finally {
// B-03b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
_approvalInFlight = false
}
}
function stopEventListener() {
if (_eventUnlisten) {
try {
_eventUnlisten()
} catch (e) {
console.error('停止事件监听失败:', e)
}
setEventUnlisten(null)
}
}
return {
runWorkflow,
loadWorkflowExecutions,
startEventListener,
stopEventListener,
clearLiveEvents,
approveHumanApproval,
cancelHumanApproval,
}
}