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:
copilot-swe-agent[bot] 2026-04-10 12:43:16 +00:00 committed by GitHub
parent feca79cde3
commit b487b6bc57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 778 additions and 7 deletions

View File

@ -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 · 语料处理

View File

@ -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",

View File

@ -0,0 +1,9 @@
{
"indexes": {
"team_reports": {},
"team_receipts": {},
"cold_corpus": []
},
"events": [],
"last_save": null
}

View File

@ -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
};

View File

@ -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 });
}
});
// ─── 人格体数据库引擎状态APIS15 ───
// 人格体引擎巡检状态
@ -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);
});

View File

@ -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
};