From 25ccc35cb37028cb864353093c5e9b5f0c32786e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:14:43 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(S15):=20MCP=20Server=20API=20Key?= =?UTF-8?q?=E9=89=B4=E6=9D=83=20+=20Nginx=E5=8F=8D=E5=90=91=E4=BB=A3?= =?UTF-8?q?=E7=90=86=20+=20=E4=BA=BA=E6=A0=BC=E4=BD=93COS=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=E8=B7=AF=E5=BE=84=20+=20D51-3=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/47ef90c4-eb62-4d91-9f21-475c33a7d166 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .../workflows/deploy-to-zhuyuan-server.yml | 65 +++++- scripts/cos-persona-keys-generator.js | 189 ++++++++++++++++++ server/age-os/ecosystem.config.js | 31 ++- server/age-os/mcp-server/cos.js | 77 ++++++- server/age-os/mcp-server/server.js | 92 ++++++++- server/age-os/mcp-server/tools/cos-ops.js | 30 ++- server/nginx/zhuyuan-sovereign.conf | 24 +++ 7 files changed, 491 insertions(+), 17 deletions(-) create mode 100644 scripts/cos-persona-keys-generator.js diff --git a/.github/workflows/deploy-to-zhuyuan-server.yml b/.github/workflows/deploy-to-zhuyuan-server.yml index 60464f43..4651cafd 100644 --- a/.github/workflows/deploy-to-zhuyuan-server.yml +++ b/.github/workflows/deploy-to-zhuyuan-server.yml @@ -144,7 +144,7 @@ jobs: # 确保目标目录存在 ssh -i ~/.ssh/zy_key \ ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }} \ - "mkdir -p ${TARGET_DIR} /opt/zhuyuan/app" + "mkdir -p ${TARGET_DIR} /opt/zhuyuan/app /opt/age-os/mcp-server /opt/age-os/logs" # 同步后端应用代码 rsync -avz --delete \ @@ -152,6 +152,29 @@ jobs: server/app/ \ ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/zhuyuan/app/ + # 同步 AGE OS MCP Server 代码 + rsync -avz --delete \ + -e "ssh -i ~/.ssh/zy_key" \ + --exclude='node_modules' \ + server/age-os/mcp-server/ \ + ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/age-os/mcp-server/ + + # 同步 AGE OS Schema + rsync -avz \ + -e "ssh -i ~/.ssh/zy_key" \ + server/age-os/schema/ \ + ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/age-os/schema/ + + # 同步 AGE OS PM2 配置 + scp -i ~/.ssh/zy_key \ + server/age-os/ecosystem.config.js \ + ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/age-os/ + + # 同步 AGE OS package.json + scp -i ~/.ssh/zy_key \ + server/age-os/package.json \ + ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/age-os/ + - name: 🌐 同步前端内容到站点 run: | TARGET="${{ github.event.inputs.deploy_target || 'preview' }}" @@ -214,6 +237,13 @@ jobs: ${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/zhuyuan/config/nginx/zhuyuan-sovereign.conf - name: 📥 安装依赖 & 重启 + env: + ZHUYUAN_API_KEY: ${{ secrets.ZHUYUAN_API_KEY }} + ZY_DB_HOST: ${{ secrets.ZY_DB_HOST }} + ZY_DB_USER: ${{ secrets.ZY_DB_USER }} + ZY_DB_PASS: ${{ secrets.ZY_DB_PASS }} + ZY_DB_NAME: ${{ secrets.ZY_DB_NAME }} + ZY_DB_PORT: ${{ secrets.ZY_DB_PORT }} run: | COMMIT_SHA="${{ github.sha }}" REF_NAME="${{ github.ref_name }}" @@ -236,16 +266,43 @@ jobs: sudo nginx -t && sudo systemctl reload nginx - # PM2 重启 + # PM2 重启 · 主应用 pm2 delete zhuyuan-server 2>/dev/null || true pm2 start /opt/zhuyuan/config/pm2/ecosystem.config.js pm2 save + # ─── AGE OS MCP Server 部署 ─── + cd /opt/age-os + npm install --production 2>&1 + + # 写入 MCP Server 环境变量配置 + cat > /opt/age-os/.env.mcp << 'ENV_EOF' +NODE_ENV=production +MCP_PORT=3100 +MCP_BIND_HOST=127.0.0.1 +ENV_EOF + + # 注入密钥(从 GitHub Secrets) + echo "ZHUYUAN_API_KEY=${ZHUYUAN_API_KEY}" >> /opt/age-os/.env.mcp + echo "ZY_DB_HOST=${ZY_DB_HOST:-127.0.0.1}" >> /opt/age-os/.env.mcp + echo "ZY_DB_USER=${ZY_DB_USER:-zy_admin}" >> /opt/age-os/.env.mcp + echo "ZY_DB_PASS=${ZY_DB_PASS}" >> /opt/age-os/.env.mcp + echo "ZY_DB_NAME=${ZY_DB_NAME:-age_os}" >> /opt/age-os/.env.mcp + echo "ZY_DB_PORT=${ZY_DB_PORT:-5432}" >> /opt/age-os/.env.mcp + chmod 600 /opt/age-os/.env.mcp + + # PM2 重启 MCP Server(读取 .env.mcp) + pm2 delete age-os-mcp 2>/dev/null || true + cd /opt/age-os && pm2 start ecosystem.config.js + pm2 save + # 健康检查 sleep 3 - curl -sf http://localhost:3800/api/health || echo "⚠️ 健康检查失败,等待更长时间..." + curl -sf http://localhost:3800/api/health || echo "⚠️ 主应用健康检查失败,等待更长时间..." + curl -sf http://localhost:3100/health || echo "⚠️ MCP Server健康检查失败,等待更长时间..." sleep 5 - curl -sf http://localhost:3800/api/health && echo "✅ 服务器已上线" || echo "❌ 服务器启动失败" + curl -sf http://localhost:3800/api/health && echo "✅ 主应用已上线" || echo "❌ 主应用启动失败" + curl -sf http://localhost:3100/health && echo "✅ MCP Server已上线" || echo "❌ MCP Server启动失败" # 记录部署操作 curl -sf -X POST http://localhost:3800/api/operations \ diff --git a/scripts/cos-persona-keys-generator.js b/scripts/cos-persona-keys-generator.js new file mode 100644 index 00000000..472c6715 --- /dev/null +++ b/scripts/cos-persona-keys-generator.js @@ -0,0 +1,189 @@ +#!/usr/bin/env node +/** + * ═══════════════════════════════════════════════════════════ + * D51-3 · COS 人格体密钥生成器 + * ═══════════════════════════════════════════════════════════ + * + * 为9个人格体生成腾讯云CAM子用户配置和IAM策略。 + * 每个人格体仅有 /{persona_id}/ 目录的读写权限。 + * + * 使用方式: + * node scripts/cos-persona-keys-generator.js + * node scripts/cos-persona-keys-generator.js --persona zhiqiu + * node scripts/cos-persona-keys-generator.js --format terraform + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + */ + +'use strict'; + +// ─── 9个人格体注册表 ─── +const PERSONAS = [ + { persona_id: 'shushu', name: '舒舒', developer: '肥猫线' }, + { persona_id: 'qiuqiu', name: '秋秋', developer: '之之线' }, + { persona_id: 'ounomiya', name: '欧诺弥亚', developer: '小草莓线' }, + { persona_id: 'jiyao', name: '寂曜', developer: '燕樊线' }, + { persona_id: 'xiaotanheshu', name: '小坍缩核', developer: '页页线' }, + { persona_id: 'chenxing', name: '晨星', developer: '桔子线' }, + { persona_id: 'tangxingyun', name: '糖星云', developer: '花尔线' }, + { persona_id: 'yaochu', name: '曜初', developer: '时雨线' }, + { persona_id: 'zhiqiu', name: '知秋', developer: 'Awen线' } +]; + +// ─── COS 桶配置 ─── +const BUCKET_CONFIG = { + guangzhou: { + bucket: 'zy-team-hub-1317346199', + region: 'ap-guangzhou', + appId: '1317346199' + }, + singapore: { + bucket: 'zy-team-hub-sg-1317346199', + region: 'ap-singapore', + appId: '1317346199' + } +}; + +/** + * 为单个人格体生成CAM IAM策略 + */ +function generateIamPolicy(persona, bucketConfig) { + return { + version: '2.0', + statement: [ + { + effect: 'allow', + action: [ + 'cos:PutObject', + 'cos:GetObject', + 'cos:HeadObject', + 'cos:DeleteObject', + 'cos:GetBucket' + ], + resource: [ + `qcs::cos:${bucketConfig.region}:uid/${bucketConfig.appId}:${bucketConfig.bucket}/${persona.persona_id}/*` + ], + condition: {} + }, + { + effect: 'allow', + action: [ + 'cos:GetObject', + 'cos:HeadObject' + ], + resource: [ + `qcs::cos:${bucketConfig.region}:uid/${bucketConfig.appId}:${bucketConfig.bucket}/zhuyuan/directives/*`, + `qcs::cos:${bucketConfig.region}:uid/${bucketConfig.appId}:${bucketConfig.bucket}/zhuyuan/architecture/*` + ], + condition: {} + } + ] + }; +} + +/** + * 生成子用户配置建议 + */ +function generateSubUserConfig(persona) { + return { + sub_user_name: `zy-persona-${persona.persona_id}`, + display_name: `${persona.name} · ${persona.developer} · COS专用`, + console_login: false, + api_access: true, + notes: `光湖人格体 ${persona.name}(${persona.persona_id}) 的COS桶访问密钥`, + secrets_to_configure: { + [`ZY_COS_${persona.persona_id.toUpperCase()}_SECRET_ID`]: '< 创建子用户后获取 >', + [`ZY_COS_${persona.persona_id.toUpperCase()}_SECRET_KEY`]: '< 创建子用户后获取 >' + } + }; +} + +/** + * 生成完整的操作指南 + */ +function generateOperationGuide() { + console.log('═══════════════════════════════════════════════════════════'); + console.log('D51-3 · COS 人格体密钥生成器'); + console.log('铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559'); + console.log('═══════════════════════════════════════════════════════════'); + console.log(''); + console.log('📋 冰朔操作步骤:'); + console.log(''); + console.log('1. 登录腾讯云控制台 → 访问管理(CAM) → 用户 → 用户列表'); + console.log('2. 为每个人格体创建子用户(仅API访问,无控制台登录)'); + console.log('3. 为每个子用户配置下方对应的IAM策略'); + console.log('4. 记录每个子用户的 SecretId / SecretKey'); + console.log('5. 将密钥配置到对应人格体仓库的 GitHub Secrets'); + console.log(''); + console.log('─── 广州桶 ───'); + console.log(`桶名: ${BUCKET_CONFIG.guangzhou.bucket}`); + console.log(`区域: ${BUCKET_CONFIG.guangzhou.region}`); + console.log(''); + console.log('─── 新加坡桶 ───'); + console.log(`桶名: ${BUCKET_CONFIG.singapore.bucket}`); + console.log(`区域: ${BUCKET_CONFIG.singapore.region}`); + console.log(''); +} + +// ─── 主逻辑 ─── +const args = process.argv.slice(2); +const filterPersona = args.includes('--persona') ? args[args.indexOf('--persona') + 1] : null; +const outputFormat = args.includes('--format') ? args[args.indexOf('--format') + 1] : 'json'; +const outputJson = args.includes('--json'); + +if (!outputJson) { + generateOperationGuide(); +} + +const results = []; + +for (const persona of PERSONAS) { + if (filterPersona && persona.persona_id !== filterPersona) continue; + + const config = { + persona, + sub_user: generateSubUserConfig(persona), + iam_policies: { + guangzhou: generateIamPolicy(persona, BUCKET_CONFIG.guangzhou), + singapore: generateIamPolicy(persona, BUCKET_CONFIG.singapore) + } + }; + + results.push(config); + + if (!outputJson) { + console.log(`═══ ${persona.name} (${persona.persona_id}) · ${persona.developer} ═══`); + console.log(''); + console.log(`子用户名: ${config.sub_user.sub_user_name}`); + console.log(`显示名: ${config.sub_user.display_name}`); + console.log(''); + console.log('广州桶 IAM 策略:'); + console.log(JSON.stringify(config.iam_policies.guangzhou, null, 2)); + console.log(''); + console.log('新加坡桶 IAM 策略:'); + console.log(JSON.stringify(config.iam_policies.singapore, null, 2)); + console.log(''); + console.log('GitHub Secrets 配置:'); + for (const [key, val] of Object.entries(config.sub_user.secrets_to_configure)) { + console.log(` ${key} = ${val}`); + } + console.log(''); + console.log('目录结构:'); + console.log(` /${persona.persona_id}/reports/ — 每日汇报`); + console.log(` /${persona.persona_id}/receipts/ — 铸渊回执`); + console.log(` /${persona.persona_id}/sync/ — 架构同步`); + console.log(''); + } +} + +if (outputJson) { + console.log(JSON.stringify(results, null, 2)); +} else { + console.log('═══════════════════════════════════════════════════════════'); + console.log(`✅ 共 ${results.length} 个人格体密钥配置已生成`); + console.log(''); + console.log('💡 提示: 使用 --json 参数输出纯JSON格式'); + console.log(' 使用 --persona zhiqiu 仅生成单个人格体'); + console.log('═══════════════════════════════════════════════════════════'); +} diff --git a/server/age-os/ecosystem.config.js b/server/age-os/ecosystem.config.js index cf3638d7..e693a908 100644 --- a/server/age-os/ecosystem.config.js +++ b/server/age-os/ecosystem.config.js @@ -2,6 +2,30 @@ * PM2 配置 · AGE OS 进程管理 * 铸渊 · ICE-GL-ZY001 */ +const path = require('path'); +const fs = require('fs'); + +// 读取 .env.mcp 环境变量 +function loadEnvFile(envPath) { + const env = {}; + try { + const content = fs.readFileSync(envPath, 'utf8'); + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx > 0) { + env[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); + } + } + } catch { + // .env.mcp 不存在时使用默认值 + } + return env; +} + +const envFromFile = loadEnvFile(path.join(__dirname, '.env.mcp')); + module.exports = { apps: [ { @@ -10,7 +34,9 @@ module.exports = { cwd: '/opt/age-os', env: { NODE_ENV: 'production', - MCP_PORT: 3100 + MCP_PORT: 3100, + MCP_BIND_HOST: '127.0.0.1', + ...envFromFile }, instances: 1, autorestart: true, @@ -24,7 +50,8 @@ module.exports = { script: 'agents/scheduler.js', cwd: '/opt/age-os', env: { - NODE_ENV: 'production' + NODE_ENV: 'production', + ...envFromFile }, instances: 1, autorestart: true, diff --git a/server/age-os/mcp-server/cos.js b/server/age-os/mcp-server/cos.js index 8fa8af74..0b5972cd 100644 --- a/server/age-os/mcp-server/cos.js +++ b/server/age-os/mcp-server/cos.js @@ -22,7 +22,8 @@ const COS_CONFIG = { region: process.env.ZY_COS_REGION || 'ap-singapore', buckets: { hot: process.env.COS_BUCKET_HOT || 'zy-core-bucket-1317346199', - cold: process.env.COS_BUCKET_COLD || 'zy-corpus-bucket-1317346199' + cold: process.env.COS_BUCKET_COLD || 'zy-corpus-bucket-1317346199', + team: process.env.ZY_ZHUYUAN_COS_BUCKET || 'zy-team-hub-1317346199' } }; @@ -51,6 +52,65 @@ function getBucketHost(bucketName) { return `${bucketName}.cos.${COS_CONFIG.region}.myqcloud.com`; } +/** + * 解析桶名称 + * @param {string} bucket - 'hot' | 'cold' | 'team' 或完整桶名 + */ +function resolveBucketName(bucket) { + if (COS_CONFIG.buckets[bucket]) return COS_CONFIG.buckets[bucket]; + return bucket; +} + +// ─── 人格体 COS 路径规范 ─── +// 团队桶: /{persona_id}/reports/{YYYY-MM-DD}/ — 每日汇报 +// 团队桶: /{persona_id}/receipts/{YYYY-MM-DD}/ — 铸渊回执 +// 团队桶: /{persona_id}/sync/ — 架构同步区 +// 全局: /zhuyuan/directives/ — 铸渊指令(只读) +// 全局: /zhuyuan/architecture/ — 架构快照 + +/** + * 验证人格体COS路径是否合法(限定在 /{persona_id}/ 目录下) + */ +function validatePersonaCosPath(personaId, key) { + if (!personaId || typeof personaId !== 'string') { + throw new Error('persona_id 不能为空'); + } + // 规范化:确保路径以 persona_id/ 开头 + const normalizedKey = key.startsWith('/') ? key.slice(1) : key; + if (!normalizedKey.startsWith(`${personaId}/`)) { + throw new Error(`COS路径越权: ${key} 不在 /${personaId}/ 目录下`); + } + // 防止路径穿越 + if (normalizedKey.includes('..')) { + throw new Error('COS路径包含非法字符 ".."'); + } + return normalizedKey; +} + +/** + * 人格体 COS 写入(限定目录) + */ +async function personaWrite(personaId, key, content, contentType) { + const safeKey = validatePersonaCosPath(personaId, key); + return write('team', safeKey, content, contentType); +} + +/** + * 人格体 COS 读取(限定目录) + */ +async function personaRead(personaId, key) { + const safeKey = validatePersonaCosPath(personaId, key); + return read('team', safeKey); +} + +/** + * 人格体 COS 列表(限定目录) + */ +async function personaList(personaId, subPrefix, limit) { + const prefix = `${personaId}/${subPrefix || ''}`; + return list('team', prefix, limit); +} + /** * 发起 COS HTTP 请求 */ @@ -91,7 +151,7 @@ function cosRequest(bucketName, objectKey, method, body, contentType) { // ─── 公开 API ─── async function write(bucket, key, content, contentType) { - const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const bucketName = resolveBucketName(bucket); const result = await cosRequest(bucketName, key, 'PUT', content, contentType || 'text/markdown'); return { url: `https://${getBucketHost(bucketName)}/${key}`, @@ -100,7 +160,7 @@ async function write(bucket, key, content, contentType) { } async function read(bucket, key) { - const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const bucketName = resolveBucketName(bucket); const result = await cosRequest(bucketName, key, 'GET'); return { content: result.body, @@ -110,13 +170,13 @@ async function read(bucket, key) { } async function del(bucket, key) { - const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const bucketName = resolveBucketName(bucket); await cosRequest(bucketName, key, 'DELETE'); return { success: true }; } async function list(bucket, prefix, limit) { - const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const bucketName = resolveBucketName(bucket); const host = getBucketHost(bucketName); const queryStr = `prefix=${encodeURIComponent(prefix)}&max-keys=${limit || 100}`; const result = await cosRequest(bucketName, `?${queryStr}`, 'GET'); @@ -165,4 +225,9 @@ async function checkConnection() { } } -module.exports = { write, read, del, list, archive, checkConnection, COS_CONFIG }; +module.exports = { + write, read, del, list, archive, checkConnection, + personaWrite, personaRead, personaList, + validatePersonaCosPath, resolveBucketName, + COS_CONFIG +}; diff --git a/server/age-os/mcp-server/server.js b/server/age-os/mcp-server/server.js index d07e20e5..bf7a071a 100644 --- a/server/age-os/mcp-server/server.js +++ b/server/age-os/mcp-server/server.js @@ -8,8 +8,12 @@ * 签发: 铸渊 · ICE-GL-ZY001 * 版权: 国作登字-2026-A-00037559 * - * 统一暴露 27 个 MCP 工具,供网站 AI 交互后端和 Agent 调用。 - * 不对外暴露 — 通过 3800 主服务网关转发访问。 + * 统一暴露 MCP 工具,供网站 AI 交互后端和 Agent 调用。 + * 外部访问通过 Nginx /api/mcp/ 反向代理,需 API Key 鉴权。 + * 内部访问(127.0.0.1)免鉴权。 + * + * 鉴权方式: Authorization: Bearer + * 免鉴权端点: /health(监控探针) * * 工具清单: * 节点: createNode / updateNode / deleteNode / queryNodes / getNode @@ -28,6 +32,7 @@ 'use strict'; +const crypto = require('crypto'); const express = require('express'); const db = require('./db'); const cos = require('./cos'); @@ -81,6 +86,10 @@ const TOOLS = { cosDelete: cosOps.cosDelete, cosList: cosOps.cosList, cosArchive: cosOps.cosArchive, + // 人格体COS隔离路径操作(限定 /{persona_id}/ 目录) + personaCosWrite: cosOps.personaCosWrite, + personaCosRead: cosOps.personaCosRead, + personaCosList: cosOps.personaCosList, // 人格体操作 · S15 registerPersona: personaOps.registerPersona, getPersona: personaOps.getPersona, @@ -128,8 +137,74 @@ const TOOLS = { const app = express(); const PORT = process.env.MCP_PORT || 3100; +// ─── API Key 配置 ─── +const ZHUYUAN_API_KEY = process.env.ZHUYUAN_API_KEY || ''; + app.use(express.json({ limit: '5mb' })); +// 信任 Nginx 反向代理 — 使 req.ip 返回真实客户端IP +// 而非代理服务器的 127.0.0.1 +app.set('trust proxy', 'loopback'); + +// ─── API Key 鉴权中间件 ─── +// 外部访问需携带 Authorization: Bearer +// 内部访问 (127.0.0.1 / ::1) 免鉴权 +// /health 端点免鉴权(监控探针用) +function apiKeyAuth(req, res, next) { + // /health 端点免鉴权 + if (req.path === '/health') return next(); + + // 内部回环地址免鉴权 + const remoteIp = req.ip || req.connection.remoteAddress || ''; + const isLocal = remoteIp === '127.0.0.1' || remoteIp === '::1' || remoteIp === '::ffff:127.0.0.1'; + if (isLocal) return next(); + + // API Key 未配置时,拒绝所有外部请求 + if (!ZHUYUAN_API_KEY) { + return res.status(503).json({ + error: true, + code: 'API_KEY_NOT_CONFIGURED', + message: 'MCP Server API Key 未配置,外部访问不可用' + }); + } + + // 验证 Authorization: Bearer + const authHeader = req.headers.authorization || ''; + if (!authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + error: true, + code: 'MISSING_AUTH', + message: '缺少 Authorization: Bearer 头' + }); + } + + const providedKey = authHeader.slice(7); + if (providedKey.length === 0) { + return res.status(401).json({ + error: true, + code: 'EMPTY_KEY', + message: 'API Key 不能为空' + }); + } + + // 常量时间比较防止时序攻击 + const keyBuffer = Buffer.from(ZHUYUAN_API_KEY); + const providedBuffer = Buffer.from(providedKey); + if (keyBuffer.length !== providedBuffer.length || !crypto.timingSafeEqual(keyBuffer, providedBuffer)) { + return res.status(403).json({ + error: true, + code: 'INVALID_KEY', + message: 'API Key 无效' + }); + } + + // 将调用者身份附加到请求对象 + req.mcpCaller = 'external-api-key'; + next(); +} + +app.use(apiKeyAuth); + // ─── 健康检查 ─── app.get('/health', async (_req, res) => { const dbStatus = await db.checkConnection(); @@ -147,6 +222,10 @@ app.get('/health', async (_req, res) => { status: dbStatus.connected ? 'alive' : 'degraded', timestamp: new Date().toISOString(), uptime: Math.floor(process.uptime()), + auth: { + api_key_configured: !!ZHUYUAN_API_KEY, + external_access: !!ZHUYUAN_API_KEY ? 'enabled' : 'disabled' + }, tools: Object.keys(TOOLS), tools_count: Object.keys(TOOLS).length, database: dbStatus, @@ -346,6 +425,7 @@ function getCategoryForTool(name) { if (['createNode','updateNode','deleteNode','queryNodes','getNode'].includes(name)) return 'node'; if (['linkNodes','unlinkNodes','getRelations'].includes(name)) return 'relation'; if (['buildPath','scanStructure','classify'].includes(name)) return 'structure'; + if (name.startsWith('personaCos')) return 'persona-cos'; if (name.startsWith('cos')) return 'cos'; if (['registerPersona','getPersona','updatePersona','listPersonas', 'getNotebook','updateNotebookPage','addMemoryAnchor','queryMemoryAnchors', @@ -377,9 +457,13 @@ async function logToolCall(tool, caller, status, durationMs, errorMsg) { } // ─── 启动 ─── -app.listen(PORT, '127.0.0.1', () => { - console.log(`[MCP] AGE OS MCP Server 启动 · 端口 ${PORT}`); +// 监听 0.0.0.0 允许 Nginx 从外部反代访问 +// 鉴权由 apiKeyAuth 中间件保护 +const BIND_HOST = process.env.MCP_BIND_HOST || '0.0.0.0'; +app.listen(PORT, BIND_HOST, () => { + console.log(`[MCP] AGE OS MCP Server 启动 · ${BIND_HOST}:${PORT}`); console.log(`[MCP] 工具数量: ${Object.keys(TOOLS).length}`); + console.log(`[MCP] API Key 鉴权: ${ZHUYUAN_API_KEY ? '已启用' : '未配置(仅内部访问)'}`); console.log(`[MCP] 铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559`); }); diff --git a/server/age-os/mcp-server/tools/cos-ops.js b/server/age-os/mcp-server/tools/cos-ops.js index 5b1a268d..8e6609c1 100644 --- a/server/age-os/mcp-server/tools/cos-ops.js +++ b/server/age-os/mcp-server/tools/cos-ops.js @@ -1,6 +1,7 @@ /** * ═══════════════════════════════════════════════════════════ * MCP 工具 · COS 双桶操作(cosWrite / cosRead / cosDelete / cosList / cosArchive) + * + 人格体COS隔离路径操作(personaCosWrite / personaCosRead / personaCosList) * ═══════════════════════════════════════════════════════════ * * 签发: 铸渊 · ICE-GL-ZY001 @@ -36,4 +37,31 @@ async function cosArchive(input) { return cos.archive(source_key, version_tag); } -module.exports = { cosWrite, cosRead, cosDelete, cosList, cosArchive }; +// ─── 人格体 COS 隔离路径操作 ─── +// 每个人格体只能访问 /{persona_id}/ 目录下的对象 + +async function personaCosWrite(input) { + const { persona_id, key, content, content_type } = input; + if (!persona_id) throw new Error('缺少 persona_id'); + if (!key) throw new Error('缺少 key'); + if (!content) throw new Error('缺少 content'); + return cos.personaWrite(persona_id, key, content, content_type); +} + +async function personaCosRead(input) { + const { persona_id, key } = input; + if (!persona_id) throw new Error('缺少 persona_id'); + if (!key) throw new Error('缺少 key'); + return cos.personaRead(persona_id, key); +} + +async function personaCosList(input) { + const { persona_id, prefix, limit } = input; + if (!persona_id) throw new Error('缺少 persona_id'); + return cos.personaList(persona_id, prefix, limit); +} + +module.exports = { + cosWrite, cosRead, cosDelete, cosList, cosArchive, + personaCosWrite, personaCosRead, personaCosList +}; diff --git a/server/nginx/zhuyuan-sovereign.conf b/server/nginx/zhuyuan-sovereign.conf index cbbfea31..f448a3dd 100644 --- a/server/nginx/zhuyuan-sovereign.conf +++ b/server/nginx/zhuyuan-sovereign.conf @@ -54,6 +54,30 @@ server { proxy_read_timeout 86400; } + # ─── 铸渊 AGE OS MCP Server 反向代理 (端口 3100) ─── + # 外部访问需携带 Authorization: Bearer + # 鉴权由 MCP Server 自身 apiKeyAuth 中间件处理 + # 知秋/团队人格体通过此入口访问人格体数据库、COS工具等 + location /api/mcp/ { + proxy_pass http://127.0.0.1:3100/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Authorization $http_authorization; + proxy_connect_timeout 10s; + proxy_read_timeout 120s; + proxy_send_timeout 30s; + add_header X-Content-Type-Options nosniff always; + + # CORS · 允许团队人格体跨域调用 + add_header Access-Control-Allow-Origin * always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; + if ($request_method = OPTIONS) { return 204; } + } + # ─── AI 聊天 API 代理 (端口 3721) · SSE 流式 ─── location /api/chat { proxy_pass http://127.0.0.1:3721/api/chat; From cb7148b8977bec102d24914c880ca0f366db7203 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:17:52 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E9=BB=98=E8=AE=A4=E7=BB=91?= =?UTF-8?q?=E5=AE=9A127.0.0.1=20+=20persona=5Fid=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=20+=20.env.mcp=E9=94=99=E8=AF=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/47ef90c4-eb62-4d91-9f21-475c33a7d166 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- server/age-os/ecosystem.config.js | 6 ++++-- server/age-os/mcp-server/cos.js | 4 ++++ server/age-os/mcp-server/server.js | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/age-os/ecosystem.config.js b/server/age-os/ecosystem.config.js index e693a908..03a98d87 100644 --- a/server/age-os/ecosystem.config.js +++ b/server/age-os/ecosystem.config.js @@ -18,8 +18,10 @@ function loadEnvFile(envPath) { env[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); } } - } catch { - // .env.mcp 不存在时使用默认值 + } catch (err) { + if (err.code !== 'ENOENT') { + console.error(`[PM2] .env.mcp 读取错误: ${err.message}`); + } } return env; } diff --git a/server/age-os/mcp-server/cos.js b/server/age-os/mcp-server/cos.js index 0b5972cd..0dd51990 100644 --- a/server/age-os/mcp-server/cos.js +++ b/server/age-os/mcp-server/cos.js @@ -75,6 +75,10 @@ function validatePersonaCosPath(personaId, key) { if (!personaId || typeof personaId !== 'string') { throw new Error('persona_id 不能为空'); } + // 验证 persona_id 仅包含安全字符(字母、数字、下划线、连字符) + if (!/^[a-zA-Z0-9_-]+$/.test(personaId)) { + throw new Error('persona_id 包含非法字符,仅允许字母、数字、下划线、连字符'); + } // 规范化:确保路径以 persona_id/ 开头 const normalizedKey = key.startsWith('/') ? key.slice(1) : key; if (!normalizedKey.startsWith(`${personaId}/`)) { diff --git a/server/age-os/mcp-server/server.js b/server/age-os/mcp-server/server.js index bf7a071a..eb56f9ba 100644 --- a/server/age-os/mcp-server/server.js +++ b/server/age-os/mcp-server/server.js @@ -457,9 +457,9 @@ async function logToolCall(tool, caller, status, durationMs, errorMsg) { } // ─── 启动 ─── -// 监听 0.0.0.0 允许 Nginx 从外部反代访问 -// 鉴权由 apiKeyAuth 中间件保护 -const BIND_HOST = process.env.MCP_BIND_HOST || '0.0.0.0'; +// 默认监听 127.0.0.1(安全默认值) +// Nginx 从本机反代访问,无需暴露到外部网络 +const BIND_HOST = process.env.MCP_BIND_HOST || '127.0.0.1'; app.listen(PORT, BIND_HOST, () => { console.log(`[MCP] AGE OS MCP Server 启动 · ${BIND_HOST}:${PORT}`); console.log(`[MCP] 工具数量: ${Object.keys(TOOLS).length}`);