/** * IPC Task Handlers — 任务管理域(P2-9 从 handlers.ts 拆分) * * 注意:此处为 UI 直连的 CRUD 通道;Agent 运行时走 task_manager 工具 * (electron/harness/tools/built-in/task-manager.ts),两侧共享 tasks 表。 */ import { ipcMain } from 'electron'; import { nanoid } from 'nanoid'; import type { IPCContext } from './context'; const VALID_TASK_PRIORITIES: readonly string[] = ['low', 'medium', 'high', 'critical']; const VALID_TASK_STATUSES: readonly string[] = ['pending', 'in_progress', 'completed', 'blocked', 'cancelled']; export function registerTaskHandlers(ctx: IPCContext): void { const { sessionService } = ctx; ipcMain.handle('tasks:list', async (_event, sessionId?: unknown) => { // M-46 修复: 校验 sessionId 类型(可选参数) if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId)) { return { success: false, error: 'Invalid sessionId' }; } const db = sessionService.getDB(); let sql = 'SELECT * FROM tasks'; const params: unknown[] = []; if (sessionId) { sql += ' WHERE session_id = ?'; params.push(sessionId); } sql += ' ORDER BY order_idx ASC, created_at ASC'; return { success: true, data: db.prepare(sql).all(...params) }; }); ipcMain.handle('tasks:create', async (_event, data: unknown) => { // M-46 修复: 校验 data 结构和字段类型/枚举 if (!data || typeof data !== 'object') { return { success: false, error: 'Invalid task data' }; } const req = data as Record; if (typeof req.sessionId !== 'string' || !req.sessionId.trim()) { return { success: false, error: 'Invalid sessionId' }; } if (typeof req.title !== 'string' || !req.title.trim()) { return { success: false, error: 'Invalid title' }; } if (req.priority !== undefined && !VALID_TASK_PRIORITIES.includes(req.priority as string)) { return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` }; } if (req.parentId !== undefined && req.parentId !== null && typeof req.parentId !== 'string') { return { success: false, error: 'Invalid parentId' }; } const db = sessionService.getDB(); // P4 统一(v0.3.13): ID 生成方式与 task_manager 工具一致(nanoid) const id = `task_${nanoid(12)}`; try { // P4 统一: 计算 order_idx = MAX(同 session+parent 的 order_idx) + 1(与 task_manager 工具一致) const parentId = (req.parentId as string | null) ?? null; let orderIdx = 0; if (parentId === null) { const orderRow = db.prepare( 'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id IS NULL' ).get(req.sessionId) as { maxOrder: number } | undefined; orderIdx = (orderRow?.maxOrder ?? -1) + 1; } else { const orderRow = db.prepare( 'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id = ?' ).get(req.sessionId, parentId) as { maxOrder: number } | undefined; orderIdx = (orderRow?.maxOrder ?? -1) + 1; } db.prepare(` INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, order_idx, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?) `).run( id, req.sessionId, req.title, typeof req.description === 'string' ? req.description : '', (req.priority as string) ?? 'medium', parentId, orderIdx, Date.now(), Date.now(), ); return { success: true, id }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown, sessionId: unknown) => { // M-47 修复: 校验 id 和 updates 结构/枚举 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } // P1 修复(v0.3.13): 补 session_id 越权保护(与 task_manager 工具一致) if (typeof sessionId !== 'string' || !sessionId) { return { success: false, error: 'Invalid sessionId' }; } if (!updates || typeof updates !== 'object') { return { success: false, error: 'Invalid updates' }; } const u = updates as Record; if (u.status !== undefined && !VALID_TASK_STATUSES.includes(u.status as string)) { return { success: false, error: `Invalid status (must be one of: ${VALID_TASK_STATUSES.join(', ')})` }; } if (u.priority !== undefined && !VALID_TASK_PRIORITIES.includes(u.priority as string)) { return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` }; } if (u.assignedTo !== undefined && u.assignedTo !== null && typeof u.assignedTo !== 'string') { return { success: false, error: 'Invalid assignedTo' }; } // 审计补充修复: 补全 title/description 类型校验 if (u.title !== undefined && typeof u.title !== 'string') { return { success: false, error: 'Invalid title (must be string)' }; } if (u.description !== undefined && typeof u.description !== 'string') { return { success: false, error: 'Invalid description (must be string)' }; } const db = sessionService.getDB(); try { const fields: string[] = []; const values: unknown[] = []; if (u.title !== undefined) { fields.push('title = ?'); values.push(u.title); } if (u.description !== undefined) { fields.push('description = ?'); values.push(u.description); } if (u.status !== undefined) { fields.push('status = ?'); values.push(u.status); } if (u.priority !== undefined) { fields.push('priority = ?'); values.push(u.priority); } if (u.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(u.assignedTo); } if (fields.length === 0) return { success: true }; fields.push('updated_at = ?'); values.push(Date.now()); if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); } values.push(id, sessionId); // P1 修复(v0.3.13): WHERE 补 session_id 校验,防越权修改其他会话任务 db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ? AND session_id = ?`).run(...values); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); ipcMain.handle('tasks:delete', async (_event, id: unknown, sessionId: unknown) => { // M-48 修复: 校验 id 类型 if (typeof id !== 'string' || !id) { return { success: false, error: 'Invalid task id' }; } // P1 修复(v0.3.13): 补 session_id 越权保护 if (typeof sessionId !== 'string' || !sessionId) { return { success: false, error: 'Invalid sessionId' }; } const db = sessionService.getDB(); try { db.prepare('DELETE FROM tasks WHERE id = ? AND session_id = ?').run(id, sessionId); return { success: true }; } catch (error) { return { success: false, error: (error as Error).message }; } }); }