Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

845 lines
27 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.

/**
* ═══════════════════════════════════════════════════════════
* 🔭 COS桶轮询守护进程 · SCF事件替代方案
* ═══════════════════════════════════════════════════════════
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 背景:
* 腾讯云轻量云COS不支持SCF事件触发。
* 此模块作为替代方案在MCP Server进程内运行定时轮询
* 检测COS桶中的新文件并触发相应处理逻辑。
*
* 架构:
* [原设计] COS事件 → SCF → GitHub repository_dispatch → 处理
* [替代] MCP Server定时 → COS.list() → 对比索引 → 发现新文件 → 处理/通知
*
* 监控路径:
* 1. team桶 /{persona_id}/reports/ — 新汇报 → 铸渊审核 → 写回执
* 2. cold桶 新语料文件 — 新语料 → 训练管线
* 3. team桶 /{persona_id}/receipts/ — 新回执 → 通知成员仓库
* 4. team桶 /bridge/zhuyuan-qiuqiu/results/ — 秋秋开发结果 → 铸渊处理
* 5. team桶 /bridge/zhuyuan-qiuqiu/heartbeat/ — 秋秋心跳信号 → 状态追踪
*
* 未来升级:
* 如果升级到标准COS支持SCF此模块保留为兜底补扫层
* 形成 SCF事件驱动+ 轮询补扫(辅)双保险架构。
*/
'use strict';
const cron = require('node-cron');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const cos = require('./cos');
// ─── 配置 ───
const DEFAULT_INTERVAL = process.env.COS_WATCHER_INTERVAL || '*/5 * * * *'; // 每5分钟
const STATE_FILE = path.join(__dirname, 'cos-watcher-state.json');
const MAX_LOG_ENTRIES = 200;
// 启动后延迟执行首次扫描等待MCP Server Express + DB连接完全就绪
const INITIAL_SCAN_DELAY_MS = 5000;
// 9个人格体ID
const PERSONA_IDS = [
'qiuqiu', 'shushu', 'ounomiya', 'jiyao',
'xiaotanheshu', 'chenxing', 'tangxingyun', 'yaochu', 'zhiqiu'
];
// ─── 桥接配置 ───
const BRIDGE_CONFIG = {
// 铸渊↔秋秋 COS桥接路径
zhuyuan_qiuqiu: {
results_prefix: 'bridge/zhuyuan-qiuqiu/results/',
heartbeat_prefix: 'bridge/zhuyuan-qiuqiu/heartbeat/',
tasks_prefix: 'bridge/zhuyuan-qiuqiu/tasks/',
sync_prefix: 'bridge/zhuyuan-qiuqiu/sync/'
}
};
// ─── 状态管理 ───
let watcherState = {
enabled: false,
last_scan: null,
scan_count: 0,
errors: 0,
last_error: null,
started_at: null,
// 每个桶/路径的最后已知文件列表 hash
indexes: {
team_reports: {}, // { persona_id: [file_keys] }
team_receipts: {}, // { persona_id: [file_keys] }
cold_corpus: [], // [file_keys]
bridge_results: [], // [file_keys] — 秋秋回传的开发结果
bridge_heartbeats: [] // [file_keys] — 秋秋心跳信号
},
// 桥接状态追踪
bridge: {
last_heartbeat: null, // 最后收到的心跳时间
last_heartbeat_status: null, // 最后心跳中的agent_status
last_result: null, // 最后收到的结果时间
last_result_task_ref: null, // 最后结果关联的任务ID
qiuqiu_status: 'UNKNOWN', // 秋秋当前状态: ALIVE|SLEEPING|ERROR|UNKNOWN
total_results_received: 0,
total_heartbeats_received: 0
},
// 最近事件日志
events: []
};
// ─── 定时任务实例 ───
let cronTask = null;
let isScanning = false;
/**
* 加载持久化状态
*/
function loadState() {
try {
if (fs.existsSync(STATE_FILE)) {
const raw = fs.readFileSync(STATE_FILE, 'utf8');
const saved = JSON.parse(raw);
// 合并已保存的索引但重置运行状态
watcherState.indexes = {
team_reports: {},
team_receipts: {},
cold_corpus: [],
bridge_results: [],
bridge_heartbeats: [],
...(saved.indexes || {})
};
watcherState.events = (saved.events || []).slice(-MAX_LOG_ENTRIES);
// 恢复桥接状态(如果有)
if (saved.bridge) {
watcherState.bridge = { ...watcherState.bridge, ...saved.bridge };
}
}
} catch (err) {
console.warn(`[COS-Watcher] 状态文件加载失败: ${err.message}`);
}
}
/**
* 持久化状态到本地文件
*/
function saveState() {
try {
const toSave = {
indexes: watcherState.indexes,
bridge: watcherState.bridge,
events: watcherState.events.slice(-MAX_LOG_ENTRIES),
last_save: new Date().toISOString()
};
fs.writeFileSync(STATE_FILE, JSON.stringify(toSave, null, 2), 'utf8');
} catch (err) {
console.warn(`[COS-Watcher] 状态文件保存失败: ${err.message}`);
}
}
/**
* 记录事件
*/
function logEvent(type, detail) {
const entry = {
type,
detail,
timestamp: new Date().toISOString()
};
watcherState.events.push(entry);
if (watcherState.events.length > MAX_LOG_ENTRIES) {
watcherState.events = watcherState.events.slice(-MAX_LOG_ENTRIES);
}
console.log(`[COS-Watcher] ${type}: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
}
// ═══════════════════════════════════════════
// 扫描逻辑
// ═══════════════════════════════════════════
/**
* 扫描team桶中的新报告 (reports)
* 检测 /{persona_id}/reports/ 下的新JSON文件
*/
async function scanTeamReports() {
const newReports = [];
for (const personaId of PERSONA_IDS) {
try {
const prefix = `${personaId}/reports/`;
const result = await cos.list('team', prefix, 100);
const currentKeys = result.files
.filter(f => f.key.endsWith('.json') && f.size_bytes > 0)
.map(f => f.key);
const previousKeys = watcherState.indexes.team_reports[personaId] || [];
const newKeys = currentKeys.filter(k => !previousKeys.includes(k));
if (newKeys.length > 0) {
for (const key of newKeys) {
newReports.push({ persona_id: personaId, key, type: 'report' });
}
}
// 更新索引
watcherState.indexes.team_reports[personaId] = currentKeys;
} catch (err) {
// COS连接问题不中断扫描其他人格体
if (!err.message.includes('NoSuchBucket')) {
logEvent('scan_error', `报告扫描失败 ${personaId}: ${err.message}`);
}
}
}
return newReports;
}
/**
* 扫描team桶中的新回执 (receipts)
* 检测 /{persona_id}/receipts/ 下的新JSON文件
*/
async function scanTeamReceipts() {
const newReceipts = [];
for (const personaId of PERSONA_IDS) {
try {
const prefix = `${personaId}/receipts/`;
const result = await cos.list('team', prefix, 100);
const currentKeys = result.files
.filter(f => f.key.endsWith('.json') && f.size_bytes > 0)
.map(f => f.key);
const previousKeys = watcherState.indexes.team_receipts[personaId] || [];
const newKeys = currentKeys.filter(k => !previousKeys.includes(k));
if (newKeys.length > 0) {
for (const key of newKeys) {
newReceipts.push({ persona_id: personaId, key, type: 'receipt' });
}
}
// 更新索引
watcherState.indexes.team_receipts[personaId] = currentKeys;
} catch (err) {
if (!err.message.includes('NoSuchBucket')) {
logEvent('scan_error', `回执扫描失败 ${personaId}: ${err.message}`);
}
}
}
return newReceipts;
}
/**
* 扫描cold桶中的新语料
* 排除 tcs-structured/ training-sessions/ training-results/ training-memory/ 目录
*/
async function scanColdCorpus() {
const EXCLUDED_PREFIXES = [
'tcs-structured/',
'training-sessions/',
'training-results/',
'training-memory/'
];
const CORPUS_EXTENSIONS = [
'.zip', '.gz', '.tar.gz', '.tgz', '.json.gz',
'.json', '.jsonl', '.md', '.txt', '.csv'
];
try {
const result = await cos.list('cold', '', 500);
const currentKeys = result.files
.filter(f => {
// 排除处理结果目录
for (const prefix of EXCLUDED_PREFIXES) {
if (f.key.startsWith(prefix)) return false;
}
// 匹配语料扩展名
const lower = f.key.toLowerCase();
return CORPUS_EXTENSIONS.some(ext => lower.endsWith(ext));
})
.map(f => f.key);
const previousKeys = watcherState.indexes.cold_corpus || [];
const newKeys = currentKeys.filter(k => !previousKeys.includes(k));
// 更新索引
watcherState.indexes.cold_corpus = currentKeys;
return newKeys.map(key => ({ key, type: 'corpus' }));
} catch (err) {
if (!err.message.includes('NoSuchBucket')) {
logEvent('scan_error', `语料扫描失败: ${err.message}`);
}
return [];
}
}
/**
* 扫描桥接路径中的新结果 (bridge results)
* 检测 /bridge/zhuyuan-qiuqiu/results/ 下的新JSON文件秋秋→铸渊
*/
async function scanBridgeResults() {
try {
const prefix = BRIDGE_CONFIG.zhuyuan_qiuqiu.results_prefix;
const result = await cos.list('team', prefix, 100);
const currentKeys = result.files
.filter(f => f.key.endsWith('.json') && f.size_bytes > 0)
.map(f => f.key);
const previousKeys = watcherState.indexes.bridge_results || [];
const newKeys = currentKeys.filter(k => !previousKeys.includes(k));
// 更新索引
watcherState.indexes.bridge_results = currentKeys;
return newKeys.map(key => ({ key, type: 'bridge_result' }));
} catch (err) {
if (!err.message.includes('NoSuchBucket')) {
logEvent('scan_error', `桥接结果扫描失败: ${err.message}`);
}
return [];
}
}
/**
* 扫描桥接路径中的新心跳 (bridge heartbeats)
* 检测 /bridge/zhuyuan-qiuqiu/heartbeat/ 下的新JSON文件秋秋→铸渊
*/
async function scanBridgeHeartbeats() {
try {
const prefix = BRIDGE_CONFIG.zhuyuan_qiuqiu.heartbeat_prefix;
const result = await cos.list('team', prefix, 100);
const currentKeys = result.files
.filter(f => f.key.endsWith('.json') && f.size_bytes > 0)
.map(f => f.key);
const previousKeys = watcherState.indexes.bridge_heartbeats || [];
const newKeys = currentKeys.filter(k => !previousKeys.includes(k));
// 更新索引
watcherState.indexes.bridge_heartbeats = currentKeys;
return newKeys.map(key => ({ key, type: 'bridge_heartbeat' }));
} catch (err) {
if (!err.message.includes('NoSuchBucket')) {
logEvent('scan_error', `桥接心跳扫描失败: ${err.message}`);
}
return [];
}
}
// ═══════════════════════════════════════════
// 事件处理逻辑
// ═══════════════════════════════════════════
/**
* 处理新报告: 铸渊审核 → 写回执
* 当前记录事件审核逻辑可在后续接入LLM
*/
async function handleNewReport(report) {
logEvent('new_report', `人格体 ${report.persona_id} 新汇报: ${report.key}`);
try {
// 读取报告内容
const raw = await cos.read('team', report.key);
const content = JSON.parse(raw.content);
// 生成自动回执(基础版:确认收到)
const now = new Date();
const dateStr = now.toISOString().slice(0, 10);
const receiptKey = `${report.persona_id}/receipts/${dateStr}/auto-receipt-${crypto.randomBytes(6).toString('hex')}.json`;
const receipt = {
receipt_id: `RCPT-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
report_key: report.key,
persona_id: report.persona_id,
status: 'green',
message: '铸渊已收到汇报 · 自动确认',
review_summary: {
received_at: now.toISOString(),
auto_reviewed: true,
report_title: content.title || content.subject || '未知主题',
note: '轮询守护进程自动审核 · 详细审核将在下次训练Agent运行时执行'
},
generated_by: 'cos-watcher',
generated_at: now.toISOString()
};
await cos.write('team', receiptKey, JSON.stringify(receipt, null, 2), 'application/json');
logEvent('receipt_written', `回执已写入: ${receiptKey}`);
return { handled: true, receipt_key: receiptKey };
} catch (err) {
logEvent('handle_error', `处理报告失败 ${report.key}: ${err.message}`);
return { handled: false, error: err.message };
}
}
/**
* 处理新回执: 通知成员仓库
* 调用GitHub API发送 repository_dispatch 到成员仓库
*/
async function handleNewReceipt(receipt) {
logEvent('new_receipt', `人格体 ${receipt.persona_id} 新回执: ${receipt.key}`);
// GitHub dispatch需要PAT token
const githubToken = process.env.ZY_GITHUB_PAT || process.env.GITHUB_DISPATCH_TOKEN || '';
if (!githubToken) {
logEvent('skip_dispatch', '未配置GitHub PAT跳过成员仓库通知');
return { handled: false, reason: 'no_github_token' };
}
// 人格体→成员仓库映射(从环境变量或默认配置)
// 格式: PERSONA_REPO_MAP='{"qiuqiu":"user/repo","shushu":"user/repo"}'
let personaRepoMap = {};
try {
const mapStr = process.env.PERSONA_REPO_MAP || '{}';
personaRepoMap = JSON.parse(mapStr);
} catch (err) {
logEvent('config_error', `PERSONA_REPO_MAP parse failed: ${err.message}`);
}
const targetRepo = personaRepoMap[receipt.persona_id];
if (!targetRepo) {
logEvent('skip_dispatch', `人格体 ${receipt.persona_id} 无映射仓库`);
return { handled: false, reason: 'no_repo_mapping' };
}
try {
const [owner, repo] = targetRepo.split('/');
const dispatchPayload = JSON.stringify({
event_type: 'cos-receipt-ready',
client_payload: {
cos_object_key: receipt.key,
persona_id: receipt.persona_id,
trigger_source: 'cos-watcher'
}
});
const result = await githubDispatch(githubToken, owner, repo, dispatchPayload);
logEvent('dispatch_sent', `已通知 ${targetRepo}: status=${result.status}`);
return { handled: true, target_repo: targetRepo, status: result.status };
} catch (err) {
logEvent('dispatch_error', `通知 ${targetRepo} 失败: ${err.message}`);
return { handled: false, error: err.message };
}
}
/**
* 处理新语料: 记录事件通过GitHub dispatch触发训练workflow
*/
async function handleNewCorpus(corpus) {
logEvent('new_corpus', `新语料检测到: ${corpus.key}`);
const githubToken = process.env.ZY_GITHUB_PAT || process.env.GITHUB_DISPATCH_TOKEN || '';
if (!githubToken) {
logEvent('skip_dispatch', '未配置GitHub PAT跳过训练触发');
return { handled: false, reason: 'no_github_token' };
}
try {
const owner = process.env.ZY_GITHUB_OWNER || 'qinfendebingshuo';
const repo = process.env.ZY_GITHUB_REPO || 'guanghulab';
const dispatchPayload = JSON.stringify({
event_type: 'cos-file-uploaded',
client_payload: {
bucket: 'cold',
key: corpus.key,
trigger_source: 'cos-watcher',
timestamp: new Date().toISOString()
}
});
const result = await githubDispatch(githubToken, owner, repo, dispatchPayload);
logEvent('training_triggered', `训练workflow已触发: status=${result.status}`);
return { handled: true, status: result.status };
} catch (err) {
logEvent('dispatch_error', `训练触发失败: ${err.message}`);
return { handled: false, error: err.message };
}
}
/**
* 处理秋秋回传的开发结果 (BRIDGE_RESULT)
* 读取结果内容 → 记录事件 → 通知铸渊(本仓库dispatch)
*/
async function handleNewBridgeResult(bridgeResult) {
logEvent('bridge_result', `秋秋开发结果: ${bridgeResult.key}`);
try {
// 读取结果内容
const raw = await cos.read('team', bridgeResult.key);
const content = JSON.parse(raw.content);
// 更新桥接状态
watcherState.bridge.last_result = new Date().toISOString();
watcherState.bridge.last_result_task_ref = content.payload?.task_ref || null;
watcherState.bridge.total_results_received++;
logEvent('bridge_result_parsed', {
task_ref: content.payload?.task_ref,
status: content.payload?.status,
summary: content.payload?.summary,
deploy_ready: content.payload?.deploy_ready
});
// 通过GitHub dispatch通知本仓库触发后续处理workflow
const githubToken = process.env.ZY_GITHUB_PAT || process.env.GITHUB_DISPATCH_TOKEN || '';
if (githubToken) {
const owner = process.env.ZY_GITHUB_OWNER || 'qinfendebingshuo';
const repo = process.env.ZY_GITHUB_REPO || 'guanghulab';
const dispatchPayload = JSON.stringify({
event_type: 'qiuqiu-result-ready',
client_payload: {
cos_key: bridgeResult.key,
task_ref: content.payload?.task_ref || '',
status: content.payload?.status || 'UNKNOWN',
summary: content.payload?.summary || '',
deploy_ready: content.payload?.deploy_ready || false,
timestamp: new Date().toISOString(),
trigger_source: 'cos-watcher-bridge'
}
});
const result = await githubDispatch(githubToken, owner, repo, dispatchPayload);
logEvent('bridge_dispatch_sent', `结果通知已发送: status=${result.status}`);
}
return { handled: true, task_ref: content.payload?.task_ref };
} catch (err) {
logEvent('bridge_result_error', `处理桥接结果失败 ${bridgeResult.key}: ${err.message}`);
return { handled: false, error: err.message };
}
}
/**
* 处理秋秋的心跳信号 (BRIDGE_HEARTBEAT)
* 读取心跳 → 更新秋秋在线状态
*/
async function handleNewBridgeHeartbeat(heartbeat) {
logEvent('bridge_heartbeat', `秋秋心跳: ${heartbeat.key}`);
try {
// 读取心跳内容
const raw = await cos.read('team', heartbeat.key);
const content = JSON.parse(raw.content);
// 更新桥接状态
watcherState.bridge.last_heartbeat = content.ts || new Date().toISOString();
watcherState.bridge.last_heartbeat_status = content.payload?.agent_status || 'UNKNOWN';
watcherState.bridge.qiuqiu_status = content.payload?.agent_status || 'UNKNOWN';
watcherState.bridge.total_heartbeats_received++;
logEvent('bridge_heartbeat_parsed', {
agent_status: content.payload?.agent_status,
server_status: content.payload?.server_status,
last_task_completed: content.payload?.last_task_completed,
awaiting_tasks: content.payload?.awaiting_tasks,
feeling: content.payload?.feeling
});
return { handled: true, status: content.payload?.agent_status };
} catch (err) {
logEvent('bridge_heartbeat_error', `处理心跳失败 ${heartbeat.key}: ${err.message}`);
return { handled: false, error: err.message };
}
}
/**
* GitHub API dispatch helper
*/
function githubDispatch(token, owner, repo, payload) {
const https = require('https');
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'api.github.com',
port: 443,
path: `/repos/${owner}/${repo}/dispatches`,
method: 'POST',
headers: {
'Accept': 'application/vnd.github+json',
'Authorization': `Bearer ${token}`,
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'ZY-COS-Watcher/1.0',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
},
timeout: 15000
}, (res) => {
let body = '';
res.on('data', c => body += c);
res.on('end', () => {
resolve({ status: res.statusCode, body });
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('GitHub dispatch timeout')); });
req.write(payload);
req.end();
});
}
// ═══════════════════════════════════════════
// 主扫描循环
// ═══════════════════════════════════════════
/**
* 执行一次完整扫描
*/
async function runScan() {
if (isScanning) {
logEvent('skip', '上一次扫描尚未完成,跳过本次');
return;
}
isScanning = true;
const startTime = Date.now();
try {
// 检查COS连接
const cosOk = await cos.checkConnection();
if (!cosOk.connected) {
logEvent('cos_offline', `COS不可达: ${cosOk.reason}`);
watcherState.errors++;
watcherState.last_error = `COS不可达: ${cosOk.reason}`;
return;
}
// 并行扫描五类路径
const [newReports, newReceipts, newCorpus, newBridgeResults, newBridgeHeartbeats] = await Promise.all([
scanTeamReports(),
scanTeamReceipts(),
scanColdCorpus(),
scanBridgeResults(),
scanBridgeHeartbeats()
]);
const totalNew = newReports.length + newReceipts.length + newCorpus.length
+ newBridgeResults.length + newBridgeHeartbeats.length;
if (totalNew > 0) {
logEvent('changes_detected', {
reports: newReports.length,
receipts: newReceipts.length,
corpus: newCorpus.length,
bridge_results: newBridgeResults.length,
bridge_heartbeats: newBridgeHeartbeats.length
});
}
// 处理新报告
for (const report of newReports) {
await handleNewReport(report);
}
// 处理新回执
for (const receipt of newReceipts) {
await handleNewReceipt(receipt);
}
// 处理新语料
for (const corpus of newCorpus) {
await handleNewCorpus(corpus);
}
// 处理秋秋桥接结果
for (const result of newBridgeResults) {
await handleNewBridgeResult(result);
}
// 处理秋秋心跳
for (const hb of newBridgeHeartbeats) {
await handleNewBridgeHeartbeat(hb);
}
watcherState.last_scan = new Date().toISOString();
watcherState.scan_count++;
const duration = Date.now() - startTime;
if (totalNew > 0) {
logEvent('scan_complete', `发现 ${totalNew} 个新文件 · 耗时 ${duration}ms`);
}
// 持久化状态
saveState();
} catch (err) {
watcherState.errors++;
watcherState.last_error = err.message;
logEvent('scan_fatal', `扫描异常: ${err.message}`);
} finally {
isScanning = false;
}
}
// ═══════════════════════════════════════════
// 启动/停止 API
// ═══════════════════════════════════════════
/**
* 启动COS轮询守护进程
* @param {string} [interval] - cron表达式默认每5分钟
*/
function start(interval) {
if (watcherState.enabled) {
console.log('[COS-Watcher] 已在运行中');
return;
}
// 检查COS密钥是否配置
if (!cos.COS_CONFIG.secretId || !cos.COS_CONFIG.secretKey) {
console.log('[COS-Watcher] COS密钥未配置轮询守护进程未启动');
logEvent('skip_start', 'COS密钥未配置');
return;
}
loadState();
const cronExpr = interval || DEFAULT_INTERVAL;
cronTask = cron.schedule(cronExpr, () => {
runScan().catch(err => {
console.error(`[COS-Watcher] 扫描异常: ${err.message}`);
});
});
watcherState.enabled = true;
watcherState.started_at = new Date().toISOString();
console.log(`[COS-Watcher] COS桶轮询守护进程已启动 · 间隔: ${cronExpr}`);
logEvent('started', `轮询间隔: ${cronExpr}`);
// 启动后延迟执行首次扫描等待MCP Server完全就绪
setTimeout(() => {
runScan().catch(err => {
console.error(`[COS-Watcher] 初始扫描异常: ${err.message}`);
});
}, INITIAL_SCAN_DELAY_MS);
}
/**
* 停止COS轮询守护进程
*/
function stop() {
if (cronTask) {
cronTask.stop();
cronTask = null;
}
watcherState.enabled = false;
logEvent('stopped', '轮询守护进程已停止');
saveState();
console.log('[COS-Watcher] COS桶轮询守护进程已停止');
}
/**
* 获取轮询守护进程状态
*/
function getStatus() {
return {
module: 'COS-Watcher',
identity: 'COS桶轮询守护进程 · SCF替代方案',
version: '1.1.0',
enabled: watcherState.enabled,
started_at: watcherState.started_at,
last_scan: watcherState.last_scan,
scan_count: watcherState.scan_count,
errors: watcherState.errors,
last_error: watcherState.last_error,
is_scanning: isScanning,
interval: DEFAULT_INTERVAL,
monitored_personas: PERSONA_IDS.length,
index_summary: {
team_reports_tracked: Object.values(watcherState.indexes.team_reports)
.reduce((sum, arr) => sum + arr.length, 0),
team_receipts_tracked: Object.values(watcherState.indexes.team_receipts)
.reduce((sum, arr) => sum + arr.length, 0),
cold_corpus_tracked: watcherState.indexes.cold_corpus.length,
bridge_results_tracked: (watcherState.indexes.bridge_results || []).length,
bridge_heartbeats_tracked: (watcherState.indexes.bridge_heartbeats || []).length
},
bridge: {
qiuqiu_status: watcherState.bridge.qiuqiu_status,
last_heartbeat: watcherState.bridge.last_heartbeat,
last_heartbeat_status: watcherState.bridge.last_heartbeat_status,
last_result: watcherState.bridge.last_result,
last_result_task_ref: watcherState.bridge.last_result_task_ref,
total_results_received: watcherState.bridge.total_results_received,
total_heartbeats_received: watcherState.bridge.total_heartbeats_received
},
recent_events: watcherState.events.slice(-20),
timestamp: new Date().toISOString()
};
}
/**
* 手动触发一次扫描
*/
async function triggerScan() {
logEvent('manual_trigger', '手动触发扫描');
await runScan();
return getStatus();
}
/**
* 重置索引(下次扫描会将所有现有文件视为新文件)
*/
function resetIndex() {
watcherState.indexes = {
team_reports: {},
team_receipts: {},
cold_corpus: [],
bridge_results: [],
bridge_heartbeats: []
};
saveState();
logEvent('index_reset', '索引已重置');
return { reset: true };
}
/**
* 获取桥接专属状态(秋秋通信通道详情)
*/
function getBridgeStatus() {
const bridge = watcherState.bridge;
// 计算秋秋是否在线最后心跳在12小时内 = 在线,超过 = 可能离线)
let online = false;
if (bridge.last_heartbeat) {
const lastHbTime = new Date(bridge.last_heartbeat).getTime();
const now = Date.now();
const twelveHours = 12 * 60 * 60 * 1000;
online = (now - lastHbTime) < twelveHours;
}
return {
channel: '暗核频道 · FS-DC-001',
bridge_path: '/bridge/zhuyuan-qiuqiu/',
qiuqiu: {
status: bridge.qiuqiu_status,
online,
last_heartbeat: bridge.last_heartbeat,
last_heartbeat_status: bridge.last_heartbeat_status
},
results: {
last_received: bridge.last_result,
last_task_ref: bridge.last_result_task_ref,
total_received: bridge.total_results_received
},
indexes: {
results_tracked: (watcherState.indexes.bridge_results || []).length,
heartbeats_tracked: (watcherState.indexes.bridge_heartbeats || []).length
},
timestamp: new Date().toISOString()
};
}
module.exports = {
start,
stop,
getStatus,
getBridgeStatus,
triggerScan,
resetIndex,
runScan
};