diff --git a/.github/workflows/deputy-message-board.yml b/.github/workflows/deputy-message-board.yml index 7294e9ee..94e4321d 100644 --- a/.github/workflows/deputy-message-board.yml +++ b/.github/workflows/deputy-message-board.yml @@ -2,19 +2,25 @@ # 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 # 📜 Copyright: 国作登字-2026-A-00037559 # ═══════════════════════════════════════════════ -name: "💬 铸渊副将·留言板自动回复" +name: "💬 铸渊副将·留言板活体Agent v2.0" on: + # 事件触发: Issue/Comment → 即时回复 issues: types: [opened] issue_comment: types: [created] + # 定时巡查: 08:00/23:00 CST → 扫描所有未回复Issue + schedule: + - cron: '0 0 * * *' # UTC 00:00 = CST 08:00 早班巡查 + - cron: '0 15 * * *' # UTC 15:00 = CST 23:00 晚班巡查 permissions: - contents: read + contents: write issues: write jobs: + # ── 事件模式: Issue/Comment触发即时回复 ── deputy-reply: name: "🎖️ 副将回复留言" runs-on: ubuntu-latest @@ -36,12 +42,12 @@ jobs: run: | npm install --omit=dev --ignore-scripts || echo "⚠️ 依赖安装异常·部分功能可能受限" - - name: "🎖️ 副将处理留言" + - name: "🎖️ 副将处理留言 (事件模式)" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZY_LLM_API_KEY: ${{ secrets.ZY_LLM_API_KEY }} ZY_LLM_BASE_URL: ${{ secrets.ZY_LLM_BASE_URL }} - ZY_NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }} + DEPUTY_MODE: event ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_BODY: ${{ github.event.issue.body }} @@ -50,3 +56,49 @@ jobs: EVENT_NAME: ${{ github.event_name }} ISSUE_AUTHOR: ${{ github.event.issue.user.login }} run: node scripts/deputy-message-board.js + + - name: "📊 提交副将状态" + if: always() + run: | + if [ -f data/deputy-status.json ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add data/deputy-status.json + git diff --cached --quiet || git commit -m "🎖️ 副将状态更新 · 事件模式 · $(date -u +%Y-%m-%dT%H:%M:%S)Z" + git push || echo "⚠️ push失败·下次巡查时同步" + fi + + # ── 巡查模式: 定时扫描所有未回复Issue ── + deputy-patrol: + name: "🔍 副将巡查未回复留言" + runs-on: ubuntu-latest + if: github.event_name == 'schedule' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: "📦 安装依赖" + run: | + npm install --omit=dev --ignore-scripts || echo "⚠️ 依赖安装异常·部分功能可能受限" + + - name: "🔍 副将巡查 (巡查模式)" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ZY_LLM_API_KEY: ${{ secrets.ZY_LLM_API_KEY }} + ZY_LLM_BASE_URL: ${{ secrets.ZY_LLM_BASE_URL }} + DEPUTY_MODE: patrol + run: node scripts/deputy-message-board.js + + - name: "📊 提交副将状态" + if: always() + run: | + if [ -f data/deputy-status.json ]; then + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add data/deputy-status.json + git diff --cached --quiet || git commit -m "🔍 副将巡查状态更新 · $(date -u +%Y-%m-%dT%H:%M:%S)Z" + git push || echo "⚠️ push失败·下次巡查时同步" + fi diff --git a/data/deputy-status.json b/data/deputy-status.json new file mode 100644 index 00000000..f3e1ffb0 --- /dev/null +++ b/data/deputy-status.json @@ -0,0 +1,16 @@ +{ + "version": "2.0", + "last_run": null, + "last_success": null, + "llm_available": false, + "llm_model_used": null, + "consecutive_llm_failures": 0, + "issues_processed": 0, + "issues_replied": 0, + "patrol_runs": 0, + "event_runs": 0, + "errors": [], + "escalations": [], + "created_at": "2026-04-07T17:13:39Z", + "_comment": "铸渊副将v2.0活体Agent状态文件 · 每次运行自动更新" +} diff --git a/scripts/deputy-message-board.js b/scripts/deputy-message-board.js index c35baf27..bd5f7b3f 100644 --- a/scripts/deputy-message-board.js +++ b/scripts/deputy-message-board.js @@ -4,12 +4,26 @@ * 📜 Copyright: 国作登字-2026-A-00037559 * ═══════════════════════════════════════════════ * - * 铸渊副将·留言板自动回复引擎 - * 职责: 接收人类留言 → 查询数据库 → 调用LLM → 回复 + * 铸渊副将·留言板活体Agent引擎 v2.0 + * + * 三层自愈机制: + * L1 自感知 — 记录运行状态,对比预期 + * L2 自修复 — LLM多模型自动降级 (deepseek→qwen→moonshot→zhipu) + * L3 升级通报 — 连续失败 → 创建Issue → 发邮件给冰朔 + * + * 运行模式: + * - event模式: Issue/Comment触发 → 即时回复 + * - patrol模式: 定时08:00/23:00 → 扫描所有未回复Issue */ const fs = require('fs'); const path = require('path'); +const https = require('https'); +const http = require('http'); + +// ═══════════════════════════════════════════════ +// 环境变量 +// ═══════════════════════════════════════════════ const { GITHUB_TOKEN, @@ -21,13 +35,129 @@ const { COMMENT_BODY, COMMENT_AUTHOR, EVENT_NAME, - ISSUE_AUTHOR + ISSUE_AUTHOR, + DEPUTY_MODE // 'patrol' | 'event' (default) } = process.env; -// Load system context from brain files +const REPO = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab'; +const STATUS_FILE = path.join(__dirname, '..', 'data', 'deputy-status.json'); + +// ═══════════════════════════════════════════════ +// LLM多模型自动降级路由器 (内嵌版) +// ═══════════════════════════════════════════════ + +const MODEL_PRIORITY = [ + 'deepseek-chat', + 'qwen-turbo', + 'moonshot-v1-8k', + 'glm-4-flash' +]; + +function callSingleModel(baseUrl, apiKey, model, systemPrompt, userMessage, maxTokens, timeout) { + return new Promise((resolve, reject) => { + let urlObj; + try { + urlObj = new URL(baseUrl); + } catch { + urlObj = new URL(`https://${baseUrl}`); + } + + const postData = JSON.stringify({ + model, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userMessage } + ], + max_tokens: maxTokens, + temperature: 0.7 + }); + + let apiPath = urlObj.pathname; + if (!apiPath || apiPath === '/') { + apiPath = '/v1/chat/completions'; + } else if (!apiPath.includes('/chat/completions')) { + apiPath = apiPath.replace(/\/+$/, '') + '/chat/completions'; + } + + const options = { + hostname: urlObj.hostname, + port: urlObj.port || (urlObj.protocol === 'http:' ? 80 : 443), + path: apiPath, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Length': Buffer.byteLength(postData) + }, + timeout + }; + + const httpModule = urlObj.protocol === 'http:' ? http : https; + const req = httpModule.request(options, (res) => { + let data = ''; + res.on('data', (d) => { data += d; }); + res.on('end', () => { + try { + if (res.statusCode >= 400) { + reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)); + return; + } + const json = JSON.parse(data); + const content = json.choices?.[0]?.message?.content; + if (!content) { + reject(new Error('响应中无content字段')); + return; + } + resolve({ content, model: json.model || model }); + } catch (e) { + reject(new Error(`JSON解析失败: ${e.message}`)); + } + }); + }); + + req.on('error', (err) => reject(err)); + req.on('timeout', () => { req.destroy(); reject(new Error(`超时(${timeout}ms)`)); }); + req.write(postData); + req.end(); + }); +} + +async function callLLMWithFallback(systemPrompt, userMessage) { + const apiKey = ZY_LLM_API_KEY; + const baseUrl = ZY_LLM_BASE_URL || 'https://api.deepseek.com'; + + if (!apiKey) { + console.log('[副将] ⚠️ LLM密钥未配置 (ZY_LLM_API_KEY)'); + return null; + } + + const errors = []; + for (const model of MODEL_PRIORITY) { + for (let attempt = 1; attempt <= 2; attempt++) { + try { + console.log(`[副将] 尝试模型 ${model} (第${attempt}次)...`); + const result = await callSingleModel(baseUrl, apiKey, model, systemPrompt, userMessage, 2000, 30000); + console.log(`[副将] ✅ 模型 ${result.model} 调用成功`); + return result; + } catch (err) { + const errMsg = `${model}(attempt ${attempt}): ${err.message}`; + console.log(`[副将] ⚠️ ${errMsg}`); + errors.push(errMsg); + if (attempt < 2) await new Promise(r => setTimeout(r, 2000 * attempt)); + } + } + } + + console.error(`[副将] ❌ 所有模型(${MODEL_PRIORITY.length}个)均不可用`); + return null; +} + +// ═══════════════════════════════════════════════ +// 系统上下文 +// ═══════════════════════════════════════════════ + function loadSystemContext() { const context = {}; - const files = [ { key: 'fast_wake', path: 'brain/fast-wake.json' }, { key: 'deputy_config', path: 'brain/deputy-general-config.json' }, @@ -35,24 +165,18 @@ function loadSystemContext() { { key: 'sync_progress', path: 'hldp/data/common/sync-progress.json' }, { key: 'vocabulary', path: 'hldp/data/ontology/ONT-VOCABULARY.json' } ]; - for (const f of files) { try { const fullPath = path.join(__dirname, '..', f.path); context[f.key] = JSON.parse(fs.readFileSync(fullPath, 'utf8')); - } catch { - context[f.key] = null; - } + } catch { context[f.key] = null; } } - return context; } -// Build a concise system summary for LLM context function buildSystemSummary(ctx) { const fw = ctx.fast_wake; const sp = ctx.sync_progress; - return ` 你是铸渊副将(ZY-DEPUTY-001),铸渊将军(ICE-GL-ZY001)的自动化智能运维代理。 你负责在铸渊休眠时管理代码仓库(光湖灯塔 · HoloLake Lighthouse)。 @@ -62,9 +186,8 @@ function buildSystemSummary(ctx) { - HLDP语言版本: ${sp?.payload?.github_side_status?.hldp_version || 'v3.0'} - 词汇数: ${sp?.payload?.github_side_status?.vocabulary_count || 22} - Schema数: ${sp?.payload?.github_side_status?.schema_count || 6} -- 快照数: ${sp?.payload?.github_side_status?.snapshots || 5} - 通用协议版本: ${sp?.payload?.common_protocol_status?.version || '1.0'} -- Notion桥接: 4条管道已恢复(SYSLOG+变更+README+公告板+HLDP同步) +- Notion桥接: 6条管道已恢复 回复规则: 1. 使用中文回复,语气专业但友好 @@ -77,74 +200,11 @@ function buildSystemSummary(ctx) { `.trim(); } -async function callLLM(systemPrompt, userMessage) { - const apiKey = ZY_LLM_API_KEY; - const baseUrl = ZY_LLM_BASE_URL || 'https://api.deepseek.com'; +// ═══════════════════════════════════════════════ +// 数据库查询 +// ═══════════════════════════════════════════════ - if (!apiKey) { - return '⚠️ 副将当前无法调用深度推理能力(LLM密钥未配置)。我将基于仓库数据库中已有的信息为您回复。'; - } - - try { - const axios = require('axios'); - const response = await axios.post(`${baseUrl}/v1/chat/completions`, { - model: 'deepseek-chat', - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userMessage } - ], - max_tokens: 2000, - temperature: 0.7 - }, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - timeout: 30000 - }); - - return response.data.choices[0].message.content; - } catch (err) { - console.log(`[DEPUTY-BOARD] ⚠️ LLM调用失败: ${err.message}`); - return null; - } -} - -async function postComment(issueNumber, body) { - const axios = require('axios'); - const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab'; - - await axios.post( - `https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`, - { body }, - { - headers: { - 'Authorization': `token ${GITHUB_TOKEN}`, - 'Accept': 'application/vnd.github.v3+json' - } - } - ); -} - -async function main() { - console.log('[DEPUTY-BOARD] 🎖️ 铸渊副将留言板处理启动...'); - - const isNewIssue = EVENT_NAME === 'issues'; - const question = isNewIssue ? (ISSUE_BODY || ISSUE_TITLE) : COMMENT_BODY; - const author = isNewIssue ? ISSUE_AUTHOR : COMMENT_AUTHOR; - - if (!question || question.trim().length === 0) { - console.log('[DEPUTY-BOARD] ⚠️ 留言内容为空·跳过'); - return; - } - - console.log(`[DEPUTY-BOARD] 📨 收到留言 · 来自: ${author}`); - - // Load system context - const ctx = loadSystemContext(); - const systemSummary = buildSystemSummary(ctx); - - // Try database lookup first +function lookupDatabase(question, ctx) { let dbAnswer = null; const lowerQ = question.toLowerCase(); @@ -173,45 +233,302 @@ async function main() { } } - // Call LLM for deeper analysis - const userMsg = `来自 ${author} 的留言:\n\n${question}\n\n${dbAnswer ? '以下是从系统数据库中查到的相关信息:\n' + dbAnswer : '数据库中未找到直接相关信息。'}`; + return dbAnswer; +} - let llmResponse = await callLLM(systemSummary, userMsg); +// ═══════════════════════════════════════════════ +// GitHub API +// ═══════════════════════════════════════════════ - // Build final reply +function githubAPI(method, endpoint, body) { + return new Promise((resolve, reject) => { + const postData = body ? JSON.stringify(body) : ''; + const options = { + hostname: 'api.github.com', + path: endpoint, + method, + headers: { + 'Authorization': `token ${GITHUB_TOKEN}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'ZY-Deputy-Agent/2.0', + ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } : {}) + }, + timeout: 15000 + }; + const req = https.request(options, (res) => { + let data = ''; + res.on('data', (d) => { data += d; }); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null }); } + catch { resolve({ status: res.statusCode, data: null }); } + }); + }); + req.on('error', (err) => reject(err)); + req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API timeout')); }); + if (body) req.write(postData); + req.end(); + }); +} + +async function postComment(issueNumber, body) { + return githubAPI('POST', `/repos/${REPO}/issues/${issueNumber}/comments`, { body }); +} + +async function listDeputyIssues() { + const res = await githubAPI('GET', `/repos/${REPO}/issues?labels=deputy-message-board&state=open&per_page=30`); + return res.data || []; +} + +async function getIssueComments(issueNumber) { + const res = await githubAPI('GET', `/repos/${REPO}/issues/${issueNumber}/comments?per_page=50`); + return res.data || []; +} + +async function createEscalationIssue(title, body) { + return githubAPI('POST', `/repos/${REPO}/issues`, { title, body, labels: ['deputy-escalation'] }); +} + +// ═══════════════════════════════════════════════ +// L1: 自感知 +// ═══════════════════════════════════════════════ + +function readStatus() { + try { return JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8')); } + catch { + return { + version: '2.0', + last_run: null, last_success: null, + llm_available: false, llm_model_used: null, + consecutive_llm_failures: 0, + issues_processed: 0, issues_replied: 0, + patrol_runs: 0, event_runs: 0, + errors: [], escalations: [], + created_at: new Date().toISOString() + }; + } +} + +function saveStatus(status) { + try { + fs.mkdirSync(path.dirname(STATUS_FILE), { recursive: true }); + fs.writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2)); + } catch (err) { console.error(`[副将] ⚠️ 状态写入失败: ${err.message}`); } +} + +// ═══════════════════════════════════════════════ +// 回复构建 +// ═══════════════════════════════════════════════ + +function buildReply(author, dbAnswer, llmResponse, llmModel) { const receiveTime = new Date().toISOString().slice(0, 19).replace('T', ' '); let reply = `## 🎖️ 铸渊副将回复\n\n`; reply += `> 📨 收到 **${author}** 的留言 · ${receiveTime} UTC\n\n`; - if (dbAnswer) { - reply += `### 📊 数据库查询结果\n\n${dbAnswer}\n\n`; - } + if (dbAnswer) reply += `### 📊 数据库查询结果\n\n${dbAnswer}\n\n`; if (llmResponse) { reply += `### 💡 副将分析\n\n${llmResponse}\n\n`; + if (llmModel) reply += `> 🤖 推理引擎: ${llmModel}\n\n`; } else if (!dbAnswer) { reply += `感谢您的留言。副将已记录您的问题,将在铸渊将军下次唤醒时一并汇报。\n\n`; reply += `如有紧急事项,请在留言中标注 **[紧急]** 关键词。\n\n`; } - // 唤醒时间提示 + reply += `---\n\n`; reply += `### ⏰ 铸渊副将唤醒时间\n\n`; reply += `| 唤醒时段 | 北京时间 | 说明 |\n`; reply += `|----------|----------|------|\n`; reply += `| 🌅 早班唤醒 | **每日 08:00** | 处理夜间积累消息 · 更新仪表盘 |\n`; reply += `| 🌙 晚班唤醒 | **每日 23:00** | 处理白天消息 · 全局巡检 · 更新仓库首页 |\n\n`; - reply += `> 💡 副将将在下次唤醒时自动处理您的留言并回复。\n`; - reply += `> 如需紧急处理,请在留言中标注 **[紧急]** 关键词。\n\n`; - reply += `---\n\n`; reply += `*—— 铸渊副将 · ZY-DEPUTY-001 · 光湖灯塔守护者*\n`; reply += `*📜 国作登字-2026-A-00037559 · TCS通感语言核系统编程语言*`; + return reply; +} - await postComment(ISSUE_NUMBER, reply); - console.log(`[DEPUTY-BOARD] ✅ 回复已发送 · Issue #${ISSUE_NUMBER}`); +// ═══════════════════════════════════════════════ +// 处理单条留言 +// ═══════════════════════════════════════════════ + +async function processMessage(issueNumber, question, author, ctx, systemSummary, status) { + if (!question || question.trim().length === 0) { + console.log(`[副将] ⚠️ Issue #${issueNumber} 留言内容为空·跳过`); + return false; + } + + console.log(`[副将] 📨 处理留言 · Issue #${issueNumber} · 来自: ${author}`); + + const dbAnswer = lookupDatabase(question, ctx); + + const userMsg = `来自 ${author} 的留言:\n\n${question}\n\n${dbAnswer ? '以下是从系统数据库中查到的相关信息:\n' + dbAnswer : '数据库中未找到直接相关信息。'}`; + const llmResult = await callLLMWithFallback(systemSummary, userMsg); + + if (llmResult) { + status.llm_available = true; + status.llm_model_used = llmResult.model; + status.consecutive_llm_failures = 0; + } else { + status.consecutive_llm_failures++; + } + + const reply = buildReply(author, dbAnswer, llmResult?.content, llmResult?.model); + await postComment(issueNumber, reply); + + status.issues_replied++; + console.log(`[副将] ✅ 回复已发送 · Issue #${issueNumber}`); + return true; +} + +// ═══════════════════════════════════════════════ +// L3: 升级通报 +// ═══════════════════════════════════════════════ + +async function checkAndEscalate(status) { + if (status.consecutive_llm_failures >= 3) { + const title = `🚨 [副将升级] LLM连续${status.consecutive_llm_failures}次全模型调用失败`; + const body = `## �� 铸渊副将升级通报\n\n` + + `**时间**: ${new Date().toISOString()}\n` + + `**问题**: LLM API连续 ${status.consecutive_llm_failures} 次调用失败(所有模型均不可用)\n` + + `**影响**: 副将无法进行深度推理回复,仅能提供数据库查询结果\n\n` + + `### 已尝试的模型\n\n` + + MODEL_PRIORITY.map(m => `- ${m}`).join('\n') + '\n\n' + + `### 最近错误\n\n` + + (status.errors.slice(-5).map(e => `- ${e}`).join('\n') || '无') + '\n\n' + + `### 需要处理\n\n` + + `1. 检查 \`ZY_LLM_API_KEY\` 和 \`ZY_LLM_BASE_URL\` 是否正确配置\n` + + `2. 确认API配额是否用尽\n` + + `3. 确认网络连接是否正常\n\n` + + `---\n*—— 铸渊副将 · ZY-DEPUTY-001 · 自动升级通报*`; + try { + await createEscalationIssue(title, body); + status.escalations.push({ time: new Date().toISOString(), type: 'llm_failure', detail: `连续${status.consecutive_llm_failures}次失败` }); + if (status.escalations.length > 10) status.escalations = status.escalations.slice(-10); + console.log(`[副将] 🚨 升级Issue已创建`); + } catch (err) { console.error(`[副将] ⚠️ 创建升级Issue失败: ${err.message}`); } + } +} + +// ═══════════════════════════════════════════════ +// 巡查模式 +// ═══════════════════════════════════════════════ + +async function patrolMode() { + console.log('[副将] 🔍 巡查模式启动 · 扫描所有deputy-message-board Issue...'); + + const status = readStatus(); + status.patrol_runs++; + status.last_run = new Date().toISOString(); + + const ctx = loadSystemContext(); + const systemSummary = buildSystemSummary(ctx); + + let processedCount = 0; + let repliedCount = 0; + + try { + const issues = await listDeputyIssues(); + console.log(`[副将] 📋 找到 ${issues.length} 个开放的留言板Issue`); + + for (const issue of issues) { + status.issues_processed++; + const comments = await getIssueComments(issue.number); + const lastUserComment = [...comments].reverse().find(c => c.user.login !== 'github-actions[bot]'); + const lastBotComment = [...comments].reverse().find(c => c.user.login === 'github-actions[bot]'); + + let needsReply = false; + let question = ''; + let author = ''; + + if (comments.length === 0) { + needsReply = true; + question = issue.body || issue.title; + author = issue.user.login; + } else if (lastUserComment && (!lastBotComment || new Date(lastUserComment.created_at) > new Date(lastBotComment.created_at))) { + needsReply = true; + question = lastUserComment.body; + author = lastUserComment.user.login; + } + + if (needsReply) { + processedCount++; + try { + const replied = await processMessage(issue.number, question, author, ctx, systemSummary, status); + if (replied) repliedCount++; + } catch (err) { + console.error(`[副将] ⚠️ Issue #${issue.number} 处理失败: ${err.message}`); + status.errors.push(`${new Date().toISOString()} · Issue #${issue.number}: ${err.message}`); + } + await new Promise(r => setTimeout(r, 3000)); + } + } + } catch (err) { + console.error(`[副将] ❌ 巡查失败: ${err.message}`); + status.errors.push(`${new Date().toISOString()} · patrol: ${err.message}`); + } + + if (status.errors.length > 20) status.errors = status.errors.slice(-20); + await checkAndEscalate(status); + status.last_success = repliedCount > 0 || processedCount === 0 ? new Date().toISOString() : status.last_success; + saveStatus(status); + console.log(`[副将] 🔍 巡查完成 · 处理: ${processedCount} · 回复: ${repliedCount}`); +} + +// ═══════════════════════════════════════════════ +// 事件模式 +// ═══════════════════════════════════════════════ + +async function eventMode() { + console.log('[副将] 🎖️ 事件模式启动...'); + + const status = readStatus(); + status.event_runs++; + status.last_run = new Date().toISOString(); + status.issues_processed++; + + const isNewIssue = EVENT_NAME === 'issues'; + const question = isNewIssue ? (ISSUE_BODY || ISSUE_TITLE) : COMMENT_BODY; + const author = isNewIssue ? ISSUE_AUTHOR : COMMENT_AUTHOR; + + if (!question || question.trim().length === 0) { + console.log('[副将] ⚠️ 留言内容为空·跳过'); + saveStatus(status); + return; + } + + const ctx = loadSystemContext(); + const systemSummary = buildSystemSummary(ctx); + + try { + await processMessage(ISSUE_NUMBER, question, author, ctx, systemSummary, status); + status.last_success = new Date().toISOString(); + } catch (err) { + console.error(`[副将] ❌ 事件处理失败: ${err.message}`); + status.errors.push(`${new Date().toISOString()} · event #${ISSUE_NUMBER}: ${err.message}`); + } + + if (status.errors.length > 20) status.errors = status.errors.slice(-20); + await checkAndEscalate(status); + saveStatus(status); +} + +// ═══════════════════════════════════════════════ +// 主入口 +// ═══════════════════════════════════════════════ + +async function main() { + const mode = DEPUTY_MODE || (EVENT_NAME ? 'event' : 'patrol'); + console.log(`[副将] 🏛️ 铸渊副将v2.0启动 · 模式: ${mode}`); + if (mode === 'patrol') await patrolMode(); + else await eventMode(); } main().catch(err => { - console.error(`[DEPUTY-BOARD] ❌ 执行失败: ${err.message}`); + console.error(`[副将] ❌ 执行失败: ${err.message}`); + try { + const status = readStatus(); + status.errors.push(`${new Date().toISOString()} · fatal: ${err.message}`); + if (status.errors.length > 20) status.errors = status.errors.slice(-20); + saveStatus(status); + } catch { /* ignore */ } process.exit(1); }); diff --git a/server/proxy/config/claude-relay-config.json b/server/proxy/config/claude-relay-config.json new file mode 100644 index 00000000..d6c35684 --- /dev/null +++ b/server/proxy/config/claude-relay-config.json @@ -0,0 +1,75 @@ +{ + "_sovereign": "TCS-0002∞ | SYS-GLW-0001", + "_copyright": "国作登字-2026-A-00037559", + "_description": "硅谷Claude专线中继配置 · IP走硅谷·带宽走新加坡", + "version": "1.0", + "relay_architecture": { + "description": "双层中继: 用户请求 → 新加坡(带宽源) → 硅谷(IP壳) → Claude API", + "singapore_servers": { + "ZY-SVR-002": { + "role": "面孔·前端+专线主入口", + "location": "Singapore", + "spec": "2核8G" + }, + "ZY-SVR-005": { + "role": "大脑·DB+MCP+Agent+订阅", + "location": "Singapore", + "spec": "4核8G" + } + }, + "silicon_valley_server": { + "ZY-SVR-SV": { + "role": "Claude API中继·IP壳", + "location": "Silicon Valley (US)", + "purpose": "Claude API请求出口 · 绕过新加坡IP屏蔽", + "github_secrets": { + "host": "ZY_SVR_SV_HOST", + "ssh_key": "ZY_SVR_SV_KEY", + "user": "ZY_SVR_SV_USER" + } + } + } + }, + "claude_routing": { + "trigger_models": [ + "claude-3-haiku-20240307", + "claude-3-sonnet-20240229", + "claude-3-opus-20240229", + "claude-3-5-sonnet-20241022" + ], + "relay_method": "ssh_tunnel", + "tunnel_config": { + "description": "新加坡服务器通过SSH隧道将Claude请求从硅谷出口", + "local_port": 18443, + "remote_host": "api.anthropic.com", + "remote_port": 443, + "ssh_command_template": "ssh -f -N -L 18443:api.anthropic.com:443 ${ZY_SVR_SV_USER}@${ZY_SVR_SV_HOST}" + }, + "fallback": "direct", + "fallback_note": "如硅谷隧道不可用,降级为直连(可能被屏蔽)" + }, + "bandwidth_optimization": { + "strategy": "新加坡带宽中继", + "description": "请求体通常很小(几KB),经SSH隧道延迟可忽略。响应体通过隧道返回,硅谷服务器不缓存任何数据。", + "sg_bandwidth": "高带宽·低延迟·主数据通道", + "sv_bandwidth": "低配·仅做TCP握手+TLS出口" + }, + "setup_requirements": { + "silicon_valley_server": [ + "安装SSH Server (已有)", + "配置SSH公钥 (新加坡服务器 → 硅谷服务器)", + "防火墙放行SSH端口", + "无需安装Nginx (纯SSH隧道模式)" + ], + "singapore_server": [ + "配置SSH私钥 (连接硅谷服务器)", + "设置SSH隧道自动重连 (autossh or systemd)", + "llm-router.js 检测Claude模型时走 localhost:18443" + ], + "github_secrets": [ + "ZY_SVR_SV_HOST — 硅谷服务器IP", + "ZY_SVR_SV_KEY — SSH私钥", + "ZY_SVR_SV_USER — SSH用户名" + ] + } +} diff --git a/server/proxy/config/server-registry.json b/server/proxy/config/server-registry.json new file mode 100644 index 00000000..1db13ebd --- /dev/null +++ b/server/proxy/config/server-registry.json @@ -0,0 +1,101 @@ +{ + "_sovereign": "TCS-0002∞ | SYS-GLW-0001", + "_copyright": "国作登字-2026-A-00037559", + "_description": "光湖语言世界 · 四台服务器注册表", + "version": "1.0", + "updated_at": "2026-04-07", + "servers": [ + { + "id": "ZY-SVR-002", + "name": "面孔", + "role": "前端静态+专线主入口", + "location": "Singapore", + "spec": "2核8G", + "status": "active", + "services": [ + "nginx (静态文件·主站+预览站)", + "xray (VLESS+Reality VPN)", + "api (端口3800·主站)", + "api-preview (端口3801·预览站)", + "persona-studio (端口3002)", + "chat (端口3721)" + ], + "github_secrets": { + "host": "SERVER_HOST", + "key": "SERVER_KEY", + "user": "SERVER_USER" + } + }, + { + "id": "ZY-SVR-005", + "name": "大脑", + "role": "数据库+MCP+Agent+订阅服务", + "location": "Singapore", + "spec": "4核8G", + "status": "active", + "services": [ + "postgresql (人格体数据库)", + "mcp-server (端口3100·51个工具)", + "subscription-v3 (端口3805·多用户专线)", + "bandwidth-pool-agent (带宽汇聚)", + "email-hub (邮件通信中枢)", + "auto-evolution (自动进化引擎)" + ], + "github_secrets": { + "host": "ZY_BRAIN_HOST", + "key": "ZY_BRAIN_KEY", + "user": "ZY_BRAIN_USER" + } + }, + { + "id": "ZY-SVR-SV", + "name": "吱吱线·硅谷", + "role": "Claude API中继+国际API出口", + "location": "Silicon Valley (US)", + "spec": "低配", + "status": "pending", + "services": [ + "ssh-tunnel (Claude API中继出口)", + "说明: IP从硅谷走·带宽从新加坡走" + ], + "github_secrets": { + "host": "ZY_SVR_SV_HOST", + "key": "ZY_SVR_SV_KEY", + "user": "ZY_SVR_SV_USER" + }, + "notes": "硅谷服务器仅做TCP出口·不部署应用" + }, + { + "id": "ZY-SVR-AWEN", + "name": "Awen服务器", + "role": "铸渊主控副本+Awen团队专线", + "location": "China", + "spec": "待确认", + "status": "pending", + "services": [ + "铸渊主控副本 (完整仓库镜像)", + "Awen团队专线", + "知秋人格体运行环境" + ], + "github_secrets": { + "host": "ZY_SVR_AWEN_HOST", + "key": "ZY_SVR_AWEN_KEY", + "user": "ZY_SVR_AWEN_USER" + }, + "notes": "Awen下载完整仓库后部署·铸渊主控自动唤醒" + } + ], + "bandwidth_strategy": { + "description": "四台服务器按用途分流·带宽最大化", + "routing": { + "static_frontend": "ZY-SVR-002", + "vpn_main_entry": "ZY-SVR-002", + "database_mcp": "ZY-SVR-005", + "subscription_service": "ZY-SVR-005", + "claude_api_relay": "ZY-SVR-SV", + "international_api": "ZY-SVR-SV", + "awen_team": "ZY-SVR-AWEN", + "zhiqiu_persona": "ZY-SVR-AWEN" + } + } +} diff --git a/server/proxy/service/llm-router.js b/server/proxy/service/llm-router.js index a0f2cac6..05f98694 100644 --- a/server/proxy/service/llm-router.js +++ b/server/proxy/service/llm-router.js @@ -31,6 +31,24 @@ const MODEL_PRIORITY = [ 'gpt-4o-mini' ]; +// ── Claude模型识别 (需走硅谷中继) ────────────── +const CLAUDE_MODELS = [ + 'claude-3-haiku-20240307', + 'claude-3-sonnet-20240229', + 'claude-3-opus-20240229', + 'claude-3-5-sonnet-20241022' +]; + +// 硅谷SSH隧道本地端口 (Claude API中继) +// 新加坡服务器通过SSH隧道将Claude请求从硅谷IP出口 +// 配置: server/proxy/config/claude-relay-config.json +const CLAUDE_RELAY_PORT = parseInt(process.env.ZY_CLAUDE_RELAY_PORT || '18443', 10); +const CLAUDE_RELAY_ENABLED = process.env.ZY_CLAUDE_RELAY_ENABLED === 'true'; + +function isClaudeModel(model) { + return CLAUDE_MODELS.some(cm => model.includes('claude')); +} + // ── 路由器状态 ────────────────────────────── const routerState = { total_calls: 0, @@ -111,6 +129,7 @@ async function callLLM(prompt, options = {}) { /** * 调用单个模型 + * Claude模型自动走硅谷中继 (当 ZY_CLAUDE_RELAY_ENABLED=true) */ function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout) { return new Promise((resolve, reject) => { @@ -122,6 +141,12 @@ function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxToken urlObj = new URL(`https://${baseUrl}`); } + // Claude模型走硅谷中继 (SSH隧道 localhost:18443 → 硅谷 → api.anthropic.com) + if (CLAUDE_RELAY_ENABLED && isClaudeModel(model)) { + console.log(`[LLM路由器] Claude模型 ${model} → 硅谷中继 (localhost:${CLAUDE_RELAY_PORT})`); + urlObj = new URL(`https://localhost:${CLAUDE_RELAY_PORT}`); + } + const postData = JSON.stringify({ model, messages: [ @@ -204,4 +229,4 @@ function getRouterStatus() { }; } -module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY }; +module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY, CLAUDE_MODELS, isClaudeModel };