重构: ARC-05 project.ts 上帝 store 拆分(4 子 store+barrel 兼容零改动)
拆 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
This commit is contained in:
@@ -26,4 +26,30 @@ export const workflowApi = {
|
|||||||
callback(event.payload)
|
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<void> {
|
||||||
|
return invoke('approve_human_approval', params)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 取消当前待审批节点(置节点 Cancelled) */
|
||||||
|
cancelHumanApproval(params: { execution_id: string; node_id: string }): Promise<void> {
|
||||||
|
return invoke('cancel_workflow_node', params)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,322 +1,26 @@
|
|||||||
import { reactive, computed } from 'vue'
|
import { reactive, computed } from 'vue'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { createProjectsStore } from './project/projects'
|
||||||
import { projectApi, taskApi, ideaApi, workflowApi } from '../api'
|
import { createTasksStore } from './project/tasks'
|
||||||
import type { ImportProjectInput } from '../api/project'
|
import { createIdeasStore } from './project/ideas'
|
||||||
import type { ProjectRecord, TaskRecord, IdeaRecord, WorkflowRecord, WorkflowEventPayload } from '../api/types'
|
import { createWorkflowStore } from './project/workflow'
|
||||||
|
import { state, clearError } from './project/state'
|
||||||
// ── 全局响应式状态(单例) ──
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
projects: [] as ProjectRecord[],
|
|
||||||
deletedProjects: [] as ProjectRecord[],
|
|
||||||
tasks: [] as TaskRecord[],
|
|
||||||
ideas: [] as IdeaRecord[],
|
|
||||||
workflowExecutions: [] as WorkflowRecord[],
|
|
||||||
liveEvents: [] as Array<WorkflowEventPayload & { _ts: number }>,
|
|
||||||
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
|
|
||||||
|
|
||||||
|
// ── 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() {
|
function createStore() {
|
||||||
// ── 项目 CRUD ──
|
const projectsStore = createProjectsStore()
|
||||||
async function loadProjects() {
|
const tasksStore = createTasksStore()
|
||||||
state.loading = true
|
const ideasStore = createIdeasStore()
|
||||||
state.error = null
|
const workflowStore = createWorkflowStore()
|
||||||
try {
|
|
||||||
state.projects = await projectApi.list()
|
|
||||||
} catch (e: any) {
|
|
||||||
state.error = e?.toString() ?? '加载项目失败'
|
|
||||||
} finally {
|
|
||||||
state.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除错误状态(供 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<string, unknown>) {
|
|
||||||
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(() => ({
|
const stats = computed(() => ({
|
||||||
ideas: state.ideas.length,
|
ideas: state.ideas.length,
|
||||||
projects: state.projects.length,
|
projects: state.projects.length,
|
||||||
@@ -334,18 +38,39 @@ function createStore() {
|
|||||||
get liveEvents() { return state.liveEvents },
|
get liveEvents() { return state.liveEvents },
|
||||||
get loading() { return state.loading },
|
get loading() { return state.loading },
|
||||||
get error() { return state.error },
|
get error() { return state.error },
|
||||||
clearError,
|
|
||||||
get deletedProjects() { return state.deletedProjects },
|
get deletedProjects() { return state.deletedProjects },
|
||||||
|
clearError,
|
||||||
// project actions
|
// project actions
|
||||||
loadProjects, createProject, importProject, updateProject, deleteProject, relocateProjectPath,
|
loadProjects: projectsStore.loadProjects,
|
||||||
loadDeletedProjects, restoreProject, purgeProject,
|
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
|
// task actions
|
||||||
loadTasks, createTask, updateTask, deleteTask,
|
loadTasks: tasksStore.loadTasks,
|
||||||
|
createTask: tasksStore.createTask,
|
||||||
|
updateTask: tasksStore.updateTask,
|
||||||
|
deleteTask: tasksStore.deleteTask,
|
||||||
// idea actions
|
// 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
|
// workflow actions
|
||||||
runWorkflow, loadWorkflowExecutions, startEventListener, stopEventListener, clearLiveEvents,
|
runWorkflow: workflowStore.runWorkflow,
|
||||||
approveHumanApproval, cancelHumanApproval, pendingApproval: computed(() => state.pendingApproval),
|
loadWorkflowExecutions: workflowStore.loadWorkflowExecutions,
|
||||||
|
startEventListener: workflowStore.startEventListener,
|
||||||
|
stopEventListener: workflowStore.stopEventListener,
|
||||||
|
clearLiveEvents: workflowStore.clearLiveEvents,
|
||||||
|
approveHumanApproval: workflowStore.approveHumanApproval,
|
||||||
|
cancelHumanApproval: workflowStore.cancelHumanApproval,
|
||||||
|
pendingApproval: computed(() => state.pendingApproval),
|
||||||
// computed
|
// computed
|
||||||
stats,
|
stats,
|
||||||
})
|
})
|
||||||
|
|||||||
63
src/stores/project/ideas.ts
Normal file
63
src/stores/project/ideas.ts
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/stores/project/projects.ts
Normal file
107
src/stores/project/projects.ts
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/stores/project/state.ts
Normal file
37
src/stores/project/state.ts
Normal file
@@ -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<WorkflowEventPayload & { _ts: number }>,
|
||||||
|
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
|
||||||
|
}
|
||||||
48
src/stores/project/tasks.ts
Normal file
48
src/stores/project/tasks.ts
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
128
src/stores/project/workflow.ts
Normal file
128
src/stores/project/workflow.ts
Normal file
@@ -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<string, unknown>) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user