658 lines
22 KiB
JavaScript
658 lines
22 KiB
JavaScript
#!/usr/bin/env node
|
||
// ═══════════════════════════════════════════════
|
||
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
||
// 📜 Copyright: 国作登字-2026-A-00037559
|
||
// ═══════════════════════════════════════════════
|
||
// server/proxy/service/protocol-mirror.js
|
||
// 🪞 铸渊专线 · 协议镜像引擎
|
||
//
|
||
// 核心概念: 镜像同步
|
||
// - 上游协议 (Xray-core) 更新时自动检测
|
||
// - 自动下载/备份/升级/验证/重启
|
||
// - TLS/Reality 指纹一致性校验
|
||
// - 对外呈现标准 Xray 行为 (伪装)
|
||
//
|
||
// 安全机制:
|
||
// - LLM分析更新风险 (安全补丁/破坏性变更)
|
||
// - 失败3次自动告警管理员
|
||
// - 升级前备份,失败可回滚
|
||
//
|
||
// 运行方式: PM2 managed (zy-protocol-mirror)
|
||
// 检查间隔: 每30分钟 (GitHub API每6小时)
|
||
// ═══════════════════════════════════════════════
|
||
|
||
'use strict';
|
||
|
||
const { execSync, execFileSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const https = require('https');
|
||
|
||
const DATA_DIR = process.env.ZY_PROXY_DATA_DIR || '/opt/zhuyuan-brain/proxy/data';
|
||
const LOG_DIR = process.env.ZY_PROXY_LOG_DIR || '/opt/zhuyuan-brain/proxy/logs';
|
||
const MIRROR_FILE = path.join(DATA_DIR, 'protocol-mirror-status.json');
|
||
const XRAY_BIN = process.env.ZY_XRAY_BIN || '/usr/local/bin/xray';
|
||
const XRAY_BACKUP_DIR = path.join(DATA_DIR, 'xray-backups');
|
||
|
||
const CHECK_INTERVAL = 30 * 60 * 1000; // 30分钟
|
||
const GITHUB_CHECK_INTERVAL = 6 * 60 * 60 * 1000; // 6小时
|
||
const MAX_FAILED_UPDATES = 3;
|
||
const MAX_UPDATE_HISTORY = 20;
|
||
|
||
// ── 读取镜像状态 ─────────────────────────────
|
||
function readMirrorStatus() {
|
||
try {
|
||
return JSON.parse(fs.readFileSync(MIRROR_FILE, 'utf8'));
|
||
} catch {
|
||
return {
|
||
installed_version: null,
|
||
latest_version: null,
|
||
update_available: false,
|
||
last_github_check: null,
|
||
last_fingerprint_check: null,
|
||
update_history: [],
|
||
failed_updates: 0,
|
||
mirror_status: 'initializing',
|
||
fingerprint: {
|
||
tls_match: true,
|
||
reality_match: true,
|
||
protocol_version: 'unknown'
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
// ── 保存镜像状态 ─────────────────────────────
|
||
function saveMirrorStatus(status) {
|
||
if (!fs.existsSync(DATA_DIR)) {
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
}
|
||
fs.writeFileSync(MIRROR_FILE, JSON.stringify(status, null, 2));
|
||
}
|
||
|
||
// ── 执行命令 ─────────────────────────────────
|
||
function runCmd(cmd, timeout = 10000) {
|
||
try {
|
||
return { ok: true, output: execSync(cmd, { encoding: 'utf8', timeout }).trim() };
|
||
} catch (err) {
|
||
return { ok: false, output: err.message };
|
||
}
|
||
}
|
||
|
||
// ── HTTPS GET 请求 ───────────────────────────
|
||
function httpsGet(url, headers = {}) {
|
||
return new Promise((resolve, reject) => {
|
||
const urlObj = new URL(url);
|
||
const options = {
|
||
hostname: urlObj.hostname,
|
||
path: urlObj.pathname + urlObj.search,
|
||
method: 'GET',
|
||
headers: {
|
||
'User-Agent': 'ZY-Protocol-Mirror/1.0',
|
||
...headers
|
||
},
|
||
timeout: 15000
|
||
};
|
||
|
||
const req = https.request(options, (res) => {
|
||
// 处理重定向
|
||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||
httpsGet(res.headers.location, headers).then(resolve).catch(reject);
|
||
return;
|
||
}
|
||
|
||
let data = '';
|
||
res.on('data', (d) => { data += d; });
|
||
res.on('end', () => {
|
||
resolve({ statusCode: res.statusCode, body: data, headers: res.headers });
|
||
});
|
||
});
|
||
|
||
req.on('error', (err) => {
|
||
reject(new Error(`HTTPS请求失败: ${err.message}`));
|
||
});
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
reject(new Error('HTTPS请求超时'));
|
||
});
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// ── 获取已安装的Xray版本 ────────────────────
|
||
function getInstalledVersion() {
|
||
try {
|
||
const output = execFileSync(XRAY_BIN, ['version'], {
|
||
encoding: 'utf8',
|
||
timeout: 5000
|
||
}).trim();
|
||
|
||
// Xray version output: "Xray 1.8.x (Xray, Penetrates Everything.) ..."
|
||
const match = output.match(/Xray\s+(\d+\.\d+\.\d+)/i);
|
||
if (match) {
|
||
return match[1];
|
||
}
|
||
console.error('[协议镜像] 无法解析Xray版本号:', output.slice(0, 100));
|
||
return null;
|
||
} catch (err) {
|
||
console.error('[协议镜像] 获取Xray版本失败:', err.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── 查询GitHub最新版本 ──────────────────────
|
||
async function fetchLatestRelease() {
|
||
try {
|
||
const resp = await httpsGet('https://api.github.com/repos/XTLS/Xray-core/releases/latest', {
|
||
'Accept': 'application/vnd.github.v3+json'
|
||
});
|
||
|
||
if (resp.statusCode !== 200) {
|
||
console.error(`[协议镜像] GitHub API返回 ${resp.statusCode}`);
|
||
return null;
|
||
}
|
||
|
||
const release = JSON.parse(resp.body);
|
||
const tagName = release.tag_name || '';
|
||
// tag格式: "v1.8.x"
|
||
const version = tagName.replace(/^v/, '');
|
||
const body = release.body || '';
|
||
const publishedAt = release.published_at || null;
|
||
|
||
// 查找Linux AMD64二进制下载链接
|
||
let downloadUrl = null;
|
||
if (release.assets && Array.isArray(release.assets)) {
|
||
const linuxAsset = release.assets.find(a =>
|
||
a.name && a.name.includes('linux') && a.name.includes('64') && a.name.endsWith('.zip')
|
||
);
|
||
if (linuxAsset) {
|
||
downloadUrl = linuxAsset.browser_download_url;
|
||
}
|
||
}
|
||
|
||
return { version, body, publishedAt, downloadUrl, tagName };
|
||
} catch (err) {
|
||
console.error('[协议镜像] 获取GitHub release失败:', err.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── 版本比较 ─────────────────────────────────
|
||
function isNewer(latest, installed) {
|
||
if (!latest || !installed) return false;
|
||
const latestParts = latest.split('.').map(Number);
|
||
const installedParts = installed.split('.').map(Number);
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
const l = latestParts[i] || 0;
|
||
const c = installedParts[i] || 0;
|
||
if (l > c) return true;
|
||
if (l < c) return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ── 协议指纹检查 ─────────────────────────────
|
||
function checkFingerprint() {
|
||
console.log('[协议镜像] 检查协议指纹一致性...');
|
||
const fingerprint = {
|
||
tls_match: true,
|
||
reality_match: true,
|
||
protocol_version: 'unknown'
|
||
};
|
||
|
||
// 检查TLS配置是否符合标准Xray行为
|
||
try {
|
||
const configPath = process.env.ZY_XRAY_CONFIG || '/usr/local/etc/xray/config.json';
|
||
if (fs.existsSync(configPath)) {
|
||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||
|
||
// 验证Reality配置存在且结构完整
|
||
const inbounds = config.inbounds || [];
|
||
const realityInbound = inbounds.find(ib =>
|
||
ib.streamSettings &&
|
||
ib.streamSettings.realitySettings
|
||
);
|
||
|
||
if (realityInbound) {
|
||
const reality = realityInbound.streamSettings.realitySettings;
|
||
fingerprint.reality_match = !!(reality.privateKey && reality.shortIds);
|
||
if (!fingerprint.reality_match) {
|
||
console.log('[协议镜像] ⚠️ Reality配置不完整');
|
||
}
|
||
}
|
||
|
||
// 验证TLS相关配置
|
||
const tlsInbound = inbounds.find(ib =>
|
||
ib.streamSettings &&
|
||
ib.streamSettings.security === 'tls'
|
||
);
|
||
|
||
if (tlsInbound) {
|
||
const tls = tlsInbound.streamSettings.tlsSettings || {};
|
||
fingerprint.tls_match = !!(tls.certificates && tls.certificates.length > 0);
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('[协议镜像] 指纹检查异常:', err.message);
|
||
fingerprint.tls_match = false;
|
||
}
|
||
|
||
// 确认Xray进程运行中的协议版本
|
||
const installed = getInstalledVersion();
|
||
if (installed) {
|
||
fingerprint.protocol_version = 'matching';
|
||
} else {
|
||
fingerprint.protocol_version = 'unavailable';
|
||
}
|
||
|
||
return fingerprint;
|
||
}
|
||
|
||
// ── LLM更新风险分析 ─────────────────────────
|
||
async function consultLLMForUpdate(releaseInfo, installedVersion) {
|
||
let llmRouter;
|
||
try {
|
||
llmRouter = require('./llm-router');
|
||
} catch {
|
||
console.log('[协议镜像] LLM路由器未加载,跳过风险分析');
|
||
return null;
|
||
}
|
||
|
||
const prompt = `你是光湖语言世界VPN系统的协议镜像引擎。Xray-core 有新版本发布,请分析更新风险。
|
||
|
||
当前版本: v${installedVersion}
|
||
最新版本: v${releaseInfo.version}
|
||
发布时间: ${releaseInfo.publishedAt || '未知'}
|
||
|
||
更新日志 (前1500字):
|
||
${(releaseInfo.body || '无更新日志').slice(0, 1500)}
|
||
|
||
请回答以下问题 (JSON格式):
|
||
1. is_critical: 是否是安全修复 (true/false)
|
||
2. changes_summary: 主要变更摘要 (一句话)
|
||
3. risk_level: 更新风险等级 (low/medium/high)
|
||
4. recommend_auto_update: 是否推荐自动更新 (true/false)
|
||
5. reason: 推荐理由 (一句话)`;
|
||
|
||
let result;
|
||
try {
|
||
result = await llmRouter.callLLM(prompt, {
|
||
systemPrompt: '你是光湖语言世界VPN系统的协议分析AI。负责评估Xray-core更新的风险和必要性。请用JSON格式回答。',
|
||
maxTokens: 500,
|
||
timeout: 30000
|
||
});
|
||
} catch (err) {
|
||
console.error('[协议镜像] LLM调用异常:', err.message);
|
||
return null;
|
||
}
|
||
|
||
if (!result || !result.content) return null;
|
||
|
||
// 尝试解析LLM返回的JSON
|
||
try {
|
||
const jsonMatch = result.content.match(/\{[\s\S]*\}/);
|
||
if (jsonMatch) {
|
||
return JSON.parse(jsonMatch[0]);
|
||
}
|
||
} catch {
|
||
console.log('[协议镜像] LLM返回内容无法解析为JSON');
|
||
}
|
||
|
||
return { raw_analysis: result.content, model: result.model };
|
||
}
|
||
|
||
// ── 下载文件 ─────────────────────────────────
|
||
function downloadFile(url, destPath) {
|
||
return new Promise((resolve, reject) => {
|
||
const file = fs.createWriteStream(destPath);
|
||
|
||
const cleanup = () => {
|
||
file.close();
|
||
if (fs.existsSync(destPath)) fs.unlinkSync(destPath);
|
||
};
|
||
|
||
const doRequest = (reqUrl) => {
|
||
const urlObj = new URL(reqUrl);
|
||
const options = {
|
||
hostname: urlObj.hostname,
|
||
path: urlObj.pathname + urlObj.search,
|
||
method: 'GET',
|
||
headers: { 'User-Agent': 'ZY-Protocol-Mirror/1.0' },
|
||
timeout: 120000
|
||
};
|
||
|
||
const req = https.request(options, (res) => {
|
||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||
doRequest(res.headers.location);
|
||
return;
|
||
}
|
||
if (res.statusCode !== 200) {
|
||
cleanup();
|
||
reject(new Error(`下载失败: HTTP ${res.statusCode}`));
|
||
return;
|
||
}
|
||
res.pipe(file);
|
||
file.on('finish', () => {
|
||
file.close();
|
||
resolve(destPath);
|
||
});
|
||
});
|
||
|
||
req.on('error', (err) => {
|
||
cleanup();
|
||
reject(new Error(`下载错误: ${err.message}`));
|
||
});
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
cleanup();
|
||
reject(new Error('下载超时'));
|
||
});
|
||
req.end();
|
||
};
|
||
|
||
doRequest(url);
|
||
});
|
||
}
|
||
|
||
// ── 执行更新 ─────────────────────────────────
|
||
async function performUpdate() {
|
||
const status = readMirrorStatus();
|
||
|
||
if (!status.update_available || !status.latest_version) {
|
||
console.log('[协议镜像] 无可用更新');
|
||
return { ok: false, detail: '无可用更新' };
|
||
}
|
||
|
||
if (status.failed_updates >= MAX_FAILED_UPDATES) {
|
||
console.error('[协议镜像] 已连续失败3次,需人工介入');
|
||
sendAlertEmail(
|
||
'🪞 协议镜像更新失败 - 需人工介入',
|
||
`Xray-core 更新已连续失败 ${status.failed_updates} 次。\n` +
|
||
`目标版本: v${status.latest_version}\n` +
|
||
`当前版本: v${status.installed_version}\n` +
|
||
`请手动检查并更新。`
|
||
);
|
||
return { ok: false, detail: `连续失败${status.failed_updates}次,已通知管理员` };
|
||
}
|
||
|
||
console.log(`[协议镜像] 开始更新: v${status.installed_version} → v${status.latest_version}`);
|
||
status.mirror_status = 'updating';
|
||
saveMirrorStatus(status);
|
||
|
||
const updateRecord = {
|
||
from_version: status.installed_version,
|
||
to_version: status.latest_version,
|
||
started_at: new Date().toISOString(),
|
||
completed_at: null,
|
||
success: false,
|
||
detail: ''
|
||
};
|
||
|
||
try {
|
||
// 1. 获取下载链接
|
||
const release = await fetchLatestRelease();
|
||
if (!release || !release.downloadUrl) {
|
||
throw new Error('无法获取下载链接');
|
||
}
|
||
|
||
// 2. 下载新版本
|
||
console.log('[协议镜像] 下载新版本...');
|
||
const downloadPath = path.join(DATA_DIR, `xray-${release.version}.zip`);
|
||
await downloadFile(release.downloadUrl, downloadPath);
|
||
|
||
// 3. 备份当前版本
|
||
console.log('[协议镜像] 备份当前版本...');
|
||
if (!fs.existsSync(XRAY_BACKUP_DIR)) {
|
||
fs.mkdirSync(XRAY_BACKUP_DIR, { recursive: true });
|
||
}
|
||
const backupPath = path.join(XRAY_BACKUP_DIR, `xray-${status.installed_version}-${Date.now()}`);
|
||
if (fs.existsSync(XRAY_BIN)) {
|
||
fs.copyFileSync(XRAY_BIN, backupPath);
|
||
console.log(`[协议镜像] 备份完成: ${backupPath}`);
|
||
}
|
||
|
||
// 4. 解压并安装
|
||
console.log('[协议镜像] 解压并安装...');
|
||
const extractDir = path.join(DATA_DIR, 'xray-extract');
|
||
if (fs.existsSync(extractDir)) {
|
||
execFileSync('rm', ['-rf', extractDir], { timeout: 10000 });
|
||
}
|
||
fs.mkdirSync(extractDir, { recursive: true });
|
||
execFileSync('unzip', ['-o', downloadPath, '-d', extractDir], {
|
||
encoding: 'utf8',
|
||
timeout: 30000
|
||
});
|
||
|
||
const newBin = path.join(extractDir, 'xray');
|
||
if (!fs.existsSync(newBin)) {
|
||
throw new Error('解压后未找到xray二进制文件');
|
||
}
|
||
|
||
// 设置执行权限并安装
|
||
fs.chmodSync(newBin, 0o755);
|
||
execFileSync('cp', [newBin, XRAY_BIN], { timeout: 10000 });
|
||
|
||
// 5. 验证安装
|
||
console.log('[协议镜像] 验证安装...');
|
||
const newVersion = getInstalledVersion();
|
||
if (!newVersion) {
|
||
throw new Error('安装后无法获取版本号');
|
||
}
|
||
console.log(`[协议镜像] 新版本已安装: v${newVersion}`);
|
||
|
||
// 6. 重启Xray服务
|
||
console.log('[协议镜像] 重启Xray服务...');
|
||
try {
|
||
execFileSync('systemctl', ['restart', 'xray'], { encoding: 'utf8', timeout: 30000 });
|
||
} catch (err) {
|
||
throw new Error(`重启失败: ${err.message}`);
|
||
}
|
||
|
||
// 等待2秒后验证进程
|
||
await new Promise(r => setTimeout(r, 2000));
|
||
try {
|
||
execFileSync('pgrep', ['-x', 'xray'], { encoding: 'utf8', timeout: 5000 });
|
||
} catch {
|
||
throw new Error('Xray重启后进程未运行');
|
||
}
|
||
|
||
// 7. 清理下载文件
|
||
if (fs.existsSync(downloadPath)) fs.unlinkSync(downloadPath);
|
||
if (fs.existsSync(extractDir)) {
|
||
execFileSync('rm', ['-rf', extractDir], { timeout: 10000 });
|
||
}
|
||
|
||
// 更新成功
|
||
updateRecord.success = true;
|
||
updateRecord.completed_at = new Date().toISOString();
|
||
updateRecord.detail = `成功从 v${status.installed_version} 更新到 v${newVersion}`;
|
||
|
||
status.installed_version = newVersion;
|
||
status.update_available = false;
|
||
status.failed_updates = 0;
|
||
status.mirror_status = 'synced';
|
||
status.update_history.push(updateRecord);
|
||
|
||
if (status.update_history.length > MAX_UPDATE_HISTORY) {
|
||
status.update_history = status.update_history.slice(-MAX_UPDATE_HISTORY);
|
||
}
|
||
|
||
saveMirrorStatus(status);
|
||
console.log(`[协议镜像] ✅ 更新完成: v${newVersion}`);
|
||
|
||
// 发送更新成功通知
|
||
sendAlertEmail(
|
||
`🪞 Xray-core 已更新至 v${newVersion}`,
|
||
getUpdateSummary()
|
||
);
|
||
|
||
return { ok: true, detail: updateRecord.detail };
|
||
} catch (err) {
|
||
console.error('[协议镜像] ❌ 更新失败:', err.message);
|
||
|
||
updateRecord.completed_at = new Date().toISOString();
|
||
updateRecord.detail = `更新失败: ${err.message}`;
|
||
|
||
status.failed_updates++;
|
||
status.mirror_status = 'error';
|
||
status.update_history.push(updateRecord);
|
||
|
||
if (status.update_history.length > MAX_UPDATE_HISTORY) {
|
||
status.update_history = status.update_history.slice(-MAX_UPDATE_HISTORY);
|
||
}
|
||
|
||
saveMirrorStatus(status);
|
||
|
||
// 连续失败达到上限时告警
|
||
if (status.failed_updates >= MAX_FAILED_UPDATES) {
|
||
sendAlertEmail(
|
||
'🪞 协议镜像更新失败 - 需人工介入',
|
||
`Xray-core 更新已连续失败 ${status.failed_updates} 次。\n` +
|
||
`目标版本: v${status.latest_version}\n` +
|
||
`当前版本: v${status.installed_version}\n` +
|
||
`最后错误: ${err.message}\n` +
|
||
`请手动检查并更新。`
|
||
);
|
||
}
|
||
|
||
return { ok: false, detail: `更新失败: ${err.message}` };
|
||
}
|
||
}
|
||
|
||
// ── 发送告警邮件 ─────────────────────────────
|
||
function sendAlertEmail(subject, body) {
|
||
try {
|
||
const sendScript = path.join(__dirname, 'send-subscription.js');
|
||
execFileSync('node', [sendScript, 'alert', `${subject}\n\n${body}`], {
|
||
encoding: 'utf8',
|
||
timeout: 30000
|
||
});
|
||
console.log('[协议镜像] 通知邮件已发送');
|
||
} catch (err) {
|
||
console.error('[协议镜像] 邮件发送失败:', err.message);
|
||
}
|
||
}
|
||
|
||
// ── 获取更新摘要 ─────────────────────────────
|
||
function getUpdateSummary() {
|
||
const status = readMirrorStatus();
|
||
const lastUpdate = status.update_history.length > 0
|
||
? status.update_history[status.update_history.length - 1]
|
||
: null;
|
||
|
||
const lines = [
|
||
'🪞 协议镜像引擎 · 状态报告',
|
||
'─'.repeat(30),
|
||
`当前版本: v${status.installed_version || '未知'}`,
|
||
`最新版本: v${status.latest_version || '未知'}`,
|
||
`镜像状态: ${status.mirror_status}`,
|
||
`更新可用: ${status.update_available ? '是' : '否'}`,
|
||
`失败次数: ${status.failed_updates}`,
|
||
''
|
||
];
|
||
|
||
if (lastUpdate) {
|
||
lines.push('最近更新:');
|
||
lines.push(` ${lastUpdate.from_version} → ${lastUpdate.to_version}`);
|
||
lines.push(` 时间: ${lastUpdate.completed_at}`);
|
||
lines.push(` 结果: ${lastUpdate.success ? '成功' : '失败'}`);
|
||
lines.push(` 详情: ${lastUpdate.detail}`);
|
||
}
|
||
|
||
lines.push('');
|
||
lines.push('指纹检查:');
|
||
lines.push(` TLS匹配: ${status.fingerprint.tls_match ? '✅' : '❌'}`);
|
||
lines.push(` Reality匹配: ${status.fingerprint.reality_match ? '✅' : '❌'}`);
|
||
lines.push(` 协议版本: ${status.fingerprint.protocol_version}`);
|
||
|
||
return lines.join('\n');
|
||
}
|
||
|
||
// ── 获取镜像状态 ─────────────────────────────
|
||
function getMirrorStatus() {
|
||
return readMirrorStatus();
|
||
}
|
||
|
||
// ── 主检查流程 ───────────────────────────────
|
||
async function checkForUpdates() {
|
||
console.log('[协议镜像] 开始检查...');
|
||
const status = readMirrorStatus();
|
||
|
||
// 1. 获取已安装版本
|
||
const installedVersion = getInstalledVersion();
|
||
if (installedVersion) {
|
||
status.installed_version = installedVersion;
|
||
}
|
||
|
||
// 2. 指纹检查 (每次都执行)
|
||
status.fingerprint = checkFingerprint();
|
||
status.last_fingerprint_check = new Date().toISOString();
|
||
|
||
if (!status.fingerprint.tls_match || !status.fingerprint.reality_match) {
|
||
console.log('[协议镜像] ⚠️ 指纹不一致,可能被探测');
|
||
}
|
||
|
||
// 3. 检查GitHub最新版本 (受频率限制)
|
||
const now = Date.now();
|
||
const lastCheck = status.last_github_check ? new Date(status.last_github_check).getTime() : 0;
|
||
const shouldCheckGitHub = (now - lastCheck) >= GITHUB_CHECK_INTERVAL;
|
||
|
||
if (shouldCheckGitHub) {
|
||
console.log('[协议镜像] 查询GitHub最新版本...');
|
||
const release = await fetchLatestRelease();
|
||
|
||
if (release && release.version) {
|
||
status.latest_version = release.version;
|
||
status.last_github_check = new Date().toISOString();
|
||
|
||
if (installedVersion && isNewer(release.version, installedVersion)) {
|
||
console.log(`[协议镜像] 🆕 发现新版本: v${release.version} (当前: v${installedVersion})`);
|
||
status.update_available = true;
|
||
status.mirror_status = 'outdated';
|
||
|
||
// LLM风险分析
|
||
const analysis = await consultLLMForUpdate(release, installedVersion);
|
||
if (analysis) {
|
||
console.log('[协议镜像] LLM分析结果:', JSON.stringify(analysis).slice(0, 200));
|
||
}
|
||
} else {
|
||
status.update_available = false;
|
||
if (status.mirror_status !== 'error') {
|
||
status.mirror_status = 'synced';
|
||
}
|
||
console.log(`[协议镜像] ✅ 版本已同步: v${installedVersion || '未知'}`);
|
||
}
|
||
} else {
|
||
console.log('[协议镜像] GitHub查询跳过 (API不可达或已限流)');
|
||
}
|
||
} else {
|
||
const nextCheckMin = Math.round((GITHUB_CHECK_INTERVAL - (now - lastCheck)) / 60000);
|
||
console.log(`[协议镜像] 使用缓存的GitHub结果 (下次查询: ${nextCheckMin}分钟后)`);
|
||
}
|
||
|
||
saveMirrorStatus(status);
|
||
|
||
// 打印摘要
|
||
console.log(`[协议镜像] 检查完成 | 版本: v${status.installed_version || '?'} | 最新: v${status.latest_version || '?'} | 状态: ${status.mirror_status}`);
|
||
|
||
return status;
|
||
}
|
||
|
||
// ── 模块导出 ─────────────────────────────────
|
||
module.exports = { checkForUpdates, performUpdate, getMirrorStatus, getUpdateSummary };
|
||
|
||
// ── 启动协议镜像引擎 ────────────────────────
|
||
console.log('🪞 光湖语言世界 · 协议镜像引擎启动');
|
||
console.log(` 检查间隔: ${CHECK_INTERVAL / 1000}秒`);
|
||
console.log(` GitHub API间隔: ${GITHUB_CHECK_INTERVAL / 3600000}小时`);
|
||
console.log(` LLM路由器: 动态多模型 (通过llm-router.js)`);
|
||
|
||
// 立即执行一次
|
||
checkForUpdates().catch(console.error);
|
||
|
||
// 定期执行
|
||
setInterval(() => {
|
||
checkForUpdates().catch(console.error);
|
||
}, CHECK_INTERVAL);
|