openai-compatible-task-master
Version:
使用MCP解析PRD文档并生成任务列表
249 lines • 12.2 kB
JavaScript
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