/** * ═══════════════════════════════════════════════════════════ * 🔭 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 };