From 8710d6cfd1f622ad1cf669ebf920e83eb1e3524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 15 Jun 2026 05:48:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20ARC-05=20project.ts=20?= =?UTF-8?q?=E4=B8=8A=E5=B8=9D=20store=20=E6=8B=86=E5=88=86(4=20=E5=AD=90?= =?UTF-8?q?=20store+barrel=20=E5=85=BC=E5=AE=B9=E9=9B=B6=E6=94=B9=E5=8A=A8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆 stores/project/{state,projects,tasks,ideas,workflow}.ts 四领域子 store(共享 state 单例 reactive 引用一致);越层 invoke approve_human_approval/cancel_workflow_node 下沉 src/api/workflow.ts;project.ts 改 barrel 组合器(实例化四子 store+re-export 所有成员,useProjectStore 单例+ProjectStore 类型保留)。所有 view(App/Dashboard/Ideas/ProjectDetail/Projects/Tasks/ToolCard)import '../stores/project' 零改动。行为零变化(含 CR-11⑪ approveHumanApproval 签名+B-34 snake_case)。批6,vue-tsc 0 --- src/api/workflow.ts | 26 +++ src/stores/project.ts | 369 +++++---------------------------- src/stores/project/ideas.ts | 63 ++++++ src/stores/project/projects.ts | 107 ++++++++++ src/stores/project/state.ts | 37 ++++ src/stores/project/tasks.ts | 48 +++++ src/stores/project/workflow.ts | 128 ++++++++++++ 7 files changed, 456 insertions(+), 322 deletions(-) create mode 100644 src/stores/project/ideas.ts create mode 100644 src/stores/project/projects.ts create mode 100644 src/stores/project/state.ts create mode 100644 src/stores/project/tasks.ts create mode 100644 src/stores/project/workflow.ts diff --git a/src/api/workflow.ts b/src/api/workflow.ts index ccb2f80..ef5f560 100644 --- a/src/api/workflow.ts +++ b/src/api/workflow.ts @@ -26,4 +26,30 @@ export const workflowApi = { callback(event.payload) }) }, + + /** + * 发送人工审批响应。 + * + * 统一以 decisions 数组表达选择,按 select_type 判定单/多选: + * - 单选:decision 取 decisions[0]; + * - 多选:decisions 透传全量,decision 取首项作占位(后端按 decisions 走)。 + * options 透传闭环后端校验(R-PD-5)。 + * B-260615-34:Tauri 2 IPC 不转 camelCase,snake_case 对齐后端 workflow.rs。 + */ + approveHumanApproval(params: { + execution_id: string + node_id: string + decision: string + comment?: string + options: string[] + decisions: string[] + select_type: 'single' | 'multiple' + }): Promise { + return invoke('approve_human_approval', params) + }, + + /** 取消当前待审批节点(置节点 Cancelled) */ + cancelHumanApproval(params: { execution_id: string; node_id: string }): Promise { + return invoke('cancel_workflow_node', params) + }, } diff --git a/src/stores/project.ts b/src/stores/project.ts index f6f0342..1d5057a 100644 --- a/src/stores/project.ts +++ b/src/stores/project.ts @@ -1,322 +1,26 @@ import { reactive, computed } from 'vue' -import { invoke } from '@tauri-apps/api/core' -import { projectApi, taskApi, ideaApi, workflowApi } from '../api' -import type { ImportProjectInput } from '../api/project' -import type { ProjectRecord, TaskRecord, IdeaRecord, WorkflowRecord, WorkflowEventPayload } from '../api/types' - -// ── 全局响应式状态(单例) ── - -const state = reactive({ - projects: [] as ProjectRecord[], - deletedProjects: [] as ProjectRecord[], - tasks: [] as TaskRecord[], - ideas: [] as IdeaRecord[], - workflowExecutions: [] as WorkflowRecord[], - liveEvents: [] as Array, - pendingApproval: null as { - execution_id: string - node_id: string - title: string - description: string - options: string[] - /** F-260615-01: single(缺省)/multiple */ - select_type?: 'single' | 'multiple' - } | null, - loading: false, - error: null as string | null, -}) - -let _eventUnlisten: (() => void) | null = null +import { createProjectsStore } from './project/projects' +import { createTasksStore } from './project/tasks' +import { createIdeasStore } from './project/ideas' +import { createWorkflowStore } from './project/workflow' +import { state, clearError } from './project/state' +// ── barrel 组合器(零行为变更,纯重构) ── +// +// 原 project.ts 是上帝 store(项目/任务/灵感/工作流多领域 + 越层 invoke 审批 IPC)。 +// 已拆为 src/stores/project/{projects,tasks,ideas,workflow}.ts 四子 store, +// 共享 src/stores/project/state.ts 单例 state。 +// +// 本文件改为组合器:内部实例化四子 store + re-export 所有成员, +// 使现有 import { useProjectStore } 的 view 零改动。 +// 越层 invoke(approve_human_approval / cancel_human_node) 已下沉到 src/api/workflow.ts。 function createStore() { - // ── 项目 CRUD ── - async function loadProjects() { - state.loading = true - state.error = null - try { - state.projects = await projectApi.list() - } catch (e: any) { - state.error = e?.toString() ?? '加载项目失败' - } finally { - state.loading = false - } - } + const projectsStore = createProjectsStore() + const tasksStore = createTasksStore() + const ideasStore = createIdeasStore() + const workflowStore = createWorkflowStore() - // 清除错误状态(供 toast 显示后重置,允许连续同值错误再次触发 watch) - function clearError() { state.error = null } - - async function createProject(name: string, description = '', ideaId?: string, path?: string, stack?: string) { - try { - const record = await projectApi.create({ name, description, idea_id: ideaId, path, stack }) - state.projects.push(record) - return record - } catch (e: any) { - state.error = e?.toString() ?? '创建项目失败' - return null - } - } - - /** 导入历史项目(选已存在目录,后端创建+绑定+探测栈+读 README 首段一步完成) */ - async function importProject(input: ImportProjectInput) { - try { - const record = await projectApi.importProject(input) - await loadProjects() // 刷新列表(后端已 insert,统一走 load 避免本地数组与后端不一致) - return record - } catch (e: any) { - state.error = e?.toString() ?? '导入项目失败' - return null - } - } - - async function updateProject(id: string, field: string, value: string) { - await projectApi.update(id, field, value) - const idx = state.projects.findIndex(p => p.id === id) - if (idx >= 0) { - (state.projects[idx] as any)[field] = value - } - } - - /** 重定位项目目录(后端重探测 stack,返回最新记录并更新本地状态) */ - async function relocateProjectPath(id: string, newPath: string) { - const record = await projectApi.relocatePath(id, newPath) - const idx = state.projects.findIndex(p => p.id === id) - if (idx >= 0) state.projects[idx] = record - return record - } - - async function deleteProject(id: string) { - try { - await projectApi.delete(id) // 软删 → 回收站(可恢复) - state.projects = state.projects.filter(p => p.id !== id) - } catch (e: any) { - state.error = e?.toString() ?? '删除项目失败' - } - } - - async function loadDeletedProjects() { - try { - state.deletedProjects = await projectApi.listDeleted() - } catch (e: any) { - state.error = e?.toString() ?? '加载回收站失败' - } - } - - async function restoreProject(id: string) { - try { - await projectApi.restore(id) - state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) - await loadProjects() // 恢复后刷新活跃列表 - } catch (e: any) { - state.error = e?.toString() ?? '恢复项目失败' - } - } - - async function purgeProject(id: string) { - try { - await projectApi.purge(id) // 彻底删(级联物理删,不可恢复) - state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) - } catch (e: any) { - state.error = e?.toString() ?? '彻底删除失败' - } - } - - // ── 任务 CRUD ── - async function loadTasks(projectId?: string) { - try { - state.tasks = await taskApi.list(projectId) - } catch (e: any) { - state.error = e?.toString() ?? '加载任务失败' - } - } - - async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) { - try { - const record = await taskApi.create(input) - state.tasks.push(record) - return record - } catch (e: any) { - state.error = e?.toString() ?? '创建任务失败' - return null - } - } - - async function updateTask(id: string, field: string, value: string) { - await taskApi.update(id, field, value) - const idx = state.tasks.findIndex(t => t.id === id) - if (idx >= 0) { - (state.tasks[idx] as any)[field] = value - } - } - - async function deleteTask(id: string) { - try { - await taskApi.delete(id) - state.tasks = state.tasks.filter(t => t.id !== id) - } catch (e: any) { - state.error = e?.toString() ?? '删除任务失败' - } - } - - // ── 灵感 CRUD ── - async function loadIdeas() { - try { - state.ideas = await ideaApi.list() - } catch (e: any) { - state.error = e?.toString() ?? '加载灵感失败' - } - } - - async function createIdea(input: { title: string; description?: string; priority?: number; tags?: string; source?: string }) { - try { - const record = await ideaApi.create(input) - state.ideas.push(record) - return record - } catch (e: any) { - state.error = e?.toString() ?? '创建灵感失败' - return null - } - } - - async function updateIdea(id: string, field: string, value: string) { - await ideaApi.update(id, field, value) - const idx = state.ideas.findIndex(i => i.id === id) - if (idx >= 0) { - (state.ideas[idx] as any)[field] = value - } - } - - async function deleteIdea(id: string) { - try { - await ideaApi.delete(id) - state.ideas = state.ideas.filter(i => i.id !== id) - } catch (e: any) { - state.error = e?.toString() ?? '删除灵感失败' - } - } - - async function evaluateIdea(id: string) { - const record = await ideaApi.evaluate(id) - const idx = state.ideas.findIndex(i => i.id === id) - if (idx >= 0) state.ideas[idx] = record - return record - } - - async function promoteIdea(id: string) { - const res = await ideaApi.promote(id) - await loadIdeas() // 后端已回写 status=promoted/promoted_to,刷新列表 - return res - } - - // ── 工作流 ── - async function runWorkflow(name: string, dag: unknown, config?: Record) { - return await workflowApi.run(name, dag, config) - } - - async function loadWorkflowExecutions() { - try { - state.workflowExecutions = await workflowApi.listExecutions() - } catch (e: any) { - state.error = e?.toString() ?? '加载工作流记录失败' - } - } - - async function startEventListener() { - if (_eventUnlisten) return _eventUnlisten - _eventUnlisten = 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 - } - } catch (e) { - console.error('处理工作流事件失败:', e, payload) - } - }) - return _eventUnlisten - } - - function clearLiveEvents() { - state.liveEvents = [] - } - - /** - * 发送审批响应。 - * - * 统一以 decisions 数组表达选择,按 pendingApproval.select_type 判定单/多选: - * - 单选(select_type='single' 或缺省):取 decisions[0] 作为后端 decision 单值; - * - 多选(select_type='multiple'):decisions 透传全量。 - * - * 后端 IPC 需同时收 decision(单值,向后兼容)与 decisions(数组),此处按 select_type - * 自动分派,消除调用方"该传 decision 还是 decisions"的歧义。 - * F-260615-01: decisions/select_type 透传,与后端 IPC 签名对齐。 - */ - async function approveHumanApproval( - decisions: string[], - comment?: string, - ) { - if (!state.pendingApproval) return - const selectType = state.pendingApproval.select_type ?? 'single' - // 单选取首项(后端 decision 为单值);多选 decision 取首项作占位(后端按 decisions 走) - const decision = selectType === 'multiple' - ? (decisions[0] ?? '') - : (decisions[0] ?? '') - - try { - // 调用 IPC 命令发送审批响应 - // R-PD-5: 透传 options 闭环后端校验(options 非空时 decision 必须 ∈ options,否则 IPC 报错)。 - // options 来自已收到的 HumanApprovalRequest 事件 payload(IPC 无法访问节点 config)。 - await invoke('approve_human_approval', { - execution_id: state.pendingApproval.execution_id, - node_id: state.pendingApproval.node_id, - decision, - comment, - options: state.pendingApproval.options ?? [], - decisions, - select_type: selectType, // B-260615-34:Tauri 2 IPC 不转 camelCase,须 snake_case 对齐后端 workflow.rs:211 - }) - - // 清除待审批状态 - state.pendingApproval = null - } catch (error) { - console.error('审批失败:', error) - } - } - - /** 取消当前待审批节点(调 cancel_workflow_node IPC 置节点 Cancelled) */ - async function cancelHumanApproval() { - if (!state.pendingApproval) return - - try { - await invoke('cancel_workflow_node', { - execution_id: state.pendingApproval.execution_id, - node_id: state.pendingApproval.node_id, - }) - // 清除待审批状态 - state.pendingApproval = null - } catch (error) { - console.error('取消审批失败:', error) - } - } - - function stopEventListener() { - if (_eventUnlisten) { - try { - _eventUnlisten() - } catch (e) { - console.error('停止事件监听失败:', e) - } - _eventUnlisten = null - } - } - - // ── 统计 ── + // ── 统计(跨领域,留在组合器) ── const stats = computed(() => ({ ideas: state.ideas.length, projects: state.projects.length, @@ -334,18 +38,39 @@ function createStore() { get liveEvents() { return state.liveEvents }, get loading() { return state.loading }, get error() { return state.error }, - clearError, get deletedProjects() { return state.deletedProjects }, + clearError, // project actions - loadProjects, createProject, importProject, updateProject, deleteProject, relocateProjectPath, - loadDeletedProjects, restoreProject, purgeProject, + loadProjects: projectsStore.loadProjects, + createProject: projectsStore.createProject, + importProject: projectsStore.importProject, + updateProject: projectsStore.updateProject, + deleteProject: projectsStore.deleteProject, + relocateProjectPath: projectsStore.relocateProjectPath, + loadDeletedProjects: projectsStore.loadDeletedProjects, + restoreProject: projectsStore.restoreProject, + purgeProject: projectsStore.purgeProject, // task actions - loadTasks, createTask, updateTask, deleteTask, + loadTasks: tasksStore.loadTasks, + createTask: tasksStore.createTask, + updateTask: tasksStore.updateTask, + deleteTask: tasksStore.deleteTask, // idea actions - loadIdeas, createIdea, updateIdea, deleteIdea, evaluateIdea, promoteIdea, + loadIdeas: ideasStore.loadIdeas, + createIdea: ideasStore.createIdea, + updateIdea: ideasStore.updateIdea, + deleteIdea: ideasStore.deleteIdea, + evaluateIdea: ideasStore.evaluateIdea, + promoteIdea: ideasStore.promoteIdea, // workflow actions - runWorkflow, loadWorkflowExecutions, startEventListener, stopEventListener, clearLiveEvents, - approveHumanApproval, cancelHumanApproval, pendingApproval: computed(() => state.pendingApproval), + runWorkflow: workflowStore.runWorkflow, + loadWorkflowExecutions: workflowStore.loadWorkflowExecutions, + startEventListener: workflowStore.startEventListener, + stopEventListener: workflowStore.stopEventListener, + clearLiveEvents: workflowStore.clearLiveEvents, + approveHumanApproval: workflowStore.approveHumanApproval, + cancelHumanApproval: workflowStore.cancelHumanApproval, + pendingApproval: computed(() => state.pendingApproval), // computed stats, }) diff --git a/src/stores/project/ideas.ts b/src/stores/project/ideas.ts new file mode 100644 index 0000000..8f1e1de --- /dev/null +++ b/src/stores/project/ideas.ts @@ -0,0 +1,63 @@ +import { ideaApi } from '../../api' +import { state } from './state' + +/** 灵感 CRUD 子 store(共享全局 state) */ +export function createIdeasStore() { + async function loadIdeas() { + try { + state.ideas = await ideaApi.list() + } catch (e: any) { + state.error = e?.toString() ?? '加载灵感失败' + } + } + + async function createIdea(input: { title: string; description?: string; priority?: number; tags?: string; source?: string }) { + try { + const record = await ideaApi.create(input) + state.ideas.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建灵感失败' + return null + } + } + + async function updateIdea(id: string, field: string, value: string) { + await ideaApi.update(id, field, value) + const idx = state.ideas.findIndex(i => i.id === id) + if (idx >= 0) { + (state.ideas[idx] as any)[field] = value + } + } + + async function deleteIdea(id: string) { + try { + await ideaApi.delete(id) + state.ideas = state.ideas.filter(i => i.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除灵感失败' + } + } + + async function evaluateIdea(id: string) { + const record = await ideaApi.evaluate(id) + const idx = state.ideas.findIndex(i => i.id === id) + if (idx >= 0) state.ideas[idx] = record + return record + } + + async function promoteIdea(id: string) { + const res = await ideaApi.promote(id) + await loadIdeas() // 后端已回写 status=promoted/promoted_to,刷新列表 + return res + } + + return { + loadIdeas, + createIdea, + updateIdea, + deleteIdea, + evaluateIdea, + promoteIdea, + } +} diff --git a/src/stores/project/projects.ts b/src/stores/project/projects.ts new file mode 100644 index 0000000..32b6143 --- /dev/null +++ b/src/stores/project/projects.ts @@ -0,0 +1,107 @@ +import { projectApi } from '../../api' +import type { ImportProjectInput } from '../../api/project' +import { state, clearError } from './state' + +/** 项目 CRUD 子 store(共享全局 state) */ +export function createProjectsStore() { + async function loadProjects() { + state.loading = true + state.error = null + try { + state.projects = await projectApi.list() + } catch (e: any) { + state.error = e?.toString() ?? '加载项目失败' + } finally { + state.loading = false + } + } + + async function createProject(name: string, description = '', ideaId?: string, path?: string, stack?: string) { + try { + const record = await projectApi.create({ name, description, idea_id: ideaId, path, stack }) + state.projects.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建项目失败' + return null + } + } + + /** 导入历史项目(选已存在目录,后端创建+绑定+探测栈+读 README 首段一步完成) */ + async function importProject(input: ImportProjectInput) { + try { + const record = await projectApi.importProject(input) + await loadProjects() // 刷新列表(后端已 insert,统一走 load 避免本地数组与后端不一致) + return record + } catch (e: any) { + state.error = e?.toString() ?? '导入项目失败' + return null + } + } + + async function updateProject(id: string, field: string, value: string) { + await projectApi.update(id, field, value) + const idx = state.projects.findIndex(p => p.id === id) + if (idx >= 0) { + (state.projects[idx] as any)[field] = value + } + } + + /** 重定位项目目录(后端重探测 stack,返回最新记录并更新本地状态) */ + async function relocateProjectPath(id: string, newPath: string) { + const record = await projectApi.relocatePath(id, newPath) + const idx = state.projects.findIndex(p => p.id === id) + if (idx >= 0) state.projects[idx] = record + return record + } + + async function deleteProject(id: string) { + try { + await projectApi.delete(id) // 软删 → 回收站(可恢复) + state.projects = state.projects.filter(p => p.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除项目失败' + } + } + + async function loadDeletedProjects() { + try { + state.deletedProjects = await projectApi.listDeleted() + } catch (e: any) { + state.error = e?.toString() ?? '加载回收站失败' + } + } + + async function restoreProject(id: string) { + try { + await projectApi.restore(id) + state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) + await loadProjects() // 恢复后刷新活跃列表 + } catch (e: any) { + state.error = e?.toString() ?? '恢复项目失败' + } + } + + async function purgeProject(id: string) { + try { + await projectApi.purge(id) // 彻底删(级联物理删,不可恢复) + state.deletedProjects = state.deletedProjects.filter(p => p.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '彻底删除失败' + } + } + + return { + loadProjects, + createProject, + importProject, + updateProject, + deleteProject, + relocateProjectPath, + loadDeletedProjects, + restoreProject, + purgeProject, + // clearError 来自 state(供 barrel re-export) + clearError, + } +} diff --git a/src/stores/project/state.ts b/src/stores/project/state.ts new file mode 100644 index 0000000..2a17d90 --- /dev/null +++ b/src/stores/project/state.ts @@ -0,0 +1,37 @@ +import { reactive } from 'vue' +import type { ProjectRecord, TaskRecord, IdeaRecord, WorkflowRecord, WorkflowEventPayload } from '../../api/types' + +/** 待审批请求(由 human_approval_request 事件填充) */ +export interface PendingApproval { + execution_id: string + node_id: string + title: string + description: string + options: string[] + /** F-260615-01: single(缺省)/multiple */ + select_type?: 'single' | 'multiple' +} + +// ── 全局响应式状态(单例,四子 store 共享同一份) ── +export const state = reactive({ + projects: [] as ProjectRecord[], + deletedProjects: [] as ProjectRecord[], + tasks: [] as TaskRecord[], + ideas: [] as IdeaRecord[], + workflowExecutions: [] as WorkflowRecord[], + liveEvents: [] as Array, + pendingApproval: null as PendingApproval | null, + loading: false, + error: null as string | null, +}) + +/** 清除错误状态(供 toast 显示后重置,允许连续同值错误再次触发 watch) */ +export function clearError() { state.error = null } + +/** 工作流事件监听 unlisten 句柄(单例,workflow 子 store 读写) */ +export let _eventUnlisten: (() => void) | null = null + +/** 内部重置 unlisten 句柄(workflow 子 store stopEventListener/startEventListener 用) */ +export function setEventUnlisten(fn: (() => void) | null) { + _eventUnlisten = fn +} diff --git a/src/stores/project/tasks.ts b/src/stores/project/tasks.ts new file mode 100644 index 0000000..df07889 --- /dev/null +++ b/src/stores/project/tasks.ts @@ -0,0 +1,48 @@ +import { taskApi } from '../../api' +import { state } from './state' + +/** 任务 CRUD 子 store(共享全局 state) */ +export function createTasksStore() { + async function loadTasks(projectId?: string) { + try { + state.tasks = await taskApi.list(projectId) + } catch (e: any) { + state.error = e?.toString() ?? '加载任务失败' + } + } + + async function createTask(input: { project_id: string; title: string; description?: string; priority?: number; branch_name?: string; assignee?: string }) { + try { + const record = await taskApi.create(input) + state.tasks.push(record) + return record + } catch (e: any) { + state.error = e?.toString() ?? '创建任务失败' + return null + } + } + + async function updateTask(id: string, field: string, value: string) { + await taskApi.update(id, field, value) + const idx = state.tasks.findIndex(t => t.id === id) + if (idx >= 0) { + (state.tasks[idx] as any)[field] = value + } + } + + async function deleteTask(id: string) { + try { + await taskApi.delete(id) + state.tasks = state.tasks.filter(t => t.id !== id) + } catch (e: any) { + state.error = e?.toString() ?? '删除任务失败' + } + } + + return { + loadTasks, + createTask, + updateTask, + deleteTask, + } +} diff --git a/src/stores/project/workflow.ts b/src/stores/project/workflow.ts new file mode 100644 index 0000000..282af33 --- /dev/null +++ b/src/stores/project/workflow.ts @@ -0,0 +1,128 @@ +import { workflowApi } from '../../api' +import { state, _eventUnlisten, setEventUnlisten } from './state' + +/** + * 工作流 + 审批子 store(共享全局 state)。 + * + * 越层 invoke(approve_human_approval / cancel_workflow_node) 已下沉到 + * src/api/workflow.ts,本子 store 不再直接 invoke IPC。 + */ +export function createWorkflowStore() { + async function runWorkflow(name: string, dag: unknown, config?: Record) { + return await workflowApi.run(name, dag, config) + } + + async function loadWorkflowExecutions() { + try { + state.workflowExecutions = await workflowApi.listExecutions() + } catch (e: any) { + state.error = e?.toString() ?? '加载工作流记录失败' + } + } + + 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 + } + } 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 + const selectType = state.pendingApproval.select_type ?? 'single' + // 单选取首项(后端 decision 为单值);多选 decision 取首项作占位(后端按 decisions 走) + const decision = selectType === 'multiple' + ? (decisions[0] ?? '') + : (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) + } + } + + /** 取消当前待审批节点(调 cancel_workflow_node IPC 置节点 Cancelled) */ + async function cancelHumanApproval() { + if (!state.pendingApproval) return + + try { + await workflowApi.cancelHumanApproval({ + execution_id: state.pendingApproval.execution_id, + node_id: state.pendingApproval.node_id, + }) + // 清除待审批状态 + state.pendingApproval = null + } catch (error) { + console.error('取消审批失败:', error) + } + } + + function stopEventListener() { + if (_eventUnlisten) { + try { + _eventUnlisten() + } catch (e) { + console.error('停止事件监听失败:', e) + } + setEventUnlisten(null) + } + } + + return { + runWorkflow, + loadWorkflowExecutions, + startEventListener, + stopEventListener, + clearLiveEvents, + approveHumanApproval, + cancelHumanApproval, + } +}