339 lines
10 KiB
JavaScript
339 lines
10 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|