feat: COS桶轮询守护进程 · SCF事件替代方案 (cos-watcher.js)
新增 MCP Server 内置 COS 轮询守护模块,替代不可用的 SCF 事件触发: - cos-watcher.js: 核心轮询引擎(node-cron · 每5分钟) - cos-watcher-ops.js: 3个MCP工具(status/trigger/reset) - server.js: 注册工具 + REST端点 + 健康检查集成 + 启停管理 - 架构文档更新标注SCF不可用 + 轮询替代方案 - 训练Agent workflow添加SCF限制说明 Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/991cbbcf-9e83-43c1-9225-e235e0a61f06 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
feca79cde3
commit
b487b6bc57
|
|
@ -13,6 +13,12 @@
|
|||
# - 手动: workflow_dispatch
|
||||
# - COS桶告警: repository_dispatch (cos-alert事件)
|
||||
# - COS Webhook: repository_dispatch (cos-file-uploaded事件)
|
||||
#
|
||||
# ⚠️ SCF限制说明 (2026-04-10):
|
||||
# 腾讯云轻量云COS不支持SCF事件触发(ObjectCreated)。
|
||||
# repository_dispatch触发保留但当前由MCP Server内置轮询守护
|
||||
# (cos-watcher.js)代替SCF发送dispatch。
|
||||
# 未来升级标准COS后可恢复SCF事件驱动。
|
||||
|
||||
name: 铸渊训练Agent · 语料处理
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@
|
|||
"_meta": {
|
||||
"doc_id": "ARCH-COS-001",
|
||||
"name": "COS共享桶基础设施架构",
|
||||
"version": "1.0",
|
||||
"description": "铸渊侧COS共享桶完整架构 · 9个人格体目录权限 · SCF云函数 · Notion回执同步",
|
||||
"version": "1.1",
|
||||
"description": "铸渊侧COS共享桶完整架构 · 9个人格体目录权限 · 轮询守护替代SCF · Notion回执同步",
|
||||
"author": "铸渊 · TCS-ZY001",
|
||||
"sovereign": "冰朔 · TCS-0002∞",
|
||||
"copyright": "国作登字-2026-A-00037559",
|
||||
"created": "2026-04-04"
|
||||
"created": "2026-04-04",
|
||||
"updated": "2026-04-10"
|
||||
},
|
||||
|
||||
"bucket": {
|
||||
|
|
@ -108,7 +109,37 @@
|
|||
},
|
||||
|
||||
"scf_functions": {
|
||||
"description": "SCF云函数配置 · 2个函数",
|
||||
"description": "SCF云函数配置 · ⚠️ 轻量云COS不支持SCF事件触发",
|
||||
"status": "UNAVAILABLE",
|
||||
"reason": "腾讯云轻量云COS不支持配置SCF事件通知(ObjectCreated事件)。以下SCF设计保留为未来升级标准COS时的参考。当前由MCP Server内置轮询守护进程(cos-watcher.js)替代。",
|
||||
"replacement": {
|
||||
"module": "cos-watcher.js",
|
||||
"location": "server/age-os/mcp-server/cos-watcher.js",
|
||||
"mechanism": "MCP Server内置定时轮询(node-cron · 每5分钟)",
|
||||
"design": "[原设计] COS事件→SCF→GitHub dispatch → [替代] MCP Server定时→COS.list()→对比索引→发现新文件→处理/通知",
|
||||
"advantages": [
|
||||
"零额外部署(复用已运行的MCP Server进程)",
|
||||
"不消耗GitHub Actions配额",
|
||||
"延迟可控(5分钟轮询)",
|
||||
"天然补扫能力,不会漏消息",
|
||||
"未来可与SCF事件驱动形成双保险"
|
||||
],
|
||||
"monitored_paths": [
|
||||
"team桶 /{persona_id}/reports/ → 新汇报 → 铸渊审核 → 写回执",
|
||||
"cold桶 新语料文件 → 新语料 → 训练管线触发",
|
||||
"team桶 /{persona_id}/receipts/ → 新回执 → 通知成员仓库"
|
||||
],
|
||||
"rest_api": [
|
||||
"GET /cos-watcher/status — 轮询守护状态",
|
||||
"POST /cos-watcher/scan — 手动触发扫描",
|
||||
"POST /cos-watcher/reset — 重置文件索引"
|
||||
],
|
||||
"mcp_tools": [
|
||||
"cosWatcherStatus — 查看轮询守护进程状态",
|
||||
"cosWatcherTriggerScan — 手动触发一次扫描",
|
||||
"cosWatcherResetIndex — 重置文件索引"
|
||||
]
|
||||
},
|
||||
|
||||
"report_trigger": {
|
||||
"function_name": "zy-cos-report-trigger",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"indexes": {
|
||||
"team_reports": {},
|
||||
"team_receipts": {},
|
||||
"cold_corpus": []
|
||||
},
|
||||
"events": [],
|
||||
"last_save": null
|
||||
}
|
||||
|
|
@ -0,0 +1,599 @@
|
|||
/**
|
||||
* ═══════════════════════════════════════════════════════════
|
||||
* 🔭 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/ — 新回执 → 通知成员仓库
|
||||
*
|
||||
* 未来升级:
|
||||
* 如果升级到标准COS(支持SCF),此模块保留为兜底补扫层,
|
||||
* 形成 SCF事件驱动(主)+ 轮询补扫(辅)双保险架构。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const cron = require('node-cron');
|
||||
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;
|
||||
|
||||
// 9个人格体ID
|
||||
const PERSONA_IDS = [
|
||||
'qiuqiu', 'shushu', 'ounomiya', 'jiyao',
|
||||
'xiaotanheshu', 'chenxing', 'tangxingyun', 'yaochu', 'zhiqiu'
|
||||
];
|
||||
|
||||
// ─── 状态管理 ───
|
||||
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]
|
||||
},
|
||||
// 最近事件日志
|
||||
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 = saved.indexes || watcherState.indexes;
|
||||
watcherState.events = (saved.events || []).slice(-MAX_LOG_ENTRIES);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[COS-Watcher] 状态文件加载失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化状态到本地文件
|
||||
*/
|
||||
function saveState() {
|
||||
try {
|
||||
const toSave = {
|
||||
indexes: watcherState.indexes,
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// 事件处理逻辑
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* 处理新报告: 铸渊审核 → 写回执
|
||||
* (当前记录事件,审核逻辑可在后续接入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-${Date.now()}.json`;
|
||||
|
||||
const receipt = {
|
||||
receipt_id: `RCPT-${Date.now()}`,
|
||||
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 {
|
||||
// 映射解析失败
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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] = await Promise.all([
|
||||
scanTeamReports(),
|
||||
scanTeamReceipts(),
|
||||
scanColdCorpus()
|
||||
]);
|
||||
|
||||
const totalNew = newReports.length + newReceipts.length + newCorpus.length;
|
||||
|
||||
if (totalNew > 0) {
|
||||
logEvent('changes_detected', {
|
||||
reports: newReports.length,
|
||||
receipts: newReceipts.length,
|
||||
corpus: newCorpus.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);
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
// 启动后立即执行一次扫描
|
||||
setTimeout(() => {
|
||||
runScan().catch(err => {
|
||||
console.error(`[COS-Watcher] 初始扫描异常: ${err.message}`);
|
||||
});
|
||||
}, 5000); // 延迟5秒,等MCP Server完全启动
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止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.0.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
|
||||
},
|
||||
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: []
|
||||
};
|
||||
saveState();
|
||||
logEvent('index_reset', '索引已重置');
|
||||
return { reset: true };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop,
|
||||
getStatus,
|
||||
triggerScan,
|
||||
resetIndex,
|
||||
runScan
|
||||
};
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
* 光之树: growBranch / growLeaf / growBloom / getTreeNode / getSubtree
|
||||
* tracePath / getPersonaBranch / getRecentLeaves
|
||||
* 天眼: writeSyslog / getTianyanView / querySyslog
|
||||
* COS轮询: cosWatcherStatus / cosWatcherTriggerScan / cosWatcherResetIndex
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
|
@ -75,6 +76,9 @@ const notionPermissionOps = require('./tools/notion-permission-ops');
|
|||
const finetuneEngineOps = require('./tools/finetune-engine-ops');
|
||||
// 光之树 + 天眼
|
||||
const lightTreeOps = require('./tools/light-tree-ops');
|
||||
// COS桶轮询守护
|
||||
const cosWatcherOps = require('./tools/cos-watcher-ops');
|
||||
const cosWatcher = require('./cos-watcher');
|
||||
|
||||
// ─── 外部集成模块(优雅降级:未安装依赖时不影响核心功能) ───
|
||||
let notionOps = null;
|
||||
|
|
@ -241,7 +245,11 @@ const TOOLS = {
|
|||
// 天眼 · SYSLOG
|
||||
writeSyslog: lightTreeOps.writeSyslog,
|
||||
getTianyanView: lightTreeOps.getTianyanView,
|
||||
querySyslog: lightTreeOps.querySyslog
|
||||
querySyslog: lightTreeOps.querySyslog,
|
||||
// COS桶轮询守护 · SCF替代
|
||||
cosWatcherStatus: cosWatcherOps.cosWatcherStatus,
|
||||
cosWatcherTriggerScan: cosWatcherOps.cosWatcherTriggerScan,
|
||||
cosWatcherResetIndex: cosWatcherOps.cosWatcherResetIndex
|
||||
};
|
||||
|
||||
// ─── Express 应用 ───
|
||||
|
|
@ -344,6 +352,12 @@ app.get('/health', async (_req, res) => {
|
|||
tools_count: Object.keys(TOOLS).length,
|
||||
database: dbStatus,
|
||||
cos: cosStatus,
|
||||
cos_watcher: {
|
||||
enabled: cosWatcher.getStatus().enabled,
|
||||
last_scan: cosWatcher.getStatus().last_scan,
|
||||
scan_count: cosWatcher.getStatus().scan_count,
|
||||
errors: cosWatcher.getStatus().errors
|
||||
},
|
||||
notion: notionStatus,
|
||||
github: githubStatus
|
||||
});
|
||||
|
|
@ -745,6 +759,38 @@ app.post('/tianyan/syslog', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// ─── COS桶轮询守护API ───
|
||||
|
||||
// 轮询守护状态
|
||||
app.get('/cos-watcher/status', async (_req, res) => {
|
||||
try {
|
||||
const status = cosWatcher.getStatus();
|
||||
res.json(status);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 手动触发扫描
|
||||
app.post('/cos-watcher/scan', async (_req, res) => {
|
||||
try {
|
||||
const result = await cosWatcher.triggerScan();
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 重置索引
|
||||
app.post('/cos-watcher/reset', async (_req, res) => {
|
||||
try {
|
||||
const result = cosWatcher.resetIndex();
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: true, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 人格体数据库引擎状态API(S15) ───
|
||||
|
||||
// 人格体引擎巡检状态
|
||||
|
|
@ -1230,6 +1276,7 @@ function getCategoryForTool(name) {
|
|||
if (['growBranch','growLeaf','growBloom','getTreeNode','getSubtree',
|
||||
'tracePath','getPersonaBranch','getRecentLeaves'].includes(name)) return 'light-tree';
|
||||
if (['writeSyslog','getTianyanView','querySyslog'].includes(name)) return 'tianyan';
|
||||
if (name.startsWith('cosWatcher')) return 'cos-watcher';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
|
|
@ -1260,17 +1307,22 @@ app.listen(PORT, BIND_HOST, () => {
|
|||
console.log(`[MCP] 工具数量: ${Object.keys(TOOLS).length}`);
|
||||
console.log(`[MCP] API Key 鉴权: ${ZHUYUAN_API_KEY ? '已启用' : '未配置(仅内部访问)'}`);
|
||||
console.log(`[MCP] 铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559`);
|
||||
|
||||
// 启动COS桶轮询守护进程(SCF替代方案)
|
||||
cosWatcher.start();
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('[MCP] 收到SIGTERM,关闭数据库连接...');
|
||||
console.log('[MCP] 收到SIGTERM,停止COS轮询 + 关闭数据库连接...');
|
||||
cosWatcher.stop();
|
||||
await db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('[MCP] 收到SIGINT,关闭数据库连接...');
|
||||
console.log('[MCP] 收到SIGINT,停止COS轮询 + 关闭数据库连接...');
|
||||
cosWatcher.stop();
|
||||
await db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* ═══════════════════════════════════════════════════════════
|
||||
* 🔭 COS桶轮询守护 MCP 工具
|
||||
* ═══════════════════════════════════════════════════════════
|
||||
*
|
||||
* 签发: 铸渊 · ICE-GL-ZY001
|
||||
* 版权: 国作登字-2026-A-00037559
|
||||
*
|
||||
* 提供MCP工具接口用于查询和控制COS轮询守护进程。
|
||||
*
|
||||
* 工具清单:
|
||||
* cosWatcherStatus — 查看轮询守护进程状态
|
||||
* cosWatcherTriggerScan — 手动触发一次COS桶扫描
|
||||
* cosWatcherResetIndex — 重置文件索引
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const cosWatcher = require('../cos-watcher');
|
||||
|
||||
/**
|
||||
* cosWatcherStatus — 查看轮询守护进程状态
|
||||
*
|
||||
* input: {} (无参数)
|
||||
*
|
||||
* output:
|
||||
* enabled: boolean — 是否运行中
|
||||
* started_at: string|null — 启动时间
|
||||
* last_scan: string|null — 上次扫描时间
|
||||
* scan_count: number — 总扫描次数
|
||||
* errors: number — 错误次数
|
||||
* last_error: string|null — 最后一次错误信息
|
||||
* is_scanning: boolean — 是否正在扫描
|
||||
* index_summary: object — 索引统计
|
||||
* recent_events: array — 最近事件列表
|
||||
*/
|
||||
async function cosWatcherStatus(_input) {
|
||||
return cosWatcher.getStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* cosWatcherTriggerScan — 手动触发一次COS桶扫描
|
||||
*
|
||||
* 不等待定时任务,立即执行一次完整扫描。
|
||||
*
|
||||
* input: {} (无参数)
|
||||
*
|
||||
* output:
|
||||
* 同 cosWatcherStatus 的输出(扫描后的最新状态)
|
||||
*/
|
||||
async function cosWatcherTriggerScan(_input) {
|
||||
return cosWatcher.triggerScan();
|
||||
}
|
||||
|
||||
/**
|
||||
* cosWatcherResetIndex — 重置文件索引
|
||||
*
|
||||
* 清空所有已知文件索引。下次扫描时会将所有现有文件视为"新文件"。
|
||||
* 谨慎使用:可能导致重复处理已有文件。
|
||||
*
|
||||
* input: {} (无参数)
|
||||
*
|
||||
* output:
|
||||
* reset: boolean — 是否成功重置
|
||||
*/
|
||||
async function cosWatcherResetIndex(_input) {
|
||||
return cosWatcher.resetIndex();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cosWatcherStatus,
|
||||
cosWatcherTriggerScan,
|
||||
cosWatcherResetIndex
|
||||
};
|
||||
Loading…
Reference in New Issue