UNPKG

openai-compatible-task-master

Version:

使用MCP解析PRD文档并生成任务列表

249 lines 12.2 kB
import { updateTasksWithLLM } from '../llm/llm_update_tasks.js'; import chalk from 'chalk'; import { resolveFullPath, ensureFileExists, readJsonFile, writeJsonFile } from '../utils/path_util.js'; /** * 从指定路径读取tasks.json文件并验证其格式 * @param projectDir 项目根目录路径 * @param tasksPath 任务文件相对路径 * @returns 验证通过的任务数据 * @throws 当文件不存在或格式不正确时抛出错误 */ export async function readTasksFile(projectDir, tasksPath) { console.debug(chalk.yellow(`读取任务文件 - projectDir: ${projectDir}, tasksPath: ${tasksPath}`)); // 构建完整路径 const fullTasksPath = resolveFullPath(projectDir, tasksPath); console.debug(chalk.yellow(`解析后路径 - fullTasksPath: ${fullTasksPath}`)); // 检查文件是否存在 if (!await ensureFileExists(fullTasksPath)) { throw new Error(`任务文件不存在: ${fullTasksPath}`); } try { // 读取并解析JSON文件 const rawData = await readJsonFile(fullTasksPath); // 验证基本结构 if (!rawData || typeof rawData !== 'object') { throw new Error('任务文件格式不正确: 根对象必须是一个对象'); } if (!rawData.tasks || !Array.isArray(rawData.tasks)) { throw new Error('任务文件格式不正确: 缺少tasks数组'); } if (!rawData.metadata || typeof rawData.metadata !== 'object') { throw new Error('任务文件格式不正确: 缺少metadata对象'); } // 验证metadata字段 const metadata = rawData.metadata; if (!metadata.projectName || typeof metadata.projectName !== 'string') { throw new Error('任务文件格式不正确: metadata中缺少projectName字符串'); } if (!metadata.totalTasks || (typeof metadata.totalTasks !== 'number' && typeof metadata.totalTasks !== 'string')) { throw new Error('任务文件格式不正确: metadata中缺少totalTasks数字'); } if (!metadata.sourceFile || typeof metadata.sourceFile !== 'string') { throw new Error('任务文件格式不正确: metadata中缺少sourceFile字符串'); } if (!metadata.generatedAt || typeof metadata.generatedAt !== 'string') { throw new Error('任务文件格式不正确: metadata中缺少generatedAt字符串'); } // 直接返回原始数据,保留子任务结构 // 注意:我们跳过了任务验证和转换,直接使用解析的JSON console.debug(chalk.yellow(`解析成功,任务数: ${rawData.tasks.length}`)); // 可以直接输出任务结构以进行调试 if (rawData.tasks.length > 0 && rawData.tasks[0].subTasks) { console.debug(chalk.yellow(`第一个任务有 ${rawData.tasks[0].subTasks.length} 个子任务`)); } return rawData; } catch (error) { const err = error; console.error(chalk.red(`解析任务文件失败: ${err}`)); throw new Error(`任务文件格式不正确: ${err}`); } } /** * 筛选需要更新的任务 * @param tasks 任务列表 * @param fromId 起始任务ID(字符串形式) * @returns 符合条件的任务子集(ID >= fromId且不是done状态) * @throws 当ID >= fromId的任务中有done状态的任务时抛出错误 */ export function filterTasks(tasks, fromId) { console.debug(chalk.yellow(`筛选任务 - fromId: ${fromId}`)); console.debug(chalk.yellow(`传入任务列表:`) + chalk.white(JSON.stringify(tasks[0], null, 2).slice(0, 300) + '...')); // 检查是否为子任务 if (fromId.includes('.')) { // 子任务格式:父任务ID.子任务序号 (如 "1.2") const [parentId, subTaskNumStr] = fromId.split('.'); console.debug(chalk.yellow(`查找父任务ID: ${parentId}, 子任务序号 >= ${subTaskNumStr}`)); // 查找所有子任务 let subTasks = []; // 从任务列表中提取所有符合条件的子任务 tasks.forEach(task => { console.debug(chalk.yellow(`检查任务: ${task.id}, 是否有子任务: ${Boolean(task.subTasks && Array.isArray(task.subTasks))}`)); // 只检查父任务ID匹配的任务 if (task.id === parentId && task.subTasks && Array.isArray(task.subTasks)) { console.debug(chalk.yellow(`任务 ${task.id} 的子任务数: ${task.subTasks.length}`)); const matchingSubTasks = task.subTasks.filter(subTask => { // 确保是同父任务的子任务且序号大于等于目标子任务序号 if (subTask.id && subTask.id.includes('.')) { const [taskParentId, taskSubNumStr] = subTask.id.split('.'); const matches = taskParentId === parentId && parseInt(taskSubNumStr) >= parseInt(subTaskNumStr); console.debug(chalk.yellow(`检查子任务: ${subTask.id}, 匹配: ${matches}`)); return matches; } return false; }); console.debug(chalk.yellow(`匹配的子任务数: ${matchingSubTasks.length}`)); subTasks = [...subTasks, ...matchingSubTasks]; } }); // 检查是否有已完成的任务 const doneTasks = subTasks.filter(task => task.status === 'done'); if (doneTasks.length > 0) { const doneTaskIds = doneTasks.map(task => task.id).join(', '); throw new Error(`筛选任务时发现已完成的任务 (ID: ${doneTaskIds}),已完成的任务不能更新`); } console.debug(chalk.yellow(`筛选结果 - 符合条件的子任务数: ${subTasks.length}`)); return subTasks; } else { // 主任务处理逻辑(保持不变) const idFilteredTasks = tasks.filter(task => { // 如果任务ID包含点号(如"1.2"),说明是子任务,不参与比较 if (task.id.includes('.')) { return false; } // 直接使用字符串比较 return task.id >= fromId; }); // 检查是否有done状态的任务 const doneTasks = idFilteredTasks.filter(task => task.status === 'done'); if (doneTasks.length > 0) { const doneTaskIds = doneTasks.map(task => task.id).join(', '); throw new Error(`筛选任务时发现已完成的任务 (ID: ${doneTaskIds}),已完成的任务不能更新`); } console.debug(chalk.yellow(`筛选结果 - 符合条件的任务数: ${idFilteredTasks.length}`)); return idFilteredTasks; } } /** * 保存更新后的任务数据到文件 * @param projectDir 项目根目录路径 * @param tasksPath 任务文件相对路径 * @param tasksData 更新后的任务数据 * @returns 成功返回true,失败抛出错误 */ export async function saveTasksFile(projectDir, tasksPath, tasksData) { console.debug(chalk.yellow(`保存任务文件 - projectDir: ${projectDir}, tasksPath: ${tasksPath}`)); // 构建完整路径 const fullTasksPath = resolveFullPath(projectDir, tasksPath); console.debug(chalk.yellow(`解析后路径 - fullTasksPath: ${fullTasksPath}`)); try { // 写入更新后的任务数据 await writeJsonFile(fullTasksPath, tasksData); console.info(chalk.green(`成功保存任务文件: ${fullTasksPath}`)); return true; } catch (error) { const err = error; console.error(chalk.red(`保存任务文件失败: ${err.message}`)); throw new Error(`保存任务文件失败: ${err.message}`); } } /** * 更新任务的入口函数 * @param projectDir 项目根目录路径 * @param tasksPath 任务文件相对路径 * @param prompt 用于更新任务的提示 * @param fromId 起始任务ID (可以是数字或字符串,如 "1.1" 表示子任务) * @param openaiUrl OpenAI兼容API的URL * @param apiKey API密钥 * @param model 模型名称 * @param streamMode 是否使用流式输出 * @returns 更新结果信息 */ export async function updateTasks(projectDir, tasksPath, prompt, fromId, openaiUrl, apiKey, model, streamMode) { const fromIdStr = String(fromId); console.info(chalk.blue(`开始更新任务 - projectDir: ${projectDir}, tasksPath: ${tasksPath}, fromId: ${fromIdStr}`)); console.info(chalk.blue(`用户提示: ${prompt}`)); // 1. 读取任务文件 const tasksData = await readTasksFile(projectDir, tasksPath); console.info(chalk.green(`成功读取任务文件,共${tasksData.tasks.length}个任务`)); // 2. 筛选需要更新的任务 const filteredTasks = filterTasks(tasksData.tasks, fromIdStr); if (filteredTasks.length === 0) { console.info(chalk.yellow('没有找到需要更新的任务')); return { saved: false }; } console.info(chalk.green(`找到${filteredTasks.length}个需要更新的任务`)); // 3. 调用LLM更新任务 console.info(chalk.blue(`开始调用LLM更新任务...`)); const updatedTasks = await updateTasksWithLLM(filteredTasks, prompt, tasksData.metadata, openaiUrl, apiKey, model, streamMode); console.info(chalk.green(`LLM任务更新完成,返回了${updatedTasks.length}个更新后的任务`)); // 4. 将更新后的任务与原始任务合并 let mergedTasks; if (fromIdStr.includes('.')) { // 处理子任务更新 const [parentId] = fromIdStr.split('.'); // 根据更新后的子任务,更新主任务的subTasks数组 mergedTasks = tasksData.tasks.map(task => { // 如果是包含所需子任务的父任务 if (task.id === parentId && task.subTasks) { // 创建一个新的子任务数组,保留不需要更新的子任务 const updatedSubTasks = [...task.subTasks]; // 使用映射创建任务ID到索引的查找表,用于更新 const subTaskIndices = new Map(); updatedSubTasks.forEach((subTask, index) => { if (subTask.id) { subTaskIndices.set(subTask.id, index); } }); // 更新对应的子任务 updatedTasks.forEach(updatedTask => { const index = subTaskIndices.get(updatedTask.id); if (index !== undefined) { // 替换现有子任务 updatedSubTasks[index] = updatedTask; } else { // 如果是新的子任务,添加到数组 updatedSubTasks.push(updatedTask); } }); // 返回更新后的父任务 return { ...task, subTasks: updatedSubTasks }; } // 其他任务保持不变 return task; }); } else { // 处理主任务更新(原逻辑) mergedTasks = [ ...tasksData.tasks.filter(task => { const taskIdStr = String(task.id); // 如果是子任务,检查其父任务ID是否小于fromId if (taskIdStr.includes('.')) { const parentId = taskIdStr.split('.')[0]; return parentId < fromIdStr; } // 使用字符串比较 return taskIdStr < fromIdStr; }), ...updatedTasks ]; } // 5. 保存更新后的任务 console.info(chalk.blue(`开始保存更新后的任务数据...`)); await saveTasksFile(projectDir, tasksPath, { ...tasksData, tasks: mergedTasks }); console.info(chalk.green(`任务数据保存成功`)); // 返回结果,包含更新后的任务和合并后的任务 return { saved: true }; } //# sourceMappingURL=update_tasks.js.map