282 lines
7.6 KiB
JavaScript
282 lines
7.6 KiB
JavaScript
|
|
/**
|
|||
|
|
* GLADA · 任务接收器 · task-receiver.js
|
|||
|
|
*
|
|||
|
|
* 负责:
|
|||
|
|
* 1. 监听任务队列中的 glada-task 类型任务
|
|||
|
|
* 2. 解析 CAB 任务规格(bridge/chat-to-agent/pending/)
|
|||
|
|
* 3. 验证任务格式和授权
|
|||
|
|
* 4. 将合法任务传递给执行循环
|
|||
|
|
*
|
|||
|
|
* 版权:国作登字-2026-A-00037559
|
|||
|
|
* 签发:铸渊 · ICE-GL-ZY001
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const ROOT = path.resolve(__dirname, '..');
|
|||
|
|
const PENDING_DIR = path.join(ROOT, 'bridge', 'chat-to-agent', 'pending');
|
|||
|
|
const COMPLETED_DIR = path.join(ROOT, 'bridge', 'chat-to-agent', 'completed');
|
|||
|
|
const GLADA_QUEUE_DIR = path.join(ROOT, 'glada', 'queue');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 扫描 pending 目录,获取所有待执行的任务规格
|
|||
|
|
* @returns {Object[]} 任务规格列表
|
|||
|
|
*/
|
|||
|
|
function scanPendingTasks() {
|
|||
|
|
const tasks = [];
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(PENDING_DIR)) {
|
|||
|
|
return tasks;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const files = fs.readdirSync(PENDING_DIR)
|
|||
|
|
.filter(f => f.endsWith('.json'))
|
|||
|
|
.sort();
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
try {
|
|||
|
|
const filePath = path.join(PENDING_DIR, file);
|
|||
|
|
const spec = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|||
|
|
tasks.push({ file, filePath, spec });
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(`[GLADA-Receiver] ⚠️ 无法解析任务文件: ${file} - ${err.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tasks;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 扫描 GLADA 本地队列
|
|||
|
|
* @returns {Object[]} 本地队列中的任务
|
|||
|
|
*/
|
|||
|
|
function scanLocalQueue() {
|
|||
|
|
const tasks = [];
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(GLADA_QUEUE_DIR)) {
|
|||
|
|
fs.mkdirSync(GLADA_QUEUE_DIR, { recursive: true });
|
|||
|
|
return tasks;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const files = fs.readdirSync(GLADA_QUEUE_DIR)
|
|||
|
|
.filter(f => f.endsWith('.json'))
|
|||
|
|
.sort();
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
try {
|
|||
|
|
const filePath = path.join(GLADA_QUEUE_DIR, file);
|
|||
|
|
const task = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|||
|
|
if (task.status === 'pending') {
|
|||
|
|
tasks.push({ file, filePath, ...task });
|
|||
|
|
}
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error(`[GLADA-Receiver] ⚠️ 无法解析本地队列文件: ${file} - ${err.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tasks;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证任务规格
|
|||
|
|
* @param {Object} spec - CAB 任务规格
|
|||
|
|
* @returns {{ valid: boolean, errors: string[] }}
|
|||
|
|
*/
|
|||
|
|
function validateTaskSpec(spec) {
|
|||
|
|
const errors = [];
|
|||
|
|
|
|||
|
|
if (!spec.task_id) {
|
|||
|
|
errors.push('缺少 task_id');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!spec.authorization || spec.authorization.sovereign !== '冰朔 · TCS-0002∞') {
|
|||
|
|
errors.push('授权不合法:sovereignty 必须为 "冰朔 · TCS-0002∞"');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!spec.development_plan || !spec.development_plan.title) {
|
|||
|
|
errors.push('缺少开发计划标题 (development_plan.title)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!spec.development_plan || !Array.isArray(spec.development_plan.steps) || spec.development_plan.steps.length === 0) {
|
|||
|
|
errors.push('开发步骤为空 (development_plan.steps)');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { valid: errors.length === 0, errors };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将 CAB 任务规格转换为 GLADA 执行任务
|
|||
|
|
* @param {Object} spec - CAB 任务规格
|
|||
|
|
* @returns {Object} GLADA 执行任务
|
|||
|
|
*/
|
|||
|
|
function convertToGladaTask(spec) {
|
|||
|
|
const now = new Date().toISOString();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
glada_task_id: `GLADA-${spec.task_id}`,
|
|||
|
|
source_task_id: spec.task_id,
|
|||
|
|
status: 'pending',
|
|||
|
|
created_at: now,
|
|||
|
|
updated_at: now,
|
|||
|
|
|
|||
|
|
// 开发计划
|
|||
|
|
plan: {
|
|||
|
|
title: spec.development_plan.title,
|
|||
|
|
description: spec.development_plan.description || '',
|
|||
|
|
steps: spec.development_plan.steps.map((step, i) => ({
|
|||
|
|
step_id: i + 1,
|
|||
|
|
description: typeof step === 'string' ? step : step.description || step.title || `步骤 ${i + 1}`,
|
|||
|
|
status: 'pending',
|
|||
|
|
started_at: null,
|
|||
|
|
completed_at: null,
|
|||
|
|
result: null,
|
|||
|
|
files_changed: [],
|
|||
|
|
reasoning: null
|
|||
|
|
})),
|
|||
|
|
priority: spec.development_plan.priority || 'normal'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 架构上下文
|
|||
|
|
architecture: spec.architecture || {},
|
|||
|
|
|
|||
|
|
// 约束
|
|||
|
|
constraints: spec.constraints || {
|
|||
|
|
no_touch_files: [],
|
|||
|
|
required_tests: true,
|
|||
|
|
deploy_after: false,
|
|||
|
|
max_files_changed: 20
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 推理上下文
|
|||
|
|
reasoning_context: spec.reasoning_context || {},
|
|||
|
|
|
|||
|
|
// 执行记录
|
|||
|
|
execution_log: [],
|
|||
|
|
|
|||
|
|
// 完成信息
|
|||
|
|
completion: {
|
|||
|
|
completed_at: null,
|
|||
|
|
total_files_changed: [],
|
|||
|
|
test_results: null,
|
|||
|
|
git_branch: null,
|
|||
|
|
git_commits: [],
|
|||
|
|
notification_sent: false
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 接收下一个待执行任务
|
|||
|
|
* 优先级:本地队列 > CAB pending 目录
|
|||
|
|
* @returns {Object|null} GLADA 执行任务或 null
|
|||
|
|
*/
|
|||
|
|
function receiveNextTask() {
|
|||
|
|
// 1. 先检查本地队列
|
|||
|
|
const localTasks = scanLocalQueue();
|
|||
|
|
if (localTasks.length > 0) {
|
|||
|
|
const task = localTasks[0];
|
|||
|
|
console.log(`[GLADA-Receiver] 📥 从本地队列取出任务: ${task.glada_task_id}`);
|
|||
|
|
return task;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 再检查 CAB pending 目录
|
|||
|
|
const pendingTasks = scanPendingTasks();
|
|||
|
|
if (pendingTasks.length > 0) {
|
|||
|
|
const { file, filePath, spec } = pendingTasks[0];
|
|||
|
|
|
|||
|
|
// 验证
|
|||
|
|
const validation = validateTaskSpec(spec);
|
|||
|
|
if (!validation.valid) {
|
|||
|
|
console.error(`[GLADA-Receiver] ❌ 任务 ${file} 验证失败:`, validation.errors);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为 GLADA 任务
|
|||
|
|
const gladaTask = convertToGladaTask(spec);
|
|||
|
|
|
|||
|
|
// 保存到本地队列
|
|||
|
|
fs.mkdirSync(GLADA_QUEUE_DIR, { recursive: true });
|
|||
|
|
const queueFile = path.join(GLADA_QUEUE_DIR, `${gladaTask.glada_task_id}.json`);
|
|||
|
|
fs.writeFileSync(queueFile, JSON.stringify(gladaTask, null, 2), 'utf-8');
|
|||
|
|
|
|||
|
|
console.log(`[GLADA-Receiver] 📥 接收CAB任务: ${spec.task_id} → ${gladaTask.glada_task_id}`);
|
|||
|
|
return gladaTask;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新任务状态
|
|||
|
|
* @param {string} gladaTaskId - GLADA 任务 ID
|
|||
|
|
* @param {Object} updates - 要更新的字段
|
|||
|
|
*/
|
|||
|
|
function updateTask(gladaTaskId, updates) {
|
|||
|
|
const filePath = path.join(GLADA_QUEUE_DIR, `${gladaTaskId}.json`);
|
|||
|
|
if (!fs.existsSync(filePath)) {
|
|||
|
|
console.error(`[GLADA-Receiver] ⚠️ 任务文件不存在: ${gladaTaskId}`);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const task = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|||
|
|
Object.assign(task, updates, { updated_at: new Date().toISOString() });
|
|||
|
|
fs.writeFileSync(filePath, JSON.stringify(task, null, 2), 'utf-8');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 将任务标记为已完成并归档
|
|||
|
|
* @param {string} gladaTaskId - GLADA 任务 ID
|
|||
|
|
* @param {Object} completionData - 完成数据
|
|||
|
|
*/
|
|||
|
|
function archiveTask(gladaTaskId, completionData) {
|
|||
|
|
const filePath = path.join(GLADA_QUEUE_DIR, `${gladaTaskId}.json`);
|
|||
|
|
if (!fs.existsSync(filePath)) return false;
|
|||
|
|
|
|||
|
|
const task = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|||
|
|
task.status = 'completed';
|
|||
|
|
task.completion = {
|
|||
|
|
...task.completion,
|
|||
|
|
...completionData,
|
|||
|
|
completed_at: new Date().toISOString()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 归档到完成目录
|
|||
|
|
const archiveDir = path.join(GLADA_QUEUE_DIR, 'completed');
|
|||
|
|
fs.mkdirSync(archiveDir, { recursive: true });
|
|||
|
|
fs.writeFileSync(
|
|||
|
|
path.join(archiveDir, `${gladaTaskId}.json`),
|
|||
|
|
JSON.stringify(task, null, 2),
|
|||
|
|
'utf-8'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 从队列移除
|
|||
|
|
fs.unlinkSync(filePath);
|
|||
|
|
|
|||
|
|
// 同时归档 CAB 源文件
|
|||
|
|
if (task.source_task_id) {
|
|||
|
|
const cabPendingFile = path.join(PENDING_DIR, `${task.source_task_id}.json`);
|
|||
|
|
if (fs.existsSync(cabPendingFile)) {
|
|||
|
|
fs.mkdirSync(COMPLETED_DIR, { recursive: true });
|
|||
|
|
fs.renameSync(cabPendingFile, path.join(COMPLETED_DIR, `${task.source_task_id}.json`));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log(`[GLADA-Receiver] ✅ 任务已归档: ${gladaTaskId}`);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
scanPendingTasks,
|
|||
|
|
scanLocalQueue,
|
|||
|
|
validateTaskSpec,
|
|||
|
|
convertToGladaTask,
|
|||
|
|
receiveNextTask,
|
|||
|
|
updateTask,
|
|||
|
|
archiveTask,
|
|||
|
|
GLADA_QUEUE_DIR
|
|||
|
|
};
|