175 lines
6.6 KiB
TypeScript
175 lines
6.6 KiB
TypeScript
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,
|
||
}
|
||
}
|