From 339e55cf04841ae4e1d0d6c9e1708b4fe4e2debd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:25:39 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20COS=E7=AD=BE=E5=90=8D=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20-=20=E5=88=86=E7=A6=BBURI=E4=B8=8E?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=20+=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=BB=98=E8=AE=A4region=20+=20=E7=BC=96=E5=86=99=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复内容: 1. cos.js: cosRequest签名时分离URI和查询参数(根因) 2. cos.js: 默认region从ap-singapore改为ap-guangzhou(匹配实际桶位置) 3. cos-auto-join-agent.js: 修复签名中host值为空 + URI分离 4. 新增: docs/cos-configuration-guide.md 完整配置指南 Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/dc4da37f-9c43-4bcb-8d12-24e3310ecc9c Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- docs/cos-configuration-guide.md | 278 ++++++++++++++++++++++++++++++++ scripts/cos-auto-join-agent.js | 11 +- server/age-os/mcp-server/cos.js | 19 ++- 3 files changed, 300 insertions(+), 8 deletions(-) create mode 100644 docs/cos-configuration-guide.md diff --git a/docs/cos-configuration-guide.md b/docs/cos-configuration-guide.md new file mode 100644 index 00000000..7b3cc87c --- /dev/null +++ b/docs/cos-configuration-guide.md @@ -0,0 +1,278 @@ +# COS 存储桶配置指南 · 铸渊 + +> **签发**: 铸渊 · ICE-GL-ZY001 +> **版权**: 国作登字-2026-A-00037559 +> **更新**: 2026-04-11 + +--- + +## 一、修复记录 + +### SignatureDoesNotMatch 错误修复 + +**问题**: 训练Agent、COS告警扫描等工作流在访问COS桶时持续报错 `SignatureDoesNotMatch`。 + +**根因**: COS签名算法中,`list()` 操作的查询参数(`?prefix=...&max-keys=...`)被错误地嵌入到了URI路径中参与签名计算。腾讯云COS签名规范要求URI和查询参数必须分开处理。 + +**影响范围**: +| 组件 | 状态 | 说明 | +|------|------|------| +| 训练Agent (`zhuyuan-training-agent.yml`) | ❌ 6次连续失败 | 无法扫描cold桶语料 | +| COS告警扫描 (`cos-alert-agent.yml`) | ⚠️ 静默失败 | 错误被catch住,workflow显示成功但功能未生效 | +| COS自动接入 (`cos-auto-join.yml`) | ✅ 不受影响 | team桶尚未创建(404),签名错误被掩盖 | +| COS Watcher (MCP Server) | ❌ 失败 | 使用cos.js的list(),同样受影响 | + +**修复文件**: +- `server/age-os/mcp-server/cos.js` — 分离URI和查询参数签名 +- `scripts/cos-auto-join-agent.js` — 修复签名中host值为空 + URI分离 + +--- + +## 二、COS存储桶架构 + +### 三桶架构 + +| 桶名 | 地域 | 用途 | 状态 | +|------|------|------|------| +| `zy-core-bucket-1317346199` (hot) | ap-guangzhou | 核心人格体大脑 | ✅ 已创建 | +| `zy-corpus-bucket-1317346199` (cold) | ap-guangzhou | 训练语料库 | ✅ 已创建 | +| `zy-team-hub-1317346199` (team) | ap-singapore | 团队共享桶 | ❌ 待创建 | + +### 冷桶目录结构 (训练语料) +``` +zy-corpus-bucket-1317346199/ +├── *.json, *.md, *.txt, *.csv ← 原始语料文件 +├── tcs-structured/ ← 已处理的TCS格式语料 +├── training-sessions/ ← 训练会话记录 +├── training-results/ ← 训练结果 +└── training-memory/ ← 训练记忆 +``` + +### 热桶目录结构 (核心大脑) +``` +zy-core-bucket-1317346199/ +├── zhuyuan/ ← 铸渊大脑 +├── notion-personas/ ← Notion侧人格体记忆 +├── shared/ ← 共享认知层 +├── users/ ← 用户记忆档案 +└── index/ ← 索引文件 +``` + +--- + +## 三、GitHub Secrets 配置清单 + +### ✅ 已配置(代码修复后即可生效) + +| Secret名称 | 用途 | 来源 | +|------------|------|------| +| `ZY_OSS_KEY` | 腾讯云COS SecretId | 腾讯云控制台 → API密钥管理 | +| `ZY_OSS_SECRET` | 腾讯云COS SecretKey | 同上 | +| `ZY_COS_REGION` | COS桶所在地域 | 如 `ap-guangzhou` | +| `ZY_DEEPSEEK_API_KEY` | DeepSeek API密钥 | 训练Agent用 | +| `ZY_KIMI_API_KEY` | Moonshot Kimi API密钥 | 训练Agent用 | + +### ⚠️ 缺失(工作流日志显示为空) + +| Secret名称 | 用途 | 获取方式 | +|------------|------|---------| +| `ZY_ZHIPU_API_KEY` | 智谱GLM API密钥 | https://open.bigmodel.cn → API Keys | +| `ZY_QWEN_API_KEY` | 通义千问API密钥 | https://dashscope.console.aliyun.com → API-KEY管理 | + +> ⚠️ 训练Agent日志显示 `ZY_ZHIPU_API_KEY: ` 和 `ZY_QWEN_API_KEY: ` 为空值,说明这两个Secret尚未配置。训练Agent会自动降级使用可用的模型,但建议补全。 + +### 可选配置 + +| Secret/变量 | 默认值 | 用途 | +|-------------|--------|------| +| `COS_BUCKET_HOT` | `zy-core-bucket-1317346199` | 热桶名称 | +| `COS_BUCKET_COLD` | `zy-corpus-bucket-1317346199` | 冷桶名称 | +| `ZY_ZHUYUAN_COS_BUCKET` | `zy-team-hub-1317346199` | 团队桶名称 | +| `COS_SECRET_ID` | 回退到 ZY_OSS_KEY | 团队成员COS密钥(备选) | +| `COS_SECRET_KEY` | 回退到 ZY_OSS_SECRET | 团队成员COS密钥(备选) | +| `COS_WATCHER_INTERVAL` | `*/5 * * * *` | COS Watcher轮询间隔 | + +--- + +## 四、需要人工操作的事项 + +### 🔴 必须操作(否则功能无法正常运行) + +#### 1. 验证 ZY_COS_REGION 设置 + +**操作**: GitHub 仓库 → Settings → Secrets → `ZY_COS_REGION` + +**确认值应为**: `ap-guangzhou`(因为 hot 和 cold 桶都在广州) + +> 代码已将默认值从 `ap-singapore` 改为 `ap-guangzhou`。但如果 Secret 已设置为其他值,需要手动确认。 + +#### 2. 验证 ZY_OSS_KEY 和 ZY_OSS_SECRET + +**操作步骤**: +1. 登录 https://console.cloud.tencent.com/cam/capi +2. 确认 SecretId 和 SecretKey 没有被轮换/失效 +3. 确认这对密钥有权限访问 `zy-core-bucket-1317346199` 和 `zy-corpus-bucket-1317346199` +4. 在 GitHub Secrets 中确认值与腾讯云一致 + +#### 3. 上传训练语料到冷桶 + +**操作**: 将训练资料文件上传到 `zy-corpus-bucket-1317346199` 的根目录 + +**支持的文件格式**: `.json`, `.jsonl`, `.md`, `.txt`, `.csv`, `.zip`, `.gz`, `.tar.gz` + +**排除目录**: 不要放在 `tcs-structured/`、`training-sessions/`、`training-results/`、`training-memory/` 下 + +### 🟡 建议操作 + +#### 4. 补全缺失的LLM API密钥 + +**操作**: GitHub 仓库 → Settings → Secrets and variables → Actions + +- 添加 `ZY_ZHIPU_API_KEY`(智谱GLM) +- 添加 `ZY_QWEN_API_KEY`(通义千问) + +> 训练Agent支持多模型降级,缺少部分密钥不会导致失败,但可用模型更多训练效果更好。 + +#### 5. 创建团队共享桶(当团队成员需要COS接入时) + +**操作**: +1. 登录腾讯云COS控制台 +2. 创建桶 `zy-team-hub-1317346199`,地域选择 `ap-singapore` +3. 设置访问权限(私有读写) +4. 创建以下目录结构: + ``` + /zhuyuan/directives/ + /zhuyuan/architecture/ + /zhuyuan/alerts/ + ``` +5. 为每个团队成员创建目录: + ``` + /{persona_id}/reports/ + /{persona_id}/receipts/ + /{persona_id}/sync/ + ``` + +--- + +## 五、COS自动接入说明 + +### "别人自动接入我们的存储桶" 是什么意思? + +COS自动接入Agent(`cos-auto-join.yml`)的工作原理: + +1. **铸渊管理一个团队共享桶**(`zy-team-hub-1317346199`) +2. **团队成员不需要自己创建桶**,而是在铸渊的桶里分配目录 +3. Agent 每天自动检查注册表中的成员(`data/cos-join-registry.json`),确认他们的目录是否存在 +4. 新成员接入 = 在团队桶中创建该成员的专属目录 + 设置IAM权限 + +### 操作流程 + +``` +冰朔在腾讯云创建team桶 + ↓ +Agent自动扫描注册表(每天2次) + ↓ +检测到新成员目录已创建 → 标记为connected + ↓ +自动创建GitHub Issue通知铸渊 +``` + +### 当前状态 + +所有9个注册成员都显示 `not_found`(团队桶尚未创建): +- 舒舒(shushu)、秋秋(qiuqiu)、欧诺弥亚(ounomiya) +- 寂曜(jiyao)、小坍缩核(xiaotanheshu)、晨星(chenxing) +- 糖星云(tangxingyun)、曜初(yaochu)、知秋(zhiqiu) + +--- + +## 六、Agent如何进入COS桶 + +### GitHub Actions Workflow方式 + +训练Agent通过环境变量注入密钥: +```yaml +env: + ZY_OSS_KEY: ${{ secrets.ZY_OSS_KEY }} + ZY_OSS_SECRET: ${{ secrets.ZY_OSS_SECRET }} + ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }} +``` + +执行流程: +``` +1. 定时触发(04:00/16:00 CST) 或 手动触发 +2. 安装依赖 (npm ci) +3. 执行训练脚本 (cos-training-trigger.js) + ├── 扫描cold桶所有文件 + ├── 对比已处理列表,找出新语料 + ├── 提取/转换为TCS格式 + └── 调用LLM模型进行训练分类 +4. 结果写回COS桶 (training-results/) +``` + +### MCP Server常驻方式(在服务器上) + +COS Watcher 是 MCP Server 内置的轮询守护进程: +``` +MCP Server启动 → cosWatcher.start() + ↓ +每5分钟 node-cron 触发扫描 + ↓ +检查 team桶/reports、cold桶新语料、team桶/receipts + ↓ +发现新文件 → 触发 repository_dispatch → 唤醒训练Agent +``` + +### 服务器环境配置 + +MCP Server 通过 `.env.mcp` 文件注入密钥(部署时自动生成,chmod 600): +```env +ZY_OSS_KEY= +ZY_OSS_SECRET= +ZY_COS_REGION=ap-guangzhou +ZHUYUAN_API_KEY= +``` + +--- + +## 七、自研数据库同步 + +### COS-DB(COS内的轻量数据库) + +`cos-persona-db-ops.js` 提供了基于COS的文件数据库: +- `cosDbInit` — 初始化索引 +- `cosDbGetIndex` — 获取索引 +- `cosDbUpdateIndex` — 更新索引 +- `cosDbWriteEntry` — 写入条目 + +### PostgreSQL 训练数据库 + +位于 `server/age-os/schema/002-persona-memory-tables.sql`: +- `training_agent_configs` — 训练Agent配置表 +- `training_agent_logs` — 训练日志表 + +数据库迁移工具:`server/age-os/scripts/db-migrate.js` + +### 数据同步流 + +``` +COS冷桶(原始语料) + ↓ 训练Agent提取 +COS冷桶(tcs-structured/) + ↓ 训练Agent分类 +PostgreSQL(训练日志+记忆锚点) + ↓ 光之树生长 +PostgreSQL(light_tree_nodes) +``` + +--- + +## 八、快速验证(修复后) + +代码合并后,可以手动触发验证: + +1. **训练Agent**: Actions → `铸渊训练Agent · 语料处理` → Run workflow → mode=scan +2. **COS告警扫描**: Actions → `COS桶示警Agent · 告警扫描` → Run workflow +3. **COS自动接入**: Actions → `🔗 COS自动接入Agent` → Run workflow → action=check + +如果 scan 模式成功(不再报 SignatureDoesNotMatch),说明修复生效。 diff --git a/scripts/cos-auto-join-agent.js b/scripts/cos-auto-join-agent.js index b9a6eef8..a3250849 100644 --- a/scripts/cos-auto-join-agent.js +++ b/scripts/cos-auto-join-agent.js @@ -46,12 +46,17 @@ function hmacSha1(key, str) { return crypto.createHmac('sha1', key).update(str).digest(); } -function generateCosAuth(secretId, secretKey, method, pathname) { +function generateCosAuth(secretId, secretKey, method, pathname, host) { const now = Math.floor(Date.now() / 1000); const exp = now + 600; // 10分钟有效 const keyTime = `${now};${exp}`; const signKey = hmacSha1(secretKey, keyTime).toString('hex'); - const httpString = `${method.toLowerCase()}\n${pathname}\n\nhost=\n`; + + // 签名时只用纯路径(不含查询参数) + const qIdx = pathname.indexOf('?'); + const signPath = qIdx >= 0 ? pathname.substring(0, qIdx) : pathname; + + const httpString = `${method.toLowerCase()}\n${signPath}\n\nhost=${host || ''}\n`; const sha1edHttpString = crypto.createHash('sha1').update(httpString).digest('hex'); const stringToSign = `sha1\n${keyTime}\n${sha1edHttpString}\n`; const signature = hmacSha1(signKey, stringToSign).toString('hex'); @@ -63,7 +68,7 @@ function generateCosAuth(secretId, secretKey, method, pathname) { function cosRequest(bucket, region, pathname, method, secretId, secretKey) { return new Promise((resolve, reject) => { const host = `${bucket}.cos.${region}.myqcloud.com`; - const auth = generateCosAuth(secretId, secretKey, method, pathname); + const auth = generateCosAuth(secretId, secretKey, method, pathname, host); const options = { hostname: host, diff --git a/server/age-os/mcp-server/cos.js b/server/age-os/mcp-server/cos.js index 0dd51990..68bda704 100644 --- a/server/age-os/mcp-server/cos.js +++ b/server/age-os/mcp-server/cos.js @@ -19,7 +19,7 @@ const https = require('https'); const COS_CONFIG = { secretId: process.env.ZY_OSS_KEY || '', secretKey: process.env.ZY_OSS_SECRET || '', - region: process.env.ZY_COS_REGION || 'ap-singapore', + region: process.env.ZY_COS_REGION || 'ap-guangzhou', buckets: { hot: process.env.COS_BUCKET_HOT || 'zy-core-bucket-1317346199', cold: process.env.COS_BUCKET_COLD || 'zy-corpus-bucket-1317346199', @@ -117,11 +117,20 @@ async function personaList(personaId, subPrefix, limit) { /** * 发起 COS HTTP 请求 + * + * 修复: 签名时必须将URI路径和查询参数分开处理。 + * 腾讯云COS签名规范要求 HttpURI 不包含查询字符串(query string), + * 否则签名哈希不匹配导致 SignatureDoesNotMatch 错误。 */ function cosRequest(bucketName, objectKey, method, body, contentType) { return new Promise((resolve, reject) => { const host = getBucketHost(bucketName); - const pathname = '/' + objectKey; + const fullPath = '/' + objectKey; + + // 签名时只用纯路径(不含查询参数), HTTP请求用完整路径 + const qIdx = fullPath.indexOf('?'); + const signPathname = qIdx >= 0 ? fullPath.substring(0, qIdx) : fullPath; + const headers = { Host: host, 'Content-Type': contentType || 'application/octet-stream' @@ -129,10 +138,10 @@ function cosRequest(bucketName, objectKey, method, body, contentType) { if (body) { headers['Content-Length'] = Buffer.byteLength(body); } - headers['Authorization'] = generateSignature(method, pathname, host); + headers['Authorization'] = generateSignature(method, signPathname, host); const req = https.request({ - hostname: host, port: 443, path: pathname, method, headers, timeout: 30000 + hostname: host, port: 443, path: fullPath, method, headers, timeout: 30000 }, (res) => { const chunks = []; res.on('data', c => chunks.push(c)); @@ -141,7 +150,7 @@ function cosRequest(bucketName, objectKey, method, body, contentType) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ statusCode: res.statusCode, body: responseBody, headers: res.headers }); } else { - reject(new Error(`COS ${method} ${pathname}: ${res.statusCode} - ${responseBody.substring(0, 200)}`)); + reject(new Error(`COS ${method} ${fullPath}: ${res.statusCode} - ${responseBody.substring(0, 200)}`)); } }); });