guanghulab/scripts/skyeye/scan-bulletin-sfp.js
2026-05-10 13:12:44 +08:00

164 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// scripts/skyeye/scan-bulletin-sfp.js
// 天眼·扫描模块D14 · 公告板指纹完整性检查
//
// 扫描内容:
// ① 扫描 data/bulletin-board/ 下所有留言
// ② 检查每条留言是否携带有效 SFP 指纹
// ③ 统计无指纹/无效指纹内容数
// ④ 统计各Agent留言活跃度
//
// 输出JSON → stdout
'use strict';
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const BULLETIN_DIR = path.join(ROOT, 'data/bulletin-board');
const SECURITY_DIR = path.join(ROOT, 'data/security');
const SFP_CONFIG_PATH = path.join(SECURITY_DIR, 'sfp-config.json');
const SFP_REGEX = /⌜SFP::([^:]+)::([^:]+)::([0-9T+:.-]+)::([a-f0-9]{12})::([a-zA-Z0-9]{6})⌝/;
// ━━━ 安全读取 JSON ━━━
function readJSON(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
// ━━━ 扫描目录中的 JSON 文件内容 ━━━
function scanDirectory(dirPath) {
const results = [];
try {
if (!fs.existsSync(dirPath)) return results;
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
for (const file of files) {
const filePath = path.join(dirPath, file);
const content = fs.readFileSync(filePath, 'utf8');
results.push({ file, path: filePath, content });
}
} catch (e) {
// ignore
}
return results;
}
// ━━━ 检查内容中的 SFP 指纹 ━━━
function checkFingerprint(content, config) {
const match = content.match(SFP_REGEX);
if (!match) {
return { has_fingerprint: false, valid: false, reason: '无指纹' };
}
const [, agentId, personaChain] = match;
// 检查 agent 是否在受信列表
const agent = (config.trusted_agents || []).find(a => a.agent_id === agentId);
if (!agent) {
return { has_fingerprint: true, valid: false, reason: `无效Agent ID: ${agentId}` };
}
// 检查亲子链
if (agent.persona_chain !== personaChain) {
return { has_fingerprint: true, valid: false, reason: `亲子链不匹配: ${agentId}` };
}
return { has_fingerprint: true, valid: true, agent_id: agentId, agent_name: agent.name };
}
// ━━━ D14 主扫描 ━━━
function scanBulletinSFP() {
const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
const now = new Date();
const bjTime = new Date(now.getTime() + BEIJING_OFFSET_MS).toISOString()
.replace('T', ' ').slice(0, 19) + '+08:00';
const config = readJSON(SFP_CONFIG_PATH) || { trusted_agents: [] };
const result = {
dimension: 'D14',
name: '公告板指纹完整性',
scan_time: bjTime,
status: '✅',
total_messages: 0,
with_fingerprint: 0,
without_fingerprint: 0,
valid_fingerprints: 0,
invalid_fingerprints: 0,
agent_activity: {},
issues: [],
sfp_config_exists: fs.existsSync(SFP_CONFIG_PATH)
};
// 检查 SFP 配置是否存在
if (!result.sfp_config_exists) {
result.status = '❌';
result.issues.push({
type: 'config_missing',
message: 'SFP配置文件缺失',
action: 'P0工单 · 恢复sfp-config.json'
});
console.log(JSON.stringify(result, null, 2));
return result;
}
// 扫描公告板各区域
const areas = ['comments', 'config-shares', 'receipts', 'work-orders'];
for (const area of areas) {
const dirPath = path.join(BULLETIN_DIR, area);
const files = scanDirectory(dirPath);
for (const file of files) {
// 跳过 .gitkeep
if (file.file === '.gitkeep') continue;
result.total_messages++;
const check = checkFingerprint(file.content, config);
if (check.has_fingerprint) {
result.with_fingerprint++;
if (check.valid) {
result.valid_fingerprints++;
// 统计活跃度
const agentId = check.agent_id;
result.agent_activity[agentId] = (result.agent_activity[agentId] || 0) + 1;
} else {
result.invalid_fingerprints++;
result.issues.push({
type: 'invalid_fingerprint',
file: file.file,
area,
reason: check.reason,
action: 'P0工单 · 清理无效指纹内容'
});
}
} else {
result.without_fingerprint++;
result.issues.push({
type: 'no_fingerprint',
file: file.file,
area,
action: '标记待清理'
});
}
}
}
// 确定整体状态
if (result.invalid_fingerprints > 0) {
result.status = '❌';
} else if (result.without_fingerprint > 0) {
result.status = '⚠️';
}
console.log(JSON.stringify(result, null, 2));
return result;
}
scanBulletinSFP();