Merge pull request #307 from qinfendebingshuo/copilot/add-mcp-server-api-key-auth

feat(S15): MCP Server API Key auth, Nginx reverse proxy, persona COS isolation
This commit is contained in:
冰朔 2026-04-07 23:27:29 +08:00 committed by GitHub
commit 3e2ffff2bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 497 additions and 17 deletions

View File

@ -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 \

View File

@ -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('═══════════════════════════════════════════════════════════');
}

View File

@ -2,6 +2,32 @@
* 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 (err) {
if (err.code !== 'ENOENT') {
console.error(`[PM2] .env.mcp 读取错误: ${err.message}`);
}
}
return env;
}
const envFromFile = loadEnvFile(path.join(__dirname, '.env.mcp'));
module.exports = {
apps: [
{
@ -10,7 +36,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 +52,8 @@ module.exports = {
script: 'agents/scheduler.js',
cwd: '/opt/age-os',
env: {
NODE_ENV: 'production'
NODE_ENV: 'production',
...envFromFile
},
instances: 1,
autorestart: true,

View File

@ -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,69 @@ 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 仅包含安全字符(字母、数字、下划线、连字符)
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}/`)) {
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 +155,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 +164,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 +174,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 +229,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
};

View File

@ -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 <ZHUYUAN_API_KEY>
* 免鉴权端点: /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 <ZHUYUAN_API_KEY>
// 内部访问 (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 <key>
const authHeader = req.headers.authorization || '';
if (!authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: true,
code: 'MISSING_AUTH',
message: '缺少 Authorization: Bearer <API_KEY> 头'
});
}
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}`);
// 默认监听 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}`);
console.log(`[MCP] API Key 鉴权: ${ZHUYUAN_API_KEY ? '已启用' : '未配置(仅内部访问)'}`);
console.log(`[MCP] 铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559`);
});

View File

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

View File

@ -54,6 +54,30 @@ server {
proxy_read_timeout 86400;
}
# ─── 铸渊 AGE OS MCP Server 反向代理 (端口 3100) ───
# 外部访问需携带 Authorization: Bearer <ZHUYUAN_API_KEY>
# 鉴权由 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;