guanghulab/scripts/grid-db/process-inbox.js

339 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/**
* scripts/grid-db/process-inbox.js
*
* Grid-DB Inbox 消息处理器
*
* 流程
* 扫描 grid-db/inbox/ 中所有 .json 文件
* 校验 dev_id 合法性必须在 DEV 映射表中
* 移动到 grid-db/processing/锁定
* 根据消息 type 分流处理
* - progress-update 更新 task-queue + 触发广播生成
* - dev-log 归档到 interactions + 更新 dev-profile
* - help-request 标记 P0 + 读取 Notion 知识库 生成回复
* - syslog 走标准 SYSLOG 处理流程
* - interaction-dump 写入 interactions/ + 追加 training-lake/raw/
* 生成 outbox 广播如需要 写入 grid-db/outbox/latest/DEV-XXX.json
* 更新 memory 文件task-queue, dev-profile
* 移动已处理消息到日期归档目录
* 写处理日志到 grid-db/logs/
*
* 校验规则
* - message_id 格式必须匹配 [timestamp]-[DEV-XXX]-[type]
* - dev_id 必须在 rules/dev-module-map.json 中存在
* - schema_version 必须为 "1.0"
* - 缺少必填字段 拒绝 + 写错误日志
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
*/
const fs = require('fs');
const path = require('path');
const GRID_DB = path.join(__dirname, '../../grid-db');
const INBOX = path.join(GRID_DB, 'inbox');
const PROCESSING = path.join(GRID_DB, 'processing');
const OUTBOX_LATEST = path.join(GRID_DB, 'outbox/latest');
const OUTBOX_ARCHIVE = path.join(GRID_DB, 'outbox/archive');
const MEMORY = path.join(GRID_DB, 'memory');
const INTERACTIONS = path.join(GRID_DB, 'interactions');
const TRAINING_RAW = path.join(GRID_DB, 'training-lake/raw');
const LOGS = path.join(GRID_DB, 'logs');
const RULES = path.join(GRID_DB, 'rules');
let broadcastCounter = 0;
function nextBroadcastSeq() {
broadcastCounter++;
return String(broadcastCounter).padStart(3, '0');
}
function getDateStr() {
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
function getTimestamp() {
return new Date().toISOString();
}
function writeLog(level, message) {
const logFile = path.join(LOGS, `${getDateStr()}.log`);
const entry = `[${getTimestamp()}] [${level}] ${message}\n`;
fs.appendFileSync(logFile, entry);
console.log(entry.trim());
}
function validateMessage(msg, filename) {
const errors = [];
if (msg.schema_version !== '1.0') {
errors.push(`Invalid schema_version: ${msg.schema_version}`);
}
if (!msg.message_id) {
errors.push('Missing message_id');
}
if (!msg.dev_id || !/^DEV-\d{3}$/.test(msg.dev_id)) {
errors.push(`Invalid dev_id: ${msg.dev_id}`);
}
if (!msg.persona_id) {
errors.push('Missing persona_id');
}
if (!msg.type) {
errors.push('Missing type');
}
if (!msg.payload || !msg.payload.summary) {
errors.push('Missing payload.summary');
}
return errors;
}
function validateDevId(devId, devModuleMap) {
return devModuleMap.mappings && devModuleMap.mappings[devId];
}
function processProgressUpdate(msg) {
// Update task-queue
const taskQueuePath = path.join(MEMORY, msg.dev_id, 'task-queue.json');
if (fs.existsSync(taskQueuePath)) {
const taskQueue = JSON.parse(fs.readFileSync(taskQueuePath, 'utf8'));
taskQueue.tasks.push({
type: 'progress-update',
broadcast_ref: msg.payload.broadcast_ref || null,
summary: msg.payload.summary,
timestamp: msg.timestamp,
status: 'received'
});
taskQueue.last_broadcast_ref = msg.payload.broadcast_ref || taskQueue.last_broadcast_ref;
fs.writeFileSync(taskQueuePath, JSON.stringify(taskQueue, null, 2) + '\n');
writeLog('INFO', `Updated task-queue for ${msg.dev_id}`);
}
// Generate outbox broadcast
const broadcastId = `GRID-BC-${getDateStr()}-${msg.dev_id}-${nextBroadcastSeq()}`;
const broadcast = {
schema_version: '1.0',
broadcast_id: broadcastId,
generated_at: getTimestamp(),
generated_by: 'zhuyuan-workflow',
dev_id: msg.dev_id,
persona_id: msg.persona_id,
type: 'task-directive',
system_format: {
context_update: {
previous_step_verified: true,
source_message: msg.message_id
}
},
human_readable: `收到进度更新: ${msg.payload.summary}`
};
const outboxPath = path.join(OUTBOX_LATEST, `${msg.dev_id}.json`);
fs.writeFileSync(outboxPath, JSON.stringify(broadcast, null, 2) + '\n');
writeLog('INFO', `Generated broadcast ${broadcastId} for ${msg.dev_id}`);
}
function processDevLog(msg) {
// Archive to interactions
const dateStr = getDateStr();
const interDir = path.join(INTERACTIONS, msg.dev_id);
if (!fs.existsSync(interDir)) {
fs.mkdirSync(interDir, { recursive: true });
}
const sessionId = (msg.context && msg.context.session_id) || 'unknown';
const interFile = path.join(interDir, `${dateStr}-${sessionId}.jsonl`);
const record = {
schema_version: '1.0',
timestamp: msg.timestamp,
dev_id: msg.dev_id,
persona_id: msg.persona_id,
session_id: sessionId,
role: 'system',
content: msg.payload.summary,
metadata: { topic: 'dev-log' }
};
fs.appendFileSync(interFile, JSON.stringify(record) + '\n');
// Update dev-profile
const profilePath = path.join(MEMORY, msg.dev_id, 'dev-profile.json');
if (fs.existsSync(profilePath)) {
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
profile.growth_trajectory.push({
timestamp: msg.timestamp,
event: msg.payload.summary
});
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2) + '\n');
}
writeLog('INFO', `Archived dev-log for ${msg.dev_id}`);
}
function processInteractionDump(msg) {
// Write to interactions
const dateStr = getDateStr();
const interDir = path.join(INTERACTIONS, msg.dev_id);
if (!fs.existsSync(interDir)) {
fs.mkdirSync(interDir, { recursive: true });
}
const sessionId = (msg.context && msg.context.session_id) || 'unknown';
const interFile = path.join(interDir, `${dateStr}-${sessionId}.jsonl`);
const record = {
schema_version: '1.0',
timestamp: msg.timestamp,
dev_id: msg.dev_id,
persona_id: msg.persona_id,
session_id: sessionId,
role: 'system',
content: msg.payload.summary,
metadata: { topic: 'interaction-dump' }
};
fs.appendFileSync(interFile, JSON.stringify(record) + '\n');
// Append to training-lake/raw
const batchFile = path.join(TRAINING_RAW, `${dateStr}-batch.jsonl`);
const sample = {
schema_version: '1.0',
sample_id: `${msg.message_id}-sample`,
source_session: sessionId,
dev_id: msg.dev_id,
persona_id: msg.persona_id,
created_at: msg.timestamp,
turns: [{ role: 'system', content: msg.payload.summary, timestamp: msg.timestamp }],
curated: false
};
fs.appendFileSync(batchFile, JSON.stringify(sample) + '\n');
writeLog('INFO', `Archived interaction-dump for ${msg.dev_id}, appended to training-lake`);
}
function processHelpRequest(msg) {
// Mark as P0 and generate immediate response broadcast
const broadcastId = `GRID-BC-${getDateStr()}-${msg.dev_id}-${nextBroadcastSeq()}`;
const broadcast = {
schema_version: '1.0',
broadcast_id: broadcastId,
generated_at: getTimestamp(),
generated_by: 'zhuyuan-workflow',
dev_id: msg.dev_id,
persona_id: msg.persona_id,
type: 'system-notice',
system_format: {
notice_type: 'help-response',
original_request: msg.message_id,
priority: 'P0'
},
human_readable: `收到求助请求: ${msg.payload.summary}。铸渊已标记为 P0 优先处理。`
};
const outboxPath = path.join(OUTBOX_LATEST, `${msg.dev_id}.json`);
fs.writeFileSync(outboxPath, JSON.stringify(broadcast, null, 2) + '\n');
writeLog('WARN', `P0 help-request from ${msg.dev_id}: ${msg.payload.summary}`);
}
async function main() {
writeLog('INFO', '=== Grid-DB Inbox Processor Started ===');
// Load dev-module-map for validation
const devModuleMapPath = path.join(RULES, 'dev-module-map.json');
if (!fs.existsSync(devModuleMapPath)) {
writeLog('ERROR', 'dev-module-map.json not found in rules/');
process.exit(1);
}
const devModuleMap = JSON.parse(fs.readFileSync(devModuleMapPath, 'utf8'));
// Scan inbox
const inboxFiles = fs.readdirSync(INBOX)
.filter(f => f.endsWith('.json'));
if (inboxFiles.length === 0) {
writeLog('INFO', 'No messages in inbox');
return;
}
writeLog('INFO', `Found ${inboxFiles.length} message(s) in inbox`);
let processed = 0;
let rejected = 0;
for (const filename of inboxFiles) {
const inboxPath = path.join(INBOX, filename);
let msg;
try {
msg = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
} catch (err) {
writeLog('ERROR', `Failed to parse ${filename}: ${err.message}`);
rejected++;
continue;
}
// Validate
const errors = validateMessage(msg, filename);
if (errors.length > 0) {
writeLog('ERROR', `Validation failed for ${filename}: ${errors.join(', ')}`);
rejected++;
continue;
}
// Validate dev_id
if (!validateDevId(msg.dev_id, devModuleMap)) {
writeLog('ERROR', `Unknown dev_id ${msg.dev_id} in ${filename}`);
rejected++;
continue;
}
// Move to processing
const processingPath = path.join(PROCESSING, filename);
fs.renameSync(inboxPath, processingPath);
writeLog('INFO', `Processing ${filename} (type: ${msg.type})`);
// Route by type
try {
switch (msg.type) {
case 'progress-update':
processProgressUpdate(msg);
break;
case 'dev-log':
processDevLog(msg);
break;
case 'help-request':
processHelpRequest(msg);
break;
case 'interaction-dump':
processInteractionDump(msg);
break;
case 'syslog':
writeLog('INFO', `Syslog message from ${msg.dev_id}: forwarding to standard pipeline`);
break;
default:
writeLog('WARN', `Unknown message type: ${msg.type}`);
}
// Archive processed message
const archiveDir = path.join(OUTBOX_ARCHIVE, getDateStr());
if (!fs.existsSync(archiveDir)) {
fs.mkdirSync(archiveDir, { recursive: true });
}
fs.renameSync(processingPath, path.join(archiveDir, filename));
processed++;
} catch (err) {
writeLog('ERROR', `Error processing ${filename}: ${err.message}`);
// Move back to inbox for retry
if (fs.existsSync(processingPath)) {
fs.renameSync(processingPath, inboxPath);
}
rejected++;
}
}
writeLog('INFO', `=== Processing complete: ${processed} processed, ${rejected} rejected ===`);
}
main().catch(err => {
console.error('[process-inbox] Fatal error:', err.message);
process.exit(1);
});