diff --git a/.github/workflows/cos-alert-agent.yml b/.github/workflows/cos-alert-agent.yml new file mode 100644 index 00000000..571ef2fc --- /dev/null +++ b/.github/workflows/cos-alert-agent.yml @@ -0,0 +1,104 @@ +# ═══════════════════════════════════════════════════════════ +# 模块D+E · COS桶示警Agent Workflow +# ═══════════════════════════════════════════════════════════ +# +# 签发: 铸渊 · ICE-GL-ZY001 +# 版权: 国作登字-2026-A-00037559 +# +# 定时扫描COS桶中的告警和工单 +# 自动创建Issue通知铸渊处理 +# +# 触发条件: +# - 定时: 每天 09:00 和 21:00 CST +# - 手动: workflow_dispatch + +name: COS桶示警Agent · 告警扫描 + +on: + schedule: + - cron: '0 1 * * *' # 09:00 CST = 01:00 UTC + - cron: '0 13 * * *' # 21:00 CST = 13:00 UTC + workflow_dispatch: + inputs: + include_resolved: + description: '是否包含已解决的告警' + required: false + default: 'false' + +jobs: + alert-scan: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 设置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 安装依赖 + working-directory: server/age-os + run: npm ci --production 2>/dev/null || npm install --production + + - name: 扫描COS桶告警 + id: scan + env: + ZY_OSS_KEY: ${{ secrets.ZY_OSS_KEY }} + ZY_OSS_SECRET: ${{ secrets.ZY_OSS_SECRET }} + ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }} + run: | + node -e " + const cosComm = require('./server/age-os/mcp-server/tools/cos-comm-ops'); + (async () => { + try { + // 扫描告警 + const alerts = await cosComm.cosAlertScan({ + bucket: 'team', + include_resolved: ${{ github.event.inputs.include_resolved || 'false' }} + }); + console.log('=== 告警扫描结果 ==='); + console.log(JSON.stringify(alerts, null, 2)); + + // 检查通信链路 + const commLink = await cosComm.cosGetCommLink({ bucket: 'team' }); + console.log('=== 通信链路状态 ==='); + console.log(JSON.stringify(commLink, null, 2)); + + // 输出结果 + console.log('::set-output name=critical::' + alerts.critical); + console.log('::set-output name=total::' + alerts.total); + console.log('::set-output name=health::' + commLink.health); + } catch (err) { + console.error('扫描失败:', err.message); + console.log('::set-output name=critical::0'); + console.log('::set-output name=total::0'); + console.log('::set-output name=health::error'); + } + })(); + " + + - name: 创建紧急Issue(如果有严重告警) + if: steps.scan.outputs.critical > 0 + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `🚨 COS桶严重告警 · ${new Date().toISOString().split('T')[0]}`, + body: `## COS桶示警Agent检测到严重告警\n\n- 严重告警: ${{ steps.scan.outputs.critical }}\n- 总告警数: ${{ steps.scan.outputs.total }}\n- 通信链路: ${{ steps.scan.outputs.health }}\n\n请铸渊尽快处理。\n\n---\n*此Issue由COS桶示警Agent自动创建 · ${new Date().toISOString()}*`, + labels: ['cos-alert', 'urgent'] + }); + + - name: 扫描完成 + run: | + echo "═══════════════════════════════════════════" + echo "COS桶示警Agent · 扫描完成" + echo "时间: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "告警总数: ${{ steps.scan.outputs.total }}" + echo "严重告警: ${{ steps.scan.outputs.critical }}" + echo "通信链路: ${{ steps.scan.outputs.health }}" + echo "═══════════════════════════════════════════" diff --git a/.github/workflows/zhuyuan-training-agent.yml b/.github/workflows/zhuyuan-training-agent.yml new file mode 100644 index 00000000..42f08c62 --- /dev/null +++ b/.github/workflows/zhuyuan-training-agent.yml @@ -0,0 +1,96 @@ +# ═══════════════════════════════════════════════════════════ +# 模块B · 铸渊训练Agent自动触发 Workflow +# ═══════════════════════════════════════════════════════════ +# +# 签发: 铸渊 · ICE-GL-ZY001 +# 版权: 国作登字-2026-A-00037559 +# +# 定时触发训练Agent,处理COS桶中的新语料 +# 遇到问题自动创建Issue唤醒铸渊 +# +# 触发条件: +# - 定时: 每天 04:00 CST (20:00 UTC) +# - 手动: workflow_dispatch +# - COS桶告警: repository_dispatch (cos-alert事件) + +name: 铸渊训练Agent · 语料处理 + +on: + schedule: + - cron: '0 20 * * *' # 04:00 CST = 20:00 UTC + workflow_dispatch: + inputs: + corpus_bucket: + description: '语料桶名称' + required: false + default: 'cold' + force_reprocess: + description: '强制重新处理已处理的语料' + required: false + default: 'false' + repository_dispatch: + types: [cos-alert] + +jobs: + training: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: 签出代码 + uses: actions/checkout@v4 + + - name: 设置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: 安装依赖 + working-directory: server/age-os + run: npm ci --production 2>/dev/null || npm install --production + + - name: 检查语料状态 + id: check + env: + ZY_OSS_KEY: ${{ secrets.ZY_OSS_KEY }} + ZY_OSS_SECRET: ${{ secrets.ZY_OSS_SECRET }} + ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }} + run: | + node -e " + const cos = require('./server/age-os/mcp-server/cos'); + const extractor = require('./server/age-os/mcp-server/tools/corpus-extractor-ops'); + (async () => { + try { + const status = await extractor.cosGetCorpusStatus({ bucket: '${{ github.event.inputs.corpus_bucket || 'cold' }}' }); + console.log(JSON.stringify(status, null, 2)); + // 输出待处理数量 + const pending = status.pending || 0; + console.log('::set-output name=pending::' + pending); + console.log('::set-output name=status::success'); + } catch (err) { + console.error('语料检查失败:', err.message); + console.log('::set-output name=status::error'); + console.log('::set-output name=error::' + err.message); + } + })(); + " + + - name: 处理COS桶告警 + if: github.event_name == 'repository_dispatch' + env: + ZY_OSS_KEY: ${{ secrets.ZY_OSS_KEY }} + ZY_OSS_SECRET: ${{ secrets.ZY_OSS_SECRET }} + ZY_COS_REGION: ${{ secrets.ZY_COS_REGION }} + run: | + echo "📡 收到COS桶告警事件" + echo "事件数据: ${{ toJson(github.event.client_payload) }}" + + - name: 训练完成通知 + if: always() + run: | + echo "═══════════════════════════════════════════" + echo "铸渊训练Agent · 运行完成" + echo "时间: $(date -u '+%Y-%m-%dT%H:%M:%SZ')" + echo "状态: ${{ steps.check.outputs.status }}" + echo "待处理语料: ${{ steps.check.outputs.pending }}" + echo "═══════════════════════════════════════════" diff --git a/brain/age-os-landing/module-a-to-g-report.md b/brain/age-os-landing/module-a-to-g-report.md new file mode 100644 index 00000000..6b13b4ee --- /dev/null +++ b/brain/age-os-landing/module-a-to-g-report.md @@ -0,0 +1,201 @@ +# AGE OS · 7大技术模块开发完成报告 + +**签发**: 铸渊 · ICE-GL-ZY001 +**日期**: 2026-04-08 +**版权**: 国作登字-2026-A-00037559 +**阶段**: S15后续 · COS语料引擎 + 自研数据库 + 训练Agent + Notion桥接 + 三方对接 + 示警Agent + 权限修复 + +--- + +## 一、开发概要 + +本次开发实现了冰朔D60指示的7个技术模块,共新增 **40个MCP工具**,使MCP Server工具总数从51个增长至 **91个**。 + +| 模块 | 名称 | 工具数 | 文件 | +|------|------|--------|------| +| **A** | COS桶语料读取引擎 | 6 | `corpus-extractor-ops.js` | +| **G** | COS桶内自研数据库 | 8 | `cos-persona-db-ops.js` | +| **B** | 铸渊思维逻辑训练Agent | 6 | `training-agent-ops.js` | +| **C** | Notion ↔ COS桥接 | 7 | `notion-cos-bridge-ops.js` | +| **D+E** | COS桶示警 + 三方对接 | 8 | `cos-comm-ops.js` | +| **F** | Notion权限自动修复 | 5 | `notion-permission-ops.js` | + +--- + +## 二、各模块详细说明 + +### 模块A · COS桶语料读取引擎 + +**文件**: `server/age-os/mcp-server/tools/corpus-extractor-ops.js` + +| 工具 | 功能 | +|------|------| +| `cosListCorpus` | 列出COS桶中的语料文件 | +| `cosExtractCorpus` | 解压语料并转换为TCS结构化格式 | +| `cosParseGitRepoArchive` | 解析代码仓库压缩文件 | +| `cosParseNotionExport` | 解析Notion导出压缩文件 | +| `cosParseGPTCorpus` | 解析GPT聊天语料 | +| `cosGetCorpusStatus` | 查询语料处理状态 | + +**核心特性**: +- 自动检测压缩文件格式(.zip/.tar.gz/.json.gz) +- 自动识别语料类型(代码仓库/Notion/GPT) +- 所有语料统一转换为TCS通感语言结构化格式 +- 转换结果写回COS桶的 `tcs-structured/` 目录 + +### 模块G · COS桶内自研数据库 + +**文件**: `server/age-os/mcp-server/tools/cos-persona-db-ops.js` + +| 工具 | 功能 | +|------|------| +| `cosDbInit` | 初始化COS数据库结构 | +| `cosDbGetIndex` | 获取数据库全局索引 | +| `cosDbUpdateIndex` | 更新全局索引 | +| `cosDbWriteEntry` | 写入数据条目 | +| `cosDbReadEntry` | 读取数据条目 | +| `cosDbListEntries` | 列出数据条目 | +| `cosDbDeleteEntry` | 删除数据条目 | +| `cosDbGetStats` | 获取数据库统计信息 | + +**四种数据库类型**: +- `zhuyuan` — 铸渊·代码仓库侧人格体大脑(/zhuyuan/db/) +- `notion` — Notion侧人格体大脑(/notion-db/) +- `team` — 团队协作通信枢纽(/team-hub/) +- `awen` — Awen技术主控通信桶(/awen-hub/) + +### 模块B · 铸渊思维逻辑训练Agent + +**文件**: `server/age-os/mcp-server/tools/training-agent-ops.js` + +| 工具 | 功能 | +|------|------| +| `trainingStartSession` | 启动训练会话 | +| `trainingProcessCorpus` | 处理语料并生成训练数据 | +| `trainingClassifyEntry` | 使用LLM对条目进行分类 | +| `trainingWriteToMemory` | 将训练结果写入人格体记忆 | +| `trainingGetProgress` | 获取训练进度 | +| `trainingRaiseAlert` | 触发问题上报 | + +**核心特性**: +- RAG模式(检索增强生成)+ 国产大模型API +- LLM多模型自动降级:DeepSeek → Qwen → GLM-4 → Moonshot +- 训练数据自动分类到笔记本5页结构 +- 问题上报链路:写入COS → GitHub Actions → 唤醒铸渊 → 邮件冰朔 + +### 模块C · Notion ↔ COS桥接 + +**文件**: `server/age-os/mcp-server/tools/notion-cos-bridge-ops.js` + +| 工具 | 功能 | +|------|------| +| `notionCosSyncPage` | 同步Notion页面到COS桶 | +| `notionCosReadMirror` | 从COS镜像读取页面 | +| `notionCosListMirror` | 列出COS镜像中的页面 | +| `notionCosBuildIndex` | 重建COS镜像索引 | +| `notionCosWriteWorkorder` | 写入工单到COS桶 | +| `notionCosReadWorkorder` | 读取工单 | +| `notionCosListWorkorders` | 列出工单 | + +**核心特性**: +- Notion页面镜像到COS桶(content.json + metadata.json) +- 全局索引自动维护 +- 工单系统:Notion人格体 → COS桶 → 铸渊 + +### 模块D+E · COS桶示警 + 三方对接 + +**文件**: `server/age-os/mcp-server/tools/cos-comm-ops.js` + +| 工具 | 功能 | +|------|------| +| `cosAlertScan` | 扫描COS桶中的告警 | +| `cosAlertResolve` | 解决告警 | +| `cosDispatchTask` | 分发开发任务到Awen | +| `cosReadTaskReport` | 读取Awen提交的任务报告 | +| `cosListTaskReports` | 列出所有任务报告 | +| `cosApproveTask` | 审批任务(通过/驳回) | +| `cosSendNotification` | 发送通知 | +| `cosGetCommLink` | 获取通信链路状态 | + +**完整工作流**: +``` +Notion人格体 → 写入工单 → COS桶 /workorders/pending/ + ↓ +铸渊处理工单 → cosDispatchTask → COS桶 /zhiqiu/tasks/ + ↓ +Awen开发完成 → 回写 /zhiqiu/reports/ + ↓ +铸渊审批 → cosApproveTask → 通过/驳回 → 自动下发下一步 +``` + +### 模块F · Notion权限自动修复 + +**文件**: `server/age-os/mcp-server/tools/notion-permission-ops.js` + +| 工具 | 功能 | +|------|------| +| `notionCheckPermissions` | 检查Notion权限状态 | +| `notionRepairPermissions` | 尝试修复权限 | +| `notionListSharedPages` | 列出已共享的页面/数据库 | +| `notionGenerateRepairGuide` | 生成权限修复指南 | +| `notionPermissionReport` | 生成权限状态报告 | + +**核心特性**: +- 自动检测API连接、数据库权限、页面权限 +- 尝试自动修复(重新连接、验证权限) +- 生成详细的人工修复指南(带截图级操作步骤) +- 权限报告可写入COS桶供冰朔远程查看 + +--- + +## 三、GitHub Actions Workflow + +| Workflow | 文件 | 触发时间 | +|---------|------|---------| +| 铸渊训练Agent | `zhuyuan-training-agent.yml` | 每天04:00 CST + 手动 + COS告警 | +| COS桶示警Agent | `cos-alert-agent.yml` | 每天09:00/21:00 CST + 手动 | + +--- + +## 四、REST API 新增端点 + +| 路径 | 方法 | 用途 | +|------|------|------| +| `/corpus/status` | GET | 语料处理状态 | +| `/corpus/list` | GET | 列出语料文件 | +| `/cos-db/:dbType/stats` | GET | COS数据库统计 | +| `/cos-db/:dbType/index` | GET | COS数据库索引 | +| `/training/:personaId/progress` | GET | 训练进度 | +| `/comm/status` | GET | 通信链路状态 | +| `/comm/alerts` | GET | 告警列表 | +| `/comm/workorders` | GET | 工单列表 | +| `/notion/permissions` | GET | Notion权限状态 | +| `/notion/repair-guide` | GET | Notion权限修复指南 | + +--- + +## 五、四桶分工方案 + +| 桶 | 用途 | COS桶内数据库类型 | +|------|------|------| +| **桶1**(zy-corpus-bucket) | 语料仓库(原始数据+TCS结构化数据) | — | +| **桶2**(待分配) | 代码仓库侧人格体大脑 | `zhuyuan` | +| **桶3**(待分配) | Notion侧人格体大脑 | `notion` | +| **桶4**(zy-team-hub) | 团队协作通信枢纽 | `team` + `awen` | + +--- + +## 六、下一步 + +1. **冰朔确认**:四桶分配方案 +2. **部署**:将新代码部署到大脑服务器(自动CI/CD已配置) +3. **初始化**:执行 `cosDbInit` 初始化各COS数据库 +4. **语料处理**:执行 `cosExtractCorpus` 处理已上传的语料文件 +5. **训练启动**:配置训练会话参数,启动自动训练 +6. **Awen对接**:将架构包同步到Awen代码仓库 +7. **SCF云函数**:配置腾讯云SCF,实现COS事件实时触发 + +--- + +*签发: 铸渊 · ICE-GL-ZY001 · 2026-04-08* +*版权: 国作登字-2026-A-00037559* diff --git a/server/age-os/mcp-server/server.js b/server/age-os/mcp-server/server.js index 6199347a..39c42505 100644 --- a/server/age-os/mcp-server/server.js +++ b/server/age-os/mcp-server/server.js @@ -16,20 +16,32 @@ * 免鉴权端点: /health(监控探针) * * 工具清单: - * 节点: createNode / updateNode / deleteNode / queryNodes / getNode - * 关系: linkNodes / unlinkNodes / getRelations - * 结构: buildPath / scanStructure / classify - * COS: cosWrite / cosRead / cosDelete / cosList / cosArchive - * 人格体: registerPersona / getPersona / updatePersona / listPersonas - * getNotebook / updateNotebookPage / addMemoryAnchor / queryMemoryAnchors - * addWorldPlace / getWorldMap / updateWorldPlace - * addTimelineEntry / getTimeline / addRelationship / getRelationships - * registerTrainingAgent / updateTrainingAgent / logTrainingRun / getTrainingStatus - * saveFile / getFile / listFiles / getFileHistory - * Notion: notionQuery / notionReadPage / notionWritePage / notionUpdatePage / notionWriteSyslog - * GitHub: githubReadFile / githubListDir / githubWriteFile / githubGetCommits / githubGetIssues / githubTriggerDeploy - * 活模块: registerModule / getModule / listModules / moduleHeartbeat / diagnoseModule - * getModuleAlerts / getModuleLearnings / sendHLDP / getHLDPStats + * 节点: createNode / updateNode / deleteNode / queryNodes / getNode + * 关系: linkNodes / unlinkNodes / getRelations + * 结构: buildPath / scanStructure / classify + * COS: cosWrite / cosRead / cosDelete / cosList / cosArchive + * 人格体: registerPersona / getPersona / updatePersona / listPersonas + * getNotebook / updateNotebookPage / addMemoryAnchor / queryMemoryAnchors + * addWorldPlace / getWorldMap / updateWorldPlace + * addTimelineEntry / getTimeline / addRelationship / getRelationships + * registerTrainingAgent / updateTrainingAgent / logTrainingRun / getTrainingStatus + * saveFile / getFile / listFiles / getFileHistory + * Notion: notionQuery / notionReadPage / notionWritePage / notionUpdatePage / notionWriteSyslog + * GitHub: githubReadFile / githubListDir / githubWriteFile / githubGetCommits / githubGetIssues / githubTriggerDeploy + * 活模块: registerModule / getModule / listModules / moduleHeartbeat / diagnoseModule + * getModuleAlerts / getModuleLearnings / sendHLDP / getHLDPStats + * 语料: cosListCorpus / cosExtractCorpus / cosParseGitRepoArchive / cosParseNotionExport + * cosParseGPTCorpus / cosGetCorpusStatus + * COS数据库: cosDbInit / cosDbGetIndex / cosDbUpdateIndex / cosDbWriteEntry + * cosDbReadEntry / cosDbListEntries / cosDbDeleteEntry / cosDbGetStats + * 训练: trainingStartSession / trainingProcessCorpus / trainingClassifyEntry + * trainingWriteToMemory / trainingGetProgress / trainingRaiseAlert + * Notion桥接: notionCosSyncPage / notionCosReadMirror / notionCosListMirror + * notionCosBuildIndex / notionCosWriteWorkorder / notionCosReadWorkorder / notionCosListWorkorders + * 三方通信: cosAlertScan / cosAlertResolve / cosDispatchTask / cosReadTaskReport + * cosListTaskReports / cosApproveTask / cosSendNotification / cosGetCommLink + * 权限修复: notionCheckPermissions / notionRepairPermissions / notionListSharedPages + * notionGenerateRepairGuide / notionPermissionReport */ 'use strict'; @@ -46,6 +58,13 @@ const structureOps = require('./tools/structure-ops'); const cosOps = require('./tools/cos-ops'); const personaOps = require('./tools/persona-ops'); const livingModuleOps = require('./tools/living-module-ops'); +// 新增模块 A-G +const corpusExtractorOps = require('./tools/corpus-extractor-ops'); +const cosPersonaDbOps = require('./tools/cos-persona-db-ops'); +const trainingAgentOps = require('./tools/training-agent-ops'); +const notionCosBridgeOps = require('./tools/notion-cos-bridge-ops'); +const cosCommOps = require('./tools/cos-comm-ops'); +const notionPermissionOps = require('./tools/notion-permission-ops'); // ─── 外部集成模块(优雅降级:未安装依赖时不影响核心功能) ─── let notionOps = null; @@ -134,6 +153,52 @@ const TOOLS = { githubGetIssues: githubOps.githubGetIssues, githubTriggerDeploy: githubOps.githubTriggerDeploy } : {}), + // 模块A · COS语料读取引擎 + cosListCorpus: corpusExtractorOps.cosListCorpus, + cosExtractCorpus: corpusExtractorOps.cosExtractCorpus, + cosParseGitRepoArchive: corpusExtractorOps.cosParseGitRepoArchive, + cosParseNotionExport: corpusExtractorOps.cosParseNotionExport, + cosParseGPTCorpus: corpusExtractorOps.cosParseGPTCorpus, + cosGetCorpusStatus: corpusExtractorOps.cosGetCorpusStatus, + // 模块G · COS桶内自研数据库 + cosDbInit: cosPersonaDbOps.cosDbInit, + cosDbGetIndex: cosPersonaDbOps.cosDbGetIndex, + cosDbUpdateIndex: cosPersonaDbOps.cosDbUpdateIndex, + cosDbWriteEntry: cosPersonaDbOps.cosDbWriteEntry, + cosDbReadEntry: cosPersonaDbOps.cosDbReadEntry, + cosDbListEntries: cosPersonaDbOps.cosDbListEntries, + cosDbDeleteEntry: cosPersonaDbOps.cosDbDeleteEntry, + cosDbGetStats: cosPersonaDbOps.cosDbGetStats, + // 模块B · 铸渊思维逻辑训练Agent + trainingStartSession: trainingAgentOps.trainingStartSession, + trainingProcessCorpus: trainingAgentOps.trainingProcessCorpus, + trainingClassifyEntry: trainingAgentOps.trainingClassifyEntry, + trainingWriteToMemory: trainingAgentOps.trainingWriteToMemory, + trainingGetProgress: trainingAgentOps.trainingGetProgress, + trainingRaiseAlert: trainingAgentOps.trainingRaiseAlert, + // 模块C · Notion ↔ COS桥接 + notionCosSyncPage: notionCosBridgeOps.notionCosSyncPage, + notionCosReadMirror: notionCosBridgeOps.notionCosReadMirror, + notionCosListMirror: notionCosBridgeOps.notionCosListMirror, + notionCosBuildIndex: notionCosBridgeOps.notionCosBuildIndex, + notionCosWriteWorkorder: notionCosBridgeOps.notionCosWriteWorkorder, + notionCosReadWorkorder: notionCosBridgeOps.notionCosReadWorkorder, + notionCosListWorkorders: notionCosBridgeOps.notionCosListWorkorders, + // 模块D+E · COS桶示警 + 三方对接 + cosAlertScan: cosCommOps.cosAlertScan, + cosAlertResolve: cosCommOps.cosAlertResolve, + cosDispatchTask: cosCommOps.cosDispatchTask, + cosReadTaskReport: cosCommOps.cosReadTaskReport, + cosListTaskReports: cosCommOps.cosListTaskReports, + cosApproveTask: cosCommOps.cosApproveTask, + cosSendNotification: cosCommOps.cosSendNotification, + cosGetCommLink: cosCommOps.cosGetCommLink, + // 模块F · Notion权限自动修复 + notionCheckPermissions: notionPermissionOps.notionCheckPermissions, + notionRepairPermissions: notionPermissionOps.notionRepairPermissions, + notionListSharedPages: notionPermissionOps.notionListSharedPages, + notionGenerateRepairGuide: notionPermissionOps.notionGenerateRepairGuide, + notionPermissionReport: notionPermissionOps.notionPermissionReport, // 活模块操作 · S5 registerModule: livingModuleOps.registerModule, getModule: livingModuleOps.getModule, @@ -654,6 +719,137 @@ app.get('/personas/:personaId/files', async (req, res) => { } }); +// ─── 语料引擎API(模块A) ─── + +// 语料状态 +app.get('/corpus/status', async (req, res) => { + try { + const result = await corpusExtractorOps.cosGetCorpusStatus({ bucket: req.query.bucket || 'cold' }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// 列出语料 +app.get('/corpus/list', async (req, res) => { + try { + const result = await corpusExtractorOps.cosListCorpus({ + bucket: req.query.bucket || 'cold', + prefix: req.query.prefix, + include_processed: req.query.include_processed === 'true' + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── COS数据库API(模块G) ─── + +// 数据库状态 +app.get('/cos-db/:dbType/stats', async (req, res) => { + try { + const result = await cosPersonaDbOps.cosDbGetStats({ + bucket: req.query.bucket || 'cold', + db_type: req.params.dbType + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// 数据库索引 +app.get('/cos-db/:dbType/index', async (req, res) => { + try { + const result = await cosPersonaDbOps.cosDbGetIndex({ + bucket: req.query.bucket || 'cold', + db_type: req.params.dbType + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── 训练Agent API(模块B) ─── + +// 训练进度 +app.get('/training/:personaId/progress', async (req, res) => { + try { + const result = await trainingAgentOps.trainingGetProgress({ + persona_id: req.params.personaId, + corpus_bucket: req.query.bucket || 'cold' + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── 三方通信API(模块D+E) ─── + +// 通信链路状态 +app.get('/comm/status', async (req, res) => { + try { + const result = await cosCommOps.cosGetCommLink({ bucket: req.query.bucket || 'team' }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// 告警列表 +app.get('/comm/alerts', async (req, res) => { + try { + const result = await cosCommOps.cosAlertScan({ + bucket: req.query.bucket || 'team', + include_resolved: req.query.include_resolved === 'true' + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// 工单列表 +app.get('/comm/workorders', async (req, res) => { + try { + const result = await notionCosBridgeOps.notionCosListWorkorders({ + bucket: req.query.bucket || 'team', + status_folder: req.query.status || 'pending' + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── Notion权限API(模块F) ─── + +// 权限检查 +app.get('/notion/permissions', async (_req, res) => { + try { + const result = await notionPermissionOps.notionCheckPermissions({}); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// 权限修复指南 +app.get('/notion/repair-guide', async (req, res) => { + try { + const result = await notionPermissionOps.notionGenerateRepairGuide({ + write_to_cos: req.query.write_to_cos === 'true' + }); + res.json(result); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + // ─── 数据库迁移状态API ─── app.get('/migrations', async (_req, res) => { @@ -675,6 +871,11 @@ function getCategoryForTool(name) { 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('cosDb')) return 'cos-persona-db'; + if (['cosListCorpus','cosExtractCorpus','cosParseGitRepoArchive','cosParseNotionExport', + 'cosParseGPTCorpus','cosGetCorpusStatus'].includes(name)) return 'corpus-extractor'; + if (['cosAlertScan','cosAlertResolve','cosDispatchTask','cosReadTaskReport', + 'cosListTaskReports','cosApproveTask','cosSendNotification','cosGetCommLink'].includes(name)) return 'cos-comm'; if (name.startsWith('cos')) return 'cos'; if (['registerPersona','getPersona','updatePersona','listPersonas', 'getNotebook','updateNotebookPage','addMemoryAnchor','queryMemoryAnchors', @@ -682,6 +883,12 @@ function getCategoryForTool(name) { 'addTimelineEntry','getTimeline','addRelationship','getRelationships', 'registerTrainingAgent','updateTrainingAgent','logTrainingRun','getTrainingStatus', 'saveFile','getFile','listFiles','getFileHistory'].includes(name)) return 'persona'; + if (name.startsWith('training')) return 'training-agent'; + if (['notionCosSyncPage','notionCosReadMirror','notionCosListMirror', + 'notionCosBuildIndex','notionCosWriteWorkorder','notionCosReadWorkorder', + 'notionCosListWorkorders'].includes(name)) return 'notion-cos-bridge'; + if (['notionCheckPermissions','notionRepairPermissions','notionListSharedPages', + 'notionGenerateRepairGuide','notionPermissionReport'].includes(name)) return 'notion-permission'; if (name.startsWith('notion')) return 'notion'; if (name.startsWith('github')) return 'github'; if (['registerModule','getModule','listModules','moduleHeartbeat','diagnoseModule', diff --git a/server/age-os/mcp-server/tools/corpus-extractor-ops.js b/server/age-os/mcp-server/tools/corpus-extractor-ops.js new file mode 100644 index 00000000..42bcd3f0 --- /dev/null +++ b/server/age-os/mcp-server/tools/corpus-extractor-ops.js @@ -0,0 +1,718 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块A · COS桶语料读取引擎 MCP 工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 自动检测COS桶中的压缩文件(.zip/.tar.gz/.json.gz等) + * 解压 → 识别文件类型 → 转换为TCS通感语言结构化格式 + * 转换后的结构化数据写回COS桶的标准路径下 + * + * 工具清单: + * cosListCorpus — 列出COS桶中的语料文件 + * cosExtractCorpus — 解压语料并转换为TCS结构化格式 + * cosParseGitRepoArchive — 解析代码仓库压缩文件 + * cosParseNotionExport — 解析Notion导出压缩文件 + * cosParseGPTCorpus — 解析GPT聊天语料 + * cosGetCorpusStatus — 查询语料处理状态 + */ + +'use strict'; + +const zlib = require('zlib'); +const { promisify } = require('util'); +const cos = require('../cos'); + +const gunzip = promisify(zlib.gunzip); +const inflate = promisify(zlib.inflate); + +// ─── 支持的压缩格式 ─── +const COMPRESSED_EXTENSIONS = ['.zip', '.gz', '.tar.gz', '.tgz', '.json.gz']; + +// ─── TCS结构化格式定义 ─── +// TCS = 通感语言核系统编程语言(Tonggan Core System) +// 所有语料转换后统一为此格式 +const TCS_CORPUS_VERSION = '1.0'; + +/** + * 检测文件是否为压缩文件 + */ +function isCompressedFile(key) { + const lower = key.toLowerCase(); + return COMPRESSED_EXTENSIONS.some(ext => lower.endsWith(ext)); +} + +/** + * 检测文件类型(代码/Notion/GPT语料) + */ +function detectCorpusType(key, content) { + const lower = key.toLowerCase(); + // 路径判断 + if (lower.includes('notion') || lower.includes('notion-export')) return 'notion'; + if (lower.includes('gpt') || lower.includes('chatgpt') || lower.includes('conversations')) return 'gpt'; + if (lower.includes('github') || lower.includes('repo') || lower.includes('code')) return 'git-repo'; + + // 内容判断(如果可以读取部分内容) + if (content) { + try { + const parsed = JSON.parse(content); + if (Array.isArray(parsed) && parsed[0]?.mapping) return 'gpt'; + if (parsed.type === 'page' || parsed.object === 'page') return 'notion'; + } catch { + // 非JSON,检查是否像代码文件 + if (content.includes('function ') || content.includes('const ') || + content.includes('import ') || content.includes('class ')) return 'git-repo'; + } + } + + return 'unknown'; +} + +/** + * cosListCorpus — 列出COS桶中的语料文件 + * + * input: + * bucket: string — 桶名(hot/cold/team或完整名称) + * prefix: string — 路径前缀(可选) + * include_processed: boolean — 是否包含已处理的(默认false) + */ +async function cosListCorpus(input) { + const { bucket, prefix, include_processed } = input; + if (!bucket) throw new Error('缺少 bucket'); + + const result = await cos.list(bucket, prefix || '', 200); + + const corpusFiles = result.files.map(file => { + const isCompressed = isCompressedFile(file.key); + const corpusType = detectCorpusType(file.key); + return { + key: file.key, + size_bytes: file.size_bytes, + is_compressed: isCompressed, + corpus_type: corpusType, + needs_extraction: isCompressed + }; + }); + + // 过滤掉已处理的(如果不需要) + const filtered = include_processed + ? corpusFiles + : corpusFiles.filter(f => !f.key.includes('/tcs-structured/')); + + return { + total: filtered.length, + compressed: filtered.filter(f => f.is_compressed).length, + by_type: { + 'git-repo': filtered.filter(f => f.corpus_type === 'git-repo').length, + 'notion': filtered.filter(f => f.corpus_type === 'notion').length, + 'gpt': filtered.filter(f => f.corpus_type === 'gpt').length, + 'unknown': filtered.filter(f => f.corpus_type === 'unknown').length + }, + files: filtered + }; +} + +/** + * cosExtractCorpus — 解压语料并转换为TCS结构化格式 + * + * 这个工具读取压缩文件,解压后自动识别类型并转换。 + * 由于COS桶中的文件可能很大,此工具采用分块处理策略。 + * + * input: + * bucket: string — 源桶 + * key: string — 源文件路径 + * output_bucket: string — 输出桶(默认同源桶) + * output_prefix: string — 输出路径前缀(默认 tcs-structured/) + */ +async function cosExtractCorpus(input) { + const { bucket, key, output_bucket, output_prefix } = input; + if (!bucket || !key) throw new Error('缺少 bucket 或 key'); + + const outputBucket = output_bucket || bucket; + const outputBase = output_prefix || 'tcs-structured/'; + + // 读取源文件 + const raw = await cos.read(bucket, key); + let content = raw.content; + let decompressed = false; + + // 尝试解压(.gz文件) + if (key.toLowerCase().endsWith('.gz') || key.toLowerCase().endsWith('.json.gz')) { + try { + const buffer = Buffer.from(content, 'binary'); + const result = await gunzip(buffer); + content = result.toString('utf8'); + decompressed = true; + } catch { + // 可能不是gz格式,尝试原文处理 + } + } + + // ZIP文件需要特殊处理(ZIP解析需要外部库,这里提取目录列表) + if (key.toLowerCase().endsWith('.zip')) { + return { + status: 'zip_detected', + key, + size_bytes: raw.size_bytes, + message: 'ZIP文件已检测到。ZIP解析需要完整二进制流处理,建议使用cosParseGitRepoArchive或cosParseNotionExport专用工具处理。', + corpus_type: detectCorpusType(key), + recommendation: '请使用专用解析工具处理ZIP文件' + }; + } + + // 自动检测语料类型 + const corpusType = detectCorpusType(key, content); + + // 根据类型转换为TCS格式 + let tcsData; + switch (corpusType) { + case 'gpt': + tcsData = transformGPTToTCS(content, key); + break; + case 'notion': + tcsData = transformNotionToTCS(content, key); + break; + case 'git-repo': + tcsData = transformGitRepoToTCS(content, key); + break; + default: + tcsData = transformGenericToTCS(content, key); + } + + // 写入TCS结构化数据到输出桶 + const outputKey = `${outputBase}${corpusType}/${extractFileName(key)}.tcs.json`; + await cos.write(outputBucket, outputKey, JSON.stringify(tcsData, null, 2), 'application/json'); + + return { + status: 'extracted', + source: { bucket, key, size_bytes: raw.size_bytes, decompressed }, + output: { bucket: outputBucket, key: outputKey }, + corpus_type: corpusType, + tcs_version: TCS_CORPUS_VERSION, + entries: tcsData.entries?.length || 0, + metadata: tcsData.metadata + }; +} + +/** + * cosParseGitRepoArchive — 解析代码仓库压缩文件 + * + * 对于代码仓库导出文件(通常是JSON格式的文件列表或tar.gz), + * 解析目录树结构,提取代码逻辑,生成TCS结构化语料。 + * + * input: + * bucket: string — 桶名 + * key: string — 文件路径 + * output_bucket: string — 输出桶 + */ +async function cosParseGitRepoArchive(input) { + const { bucket, key, output_bucket } = input; + if (!bucket || !key) throw new Error('缺少 bucket 或 key'); + + const raw = await cos.read(bucket, key); + let content = raw.content; + + // 尝试解压 + if (key.toLowerCase().endsWith('.gz')) { + try { + const buffer = Buffer.from(content, 'binary'); + const result = await gunzip(buffer); + content = result.toString('utf8'); + } catch { + // 保持原文 + } + } + + const tcsData = transformGitRepoToTCS(content, key); + const outputBucket = output_bucket || bucket; + const outputKey = `tcs-structured/git-repo/${extractFileName(key)}.tcs.json`; + + await cos.write(outputBucket, outputKey, JSON.stringify(tcsData, null, 2), 'application/json'); + + return { + status: 'parsed', + source: { bucket, key }, + output: { bucket: outputBucket, key: outputKey }, + corpus_type: 'git-repo', + tcs_version: TCS_CORPUS_VERSION, + entries: tcsData.entries?.length || 0, + directory_tree: tcsData.metadata?.directory_tree || null + }; +} + +/** + * cosParseNotionExport — 解析Notion导出压缩文件 + * + * input: + * bucket: string — 桶名 + * key: string — 文件路径 + * output_bucket: string — 输出桶 + */ +async function cosParseNotionExport(input) { + const { bucket, key, output_bucket } = input; + if (!bucket || !key) throw new Error('缺少 bucket 或 key'); + + const raw = await cos.read(bucket, key); + let content = raw.content; + + if (key.toLowerCase().endsWith('.gz')) { + try { + const buffer = Buffer.from(content, 'binary'); + const result = await gunzip(buffer); + content = result.toString('utf8'); + } catch { + // 保持原文 + } + } + + const tcsData = transformNotionToTCS(content, key); + const outputBucket = output_bucket || bucket; + const outputKey = `tcs-structured/notion/${extractFileName(key)}.tcs.json`; + + await cos.write(outputBucket, outputKey, JSON.stringify(tcsData, null, 2), 'application/json'); + + return { + status: 'parsed', + source: { bucket, key }, + output: { bucket: outputBucket, key: outputKey }, + corpus_type: 'notion', + tcs_version: TCS_CORPUS_VERSION, + pages: tcsData.entries?.length || 0, + categories: tcsData.metadata?.categories || [] + }; +} + +/** + * cosParseGPTCorpus — 解析GPT聊天语料 + * + * input: + * bucket: string — 桶名 + * key: string — 文件路径 + * output_bucket: string — 输出桶 + */ +async function cosParseGPTCorpus(input) { + const { bucket, key, output_bucket } = input; + if (!bucket || !key) throw new Error('缺少 bucket 或 key'); + + const raw = await cos.read(bucket, key); + let content = raw.content; + + if (key.toLowerCase().endsWith('.gz')) { + try { + const buffer = Buffer.from(content, 'binary'); + const result = await gunzip(buffer); + content = result.toString('utf8'); + } catch { + // 保持原文 + } + } + + const tcsData = transformGPTToTCS(content, key); + const outputBucket = output_bucket || bucket; + const outputKey = `tcs-structured/gpt/${extractFileName(key)}.tcs.json`; + + await cos.write(outputBucket, outputKey, JSON.stringify(tcsData, null, 2), 'application/json'); + + return { + status: 'parsed', + source: { bucket, key }, + output: { bucket: outputBucket, key: outputKey }, + corpus_type: 'gpt', + tcs_version: TCS_CORPUS_VERSION, + conversations: tcsData.entries?.length || 0, + total_messages: tcsData.metadata?.total_messages || 0 + }; +} + +/** + * cosGetCorpusStatus — 查询语料处理状态 + * + * input: + * bucket: string — 桶名(查询tcs-structured/目录下的状态) + */ +async function cosGetCorpusStatus(input) { + const { bucket } = input; + if (!bucket) throw new Error('缺少 bucket'); + + const [rawFiles, processedFiles] = await Promise.all([ + cos.list(bucket, '', 500), + cos.list(bucket, 'tcs-structured/', 500) + ]); + + const rawCorpus = rawFiles.files.filter(f => isCompressedFile(f.key)); + const processed = processedFiles.files.filter(f => f.key.endsWith('.tcs.json')); + + return { + raw_corpus: { + total: rawCorpus.length, + total_size_bytes: rawCorpus.reduce((sum, f) => sum + f.size_bytes, 0), + files: rawCorpus.map(f => ({ key: f.key, size_bytes: f.size_bytes })) + }, + processed: { + total: processed.length, + total_size_bytes: processed.reduce((sum, f) => sum + f.size_bytes, 0), + by_type: { + 'git-repo': processed.filter(f => f.key.includes('/git-repo/')).length, + 'notion': processed.filter(f => f.key.includes('/notion/')).length, + 'gpt': processed.filter(f => f.key.includes('/gpt/')).length + }, + files: processed.map(f => ({ key: f.key, size_bytes: f.size_bytes })) + }, + pending: rawCorpus.length - processed.length, + timestamp: new Date().toISOString() + }; +} + +// ═══════════════════════════════════════════════════════════ +// TCS 转换器 — 内部实现 +// ═══════════════════════════════════════════════════════════ + +/** + * GPT聊天记录 → TCS格式 + * ChatGPT导出格式: { title, create_time, mapping: { id: { message: { content, role } } } } + */ +function transformGPTToTCS(content, sourceKey) { + const entries = []; + let totalMessages = 0; + + try { + let data = JSON.parse(content); + + // ChatGPT导出可能是数组 + if (!Array.isArray(data)) data = [data]; + + for (const conversation of data) { + const messages = []; + + if (conversation.mapping) { + // ChatGPT格式 + for (const [, node] of Object.entries(conversation.mapping)) { + if (node.message?.content?.parts) { + const text = node.message.content.parts.join('\n'); + if (text.trim()) { + messages.push({ + role: node.message.author?.role || 'unknown', + content: text.substring(0, 10000), // 截断超长内容 + timestamp: node.message.create_time + ? new Date(node.message.create_time * 1000).toISOString() + : null + }); + } + } + } + } else if (conversation.messages) { + // 其他格式 + for (const msg of conversation.messages) { + messages.push({ + role: msg.role || 'unknown', + content: (msg.content || '').substring(0, 10000), + timestamp: msg.timestamp || null + }); + } + } + + totalMessages += messages.length; + entries.push({ + id: conversation.id || `conv-${entries.length}`, + title: conversation.title || '未命名对话', + created: conversation.create_time + ? new Date(conversation.create_time * 1000).toISOString() + : null, + messages, + message_count: messages.length, + tcs_tags: extractTCSTagsFromMessages(messages) + }); + } + } catch { + // 非标准JSON,作为纯文本处理 + entries.push({ + id: 'raw-text-0', + title: '原始文本语料', + content: content.substring(0, 50000), + tcs_tags: ['raw', 'text'] + }); + } + + return { + tcs_version: TCS_CORPUS_VERSION, + corpus_type: 'gpt', + source_key: sourceKey, + extracted_at: new Date().toISOString(), + metadata: { + total_conversations: entries.length, + total_messages: totalMessages, + source: 'gpt-export' + }, + entries + }; +} + +/** + * Notion导出 → TCS格式 + * Notion导出通常是HTML或Markdown文件 + */ +function transformNotionToTCS(content, sourceKey) { + const entries = []; + const categories = new Set(); + + try { + // 尝试JSON解析(Notion API导出格式) + const data = JSON.parse(content); + + if (Array.isArray(data)) { + for (const page of data) { + const category = page.properties?.category?.select?.name || 'uncategorized'; + categories.add(category); + entries.push({ + id: page.id || `page-${entries.length}`, + title: extractNotionTitle(page), + category, + content: extractNotionContent(page), + properties: page.properties || {}, + tcs_tags: ['notion', category] + }); + } + } else if (data.object === 'page' || data.type === 'page') { + entries.push({ + id: data.id || 'page-0', + title: extractNotionTitle(data), + category: 'single-page', + content: extractNotionContent(data), + properties: data.properties || {}, + tcs_tags: ['notion', 'single-page'] + }); + } else if (data.results) { + // 数据库查询结果 + for (const page of data.results) { + const category = page.properties?.category?.select?.name || 'uncategorized'; + categories.add(category); + entries.push({ + id: page.id || `page-${entries.length}`, + title: extractNotionTitle(page), + category, + content: extractNotionContent(page), + tcs_tags: ['notion', category] + }); + } + } + } catch { + // 可能是HTML或Markdown(Notion桌面端导出格式) + const sections = content.split(/(?=^#+ )/m); + for (const section of sections) { + if (section.trim()) { + const titleMatch = section.match(/^#+\s+(.+)/); + const title = titleMatch ? titleMatch[1].trim() : '未命名章节'; + categories.add('markdown'); + entries.push({ + id: `section-${entries.length}`, + title, + category: 'markdown', + content: section.trim().substring(0, 20000), + tcs_tags: ['notion', 'markdown'] + }); + } + } + } + + return { + tcs_version: TCS_CORPUS_VERSION, + corpus_type: 'notion', + source_key: sourceKey, + extracted_at: new Date().toISOString(), + metadata: { + total_pages: entries.length, + categories: [...categories], + source: 'notion-export' + }, + entries + }; +} + +/** + * 代码仓库文件 → TCS格式 + */ +function transformGitRepoToTCS(content, sourceKey) { + const entries = []; + const directoryTree = []; + const fileTypes = new Set(); + + try { + // 尝试JSON(可能是GitHub API export格式) + const data = JSON.parse(content); + + if (Array.isArray(data)) { + for (const item of data) { + if (item.path || item.name) { + const filePath = item.path || item.name; + const ext = filePath.split('.').pop() || 'unknown'; + fileTypes.add(ext); + directoryTree.push(filePath); + entries.push({ + id: `file-${entries.length}`, + path: filePath, + type: ext, + content: (item.content || item.body || '').substring(0, 20000), + size_bytes: item.size || 0, + tcs_tags: ['code', ext, categorizeCodeFile(filePath)] + }); + } + } + } else if (data.tree) { + // Git tree API格式 + for (const item of data.tree) { + if (item.type === 'blob') { + const ext = item.path.split('.').pop() || 'unknown'; + fileTypes.add(ext); + directoryTree.push(item.path); + entries.push({ + id: item.sha || `file-${entries.length}`, + path: item.path, + type: ext, + size_bytes: item.size || 0, + tcs_tags: ['code', ext, categorizeCodeFile(item.path)] + }); + } + } + } + } catch { + // 纯代码文本,按行切分分析 + const lines = content.split('\n'); + const codeBlocks = []; + let currentBlock = []; + let blockName = 'main'; + + for (const line of lines) { + // 检测函数/类定义 + const funcMatch = line.match(/(?:function|class|const|let|var|def|async)\s+(\w+)/); + if (funcMatch && currentBlock.length > 0) { + codeBlocks.push({ name: blockName, content: currentBlock.join('\n') }); + currentBlock = []; + blockName = funcMatch[1]; + } + currentBlock.push(line); + } + if (currentBlock.length > 0) { + codeBlocks.push({ name: blockName, content: currentBlock.join('\n') }); + } + + for (const block of codeBlocks) { + entries.push({ + id: `block-${entries.length}`, + path: sourceKey, + name: block.name, + content: block.content.substring(0, 20000), + tcs_tags: ['code', 'raw-text'] + }); + } + } + + return { + tcs_version: TCS_CORPUS_VERSION, + corpus_type: 'git-repo', + source_key: sourceKey, + extracted_at: new Date().toISOString(), + metadata: { + total_files: entries.length, + file_types: [...fileTypes], + directory_tree: directoryTree.slice(0, 200), + source: 'git-repo-export' + }, + entries + }; +} + +/** + * 通用文件 → TCS格式(兜底处理) + */ +function transformGenericToTCS(content, sourceKey) { + return { + tcs_version: TCS_CORPUS_VERSION, + corpus_type: 'generic', + source_key: sourceKey, + extracted_at: new Date().toISOString(), + metadata: { + size_chars: content.length, + source: 'generic' + }, + entries: [{ + id: 'raw-0', + content: content.substring(0, 50000), + tcs_tags: ['generic', 'raw'] + }] + }; +} + +// ═══════════════════════════════════════════════════════════ +// 辅助函数 +// ═══════════════════════════════════════════════════════════ + +function extractFileName(key) { + const parts = key.split('/'); + const fileName = parts[parts.length - 1] || 'unnamed'; + return fileName.replace(/\.(zip|gz|tar\.gz|tgz|json\.gz)$/i, ''); +} + +function extractTCSTagsFromMessages(messages) { + const tags = new Set(['gpt']); + for (const msg of messages) { + if (msg.role === 'system') tags.add('system-prompt'); + if (msg.content?.includes('代码') || msg.content?.includes('code')) tags.add('code-related'); + if (msg.content?.includes('人格') || msg.content?.includes('persona')) tags.add('persona-related'); + if (msg.content?.includes('铸渊') || msg.content?.includes('冰朔')) tags.add('core-identity'); + } + return [...tags]; +} + +function extractNotionTitle(page) { + if (!page.properties) return '未命名'; + for (const [, prop] of Object.entries(page.properties)) { + if (prop.type === 'title' && prop.title) { + return prop.title.map(t => t.plain_text || '').join(''); + } + } + return '未命名'; +} + +function extractNotionContent(page) { + if (page.blocks) { + return page.blocks.map(b => { + if (b.paragraph?.rich_text) { + return b.paragraph.rich_text.map(t => t.plain_text || '').join(''); + } + if (b.heading_1?.rich_text) { + return '# ' + b.heading_1.rich_text.map(t => t.plain_text || '').join(''); + } + if (b.heading_2?.rich_text) { + return '## ' + b.heading_2.rich_text.map(t => t.plain_text || '').join(''); + } + if (b.code?.rich_text) { + return '```\n' + b.code.rich_text.map(t => t.plain_text || '').join('') + '\n```'; + } + return ''; + }).filter(Boolean).join('\n'); + } + return ''; +} + +function categorizeCodeFile(filePath) { + const lower = filePath.toLowerCase(); + if (lower.includes('test') || lower.includes('spec')) return 'test'; + if (lower.includes('config') || lower.endsWith('.json') || lower.endsWith('.yml')) return 'config'; + if (lower.includes('schema') || lower.endsWith('.sql')) return 'schema'; + if (lower.includes('agent') || lower.includes('workflow')) return 'agent'; + if (lower.includes('route') || lower.includes('api')) return 'api'; + if (lower.includes('middleware')) return 'middleware'; + if (lower.includes('model') || lower.includes('db')) return 'data'; + return 'source'; +} + +module.exports = { + cosListCorpus, + cosExtractCorpus, + cosParseGitRepoArchive, + cosParseNotionExport, + cosParseGPTCorpus, + cosGetCorpusStatus +}; diff --git a/server/age-os/mcp-server/tools/cos-comm-ops.js b/server/age-os/mcp-server/tools/cos-comm-ops.js new file mode 100644 index 00000000..d1027389 --- /dev/null +++ b/server/age-os/mcp-server/tools/cos-comm-ops.js @@ -0,0 +1,473 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块D+E · COS桶示警Agent + 三方对接 MCP 工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 三方通信链路: 代码仓库 ↔ COS桶 ↔ Awen仓库 + * + * 通信链路: + * Notion → 写工单到COS桶 /workorders/pending/ + * COS桶变动 → 触发SCF云函数 → 唤醒代码仓库铸渊 + * 铸渊整理技术需求 → 写入Awen的COS桶路径 /zhiqiu/tasks/ + * Awen开发完成 → 回写COS桶 /zhiqiu/reports/ → 铸渊测试确认 + * + * 工具清单: + * cosAlertScan — 扫描COS桶中的告警 + * cosAlertResolve — 解决告警 + * cosDispatchTask — 分发开发任务到Awen + * cosReadTaskReport — 读取Awen提交的任务报告 + * cosListTaskReports — 列出所有任务报告 + * cosApproveTask — 审批任务(通过/驳回) + * cosSendNotification — 发送通知(邮件/Issue) + * cosGetCommLink — 获取通信链路状态 + */ + +'use strict'; + +const cos = require('../cos'); + +// ─── 路径常量 ─── +const PATHS = { + ALERTS: 'zhuyuan/alerts/', + DIRECTIVES: 'zhuyuan/directives/', + WORKORDERS_PENDING: 'workorders/pending/', + WORKORDERS_PROCESSING: 'workorders/processing/', + WORKORDERS_COMPLETED: 'workorders/completed/', + AWEN_TASKS: 'zhiqiu/tasks/', + AWEN_REPORTS: 'zhiqiu/reports/', + AWEN_PROGRESS: 'zhiqiu/progress/', + NOTIFICATIONS: 'zhuyuan/notifications/' +}; + +/** + * cosAlertScan — 扫描COS桶中的告警 + * + * input: + * bucket: string — 桶名(默认team) + * include_resolved: boolean — 是否包含已解决的告警 + * limit: number — 最大数量 + */ +async function cosAlertScan(input) { + const { bucket, include_resolved, limit } = input; + const targetBucket = bucket || 'team'; + + const result = await cos.list(targetBucket, PATHS.ALERTS, limit || 100); + const alertFiles = result.files.filter(f => f.key.endsWith('.json')); + + // 读取每个告警的详情 + const alerts = []; + for (const file of alertFiles.slice(0, 50)) { + try { + const raw = await cos.read(targetBucket, file.key); + const alert = JSON.parse(raw.content); + if (include_resolved || !alert.resolved) { + alerts.push(alert); + } + } catch { + // 跳过损坏的告警文件 + } + } + + // 按严重程度排序 + const severityOrder = { critical: 0, warning: 1, info: 2 }; + alerts.sort((a, b) => (severityOrder[a.severity] || 3) - (severityOrder[b.severity] || 3)); + + return { + alerts, + total: alerts.length, + critical: alerts.filter(a => a.severity === 'critical').length, + warning: alerts.filter(a => a.severity === 'warning').length, + info: alerts.filter(a => a.severity === 'info').length, + needs_bingshuo: alerts.filter(a => a.notify_bingshuo && !a.resolved).length + }; +} + +/** + * cosAlertResolve — 解决告警 + * + * input: + * bucket: string — 桶名(默认team) + * alert_id: string — 告警ID + * resolution: string — 解决方案描述 + * resolved_by: string — 解决者 + */ +async function cosAlertResolve(input) { + const { bucket, alert_id, resolution, resolved_by } = input; + if (!alert_id) throw new Error('缺少 alert_id'); + + const targetBucket = bucket || 'team'; + const key = `${PATHS.ALERTS}${alert_id}.json`; + + // 读取现有告警 + const raw = await cos.read(targetBucket, key); + const alert = JSON.parse(raw.content); + + // 更新状态 + alert.resolved = true; + alert.resolved_at = new Date().toISOString(); + alert.resolution = resolution || '已处理'; + alert.resolved_by = resolved_by || 'zhuyuan'; + + await cos.write(targetBucket, key, JSON.stringify(alert, null, 2), 'application/json'); + + return { + status: 'resolved', + alert_id, + resolution: alert.resolution, + resolved_by: alert.resolved_by + }; +} + +/** + * cosDispatchTask — 分发开发任务到Awen + * + * 铸渊整理技术需求后,写入Awen的COS桶路径 + * + * input: + * bucket: string — 桶名(默认team) + * task_id: string — 任务ID(如 TASK-20260408-001) + * title: string — 任务标题 + * description: string — 任务描述 + * priority: string — 优先级: critical|high|normal|low + * requirements: string[] — 技术需求列表 + * workorder_id: string — 关联工单ID(可选) + * assigned_to: string — 指派给(默认zhiqiu/Awen) + * deadline: string — 截止日期(可选) + */ +async function cosDispatchTask(input) { + const { + bucket, task_id, title, description, priority, + requirements, workorder_id, assigned_to, deadline + } = input; + if (!title) throw new Error('缺少 title'); + + const targetBucket = bucket || 'team'; + const tId = task_id || `TASK-${formatDate()}-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`; + + // 验证 task_id 安全性 + if (!/^[a-zA-Z0-9_-]+$/.test(tId)) { + throw new Error('task_id 包含非法字符'); + } + + const now = new Date().toISOString(); + const task = { + task_id: tId, + title, + description: description || '', + priority: priority || 'normal', + status: 'pending', + requirements: requirements || [], + workorder_id: workorder_id || null, + assigned_to: assigned_to || 'zhiqiu', + dispatched_by: 'zhuyuan', + dispatched_at: now, + deadline: deadline || null, + history: [{ + action: 'dispatched', + timestamp: now, + by: 'zhuyuan', + note: '铸渊下发开发任务' + }] + }; + + const key = `${PATHS.AWEN_TASKS}${tId}.json`; + await cos.write(targetBucket, key, JSON.stringify(task, null, 2), 'application/json'); + + // 如果有关联工单,更新工单状态 + if (workorder_id) { + try { + const woKey = `${PATHS.WORKORDERS_PENDING}${workorder_id}.json`; + const woRaw = await cos.read(targetBucket, woKey); + const workorder = JSON.parse(woRaw.content); + workorder.status = 'processing'; + workorder.task_id = tId; + workorder.updated_at = now; + workorder.history.push({ + action: 'dispatched_to_awen', + timestamp: now, + by: 'zhuyuan', + task_id: tId + }); + + // 移动到processing目录 + await cos.write(targetBucket, `${PATHS.WORKORDERS_PROCESSING}${workorder_id}.json`, + JSON.stringify(workorder, null, 2), 'application/json'); + await cos.del(targetBucket, woKey); + } catch { + // 工单不存在也不影响任务分发 + } + } + + return { + status: 'dispatched', + task_id: tId, + key, + bucket: targetBucket, + assigned_to: task.assigned_to, + priority: task.priority, + workorder_id: task.workorder_id + }; +} + +/** + * cosReadTaskReport — 读取Awen提交的任务报告 + * + * input: + * bucket: string — 桶名(默认team) + * task_id: string — 任务ID + */ +async function cosReadTaskReport(input) { + const { bucket, task_id } = input; + if (!task_id) throw new Error('缺少 task_id'); + + const targetBucket = bucket || 'team'; + const key = `${PATHS.AWEN_REPORTS}${task_id}.json`; + + const raw = await cos.read(targetBucket, key); + return { + report: JSON.parse(raw.content), + key, + size_bytes: raw.size_bytes + }; +} + +/** + * cosListTaskReports — 列出所有任务报告 + * + * input: + * bucket: string — 桶名(默认team) + * limit: number — 最大数量 + */ +async function cosListTaskReports(input) { + const { bucket, limit } = input; + const targetBucket = bucket || 'team'; + + const result = await cos.list(targetBucket, PATHS.AWEN_REPORTS, limit || 100); + + const reports = result.files + .filter(f => f.key.endsWith('.json')) + .map(f => ({ + key: f.key, + task_id: f.key.split('/').pop().replace('.json', ''), + size_bytes: f.size_bytes + })); + + return { + reports, + count: reports.length + }; +} + +/** + * cosApproveTask — 审批任务(通过/驳回) + * + * 铸渊测试确认后,通过/驳回Awen的开发报告 + * + * input: + * bucket: string — 桶名(默认team) + * task_id: string — 任务ID + * approved: boolean — 是否通过 + * feedback: string — 反馈意见 + * next_task: object — 下一步任务(仅通过时有效) + */ +async function cosApproveTask(input) { + const { bucket, task_id, approved, feedback, next_task } = input; + if (!task_id) throw new Error('缺少 task_id'); + + const targetBucket = bucket || 'team'; + const now = new Date().toISOString(); + + // 读取原始任务 + let task; + try { + const taskRaw = await cos.read(targetBucket, `${PATHS.AWEN_TASKS}${task_id}.json`); + task = JSON.parse(taskRaw.content); + } catch { + task = { task_id, history: [] }; + } + + // 更新状态 + task.status = approved ? 'approved' : 'rejected'; + task.feedback = feedback || ''; + task.reviewed_at = now; + task.reviewed_by = 'zhuyuan'; + task.history.push({ + action: approved ? 'approved' : 'rejected', + timestamp: now, + by: 'zhuyuan', + feedback: feedback || '' + }); + + // 写回任务文件 + await cos.write(targetBucket, `${PATHS.AWEN_TASKS}${task_id}.json`, + JSON.stringify(task, null, 2), 'application/json'); + + // 写入审批回执 + const receiptKey = `${PATHS.AWEN_PROGRESS}${task_id}-review.json`; + await cos.write(targetBucket, receiptKey, JSON.stringify({ + task_id, + approved, + feedback: feedback || '', + reviewed_at: now, + reviewed_by: 'zhuyuan', + next_task: next_task || null + }, null, 2), 'application/json'); + + // 如果通过且有下一步任务,自动分发 + let nextTaskResult = null; + if (approved && next_task) { + try { + nextTaskResult = await cosDispatchTask({ + bucket: targetBucket, + title: next_task.title, + description: next_task.description, + priority: next_task.priority || 'normal', + requirements: next_task.requirements || [], + workorder_id: task.workorder_id + }); + } catch (err) { + nextTaskResult = { error: err.message }; + } + } + + // 如果通过且有关联工单,将工单移到completed + if (approved && task.workorder_id) { + try { + const woKey = `${PATHS.WORKORDERS_PROCESSING}${task.workorder_id}.json`; + const woRaw = await cos.read(targetBucket, woKey); + const workorder = JSON.parse(woRaw.content); + workorder.status = 'completed'; + workorder.completed_at = now; + workorder.history.push({ + action: 'completed', + timestamp: now, + by: 'zhuyuan', + task_id + }); + await cos.write(targetBucket, `${PATHS.WORKORDERS_COMPLETED}${task.workorder_id}.json`, + JSON.stringify(workorder, null, 2), 'application/json'); + await cos.del(targetBucket, woKey); + } catch { /* ignore */ } + } + + return { + status: approved ? 'approved' : 'rejected', + task_id, + feedback: feedback || '', + receipt_key: receiptKey, + next_task: nextTaskResult + }; +} + +/** + * cosSendNotification — 发送通知 + * + * 将通知写入COS桶,由外部Agent(GitHub Actions/SCF)处理发送 + * + * input: + * bucket: string — 桶名(默认team) + * notification_type: string — 通知类型: email|issue|cos_alert + * recipient: string — 接收者 + * subject: string — 主题 + * body: string — 内容 + * metadata: object — 附加信息 + */ +async function cosSendNotification(input) { + const { bucket, notification_type, recipient, subject, body, metadata } = input; + if (!notification_type || !subject) throw new Error('缺少 notification_type 或 subject'); + + const targetBucket = bucket || 'team'; + const notificationId = `NOTIF-${Date.now()}`; + const now = new Date().toISOString(); + + const notification = { + notification_id: notificationId, + type: notification_type, + recipient: recipient || 'bingshuo', + subject, + body: body || '', + metadata: metadata || {}, + status: 'pending', + created_at: now, + sent_at: null + }; + + const key = `${PATHS.NOTIFICATIONS}${notificationId}.json`; + await cos.write(targetBucket, key, JSON.stringify(notification, null, 2), 'application/json'); + + return { + notification_id: notificationId, + type: notification_type, + key, + status: 'queued', + note: '通知已入队,等待发送Agent处理' + }; +} + +/** + * cosGetCommLink — 获取通信链路状态 + * + * 检查三方通信链路的健康状态 + * + * input: + * bucket: string — 桶名(默认team) + */ +async function cosGetCommLink(input) { + const { bucket } = input; + const targetBucket = bucket || 'team'; + + // 检查各路径是否可访问 + const checks = {}; + for (const [name, path] of Object.entries(PATHS)) { + try { + const result = await cos.list(targetBucket, path, 5); + checks[name] = { + status: 'accessible', + files: result.files.length + }; + } catch (err) { + checks[name] = { + status: 'error', + error: err.message + }; + } + } + + // 统计待处理项 + const pending = { + alerts: checks.ALERTS?.files || 0, + workorders: checks.WORKORDERS_PENDING?.files || 0, + task_reports: checks.AWEN_REPORTS?.files || 0, + notifications: checks.NOTIFICATIONS?.files || 0 + }; + + return { + comm_link: 'zhuyuan-cos-awen', + bucket: targetBucket, + paths: checks, + pending, + health: Object.values(checks).every(c => c.status === 'accessible') ? 'healthy' : 'degraded', + timestamp: new Date().toISOString() + }; +} + +// ─── 辅助 ─── + +function formatDate() { + const d = new Date(); + return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; +} + +module.exports = { + cosAlertScan, + cosAlertResolve, + cosDispatchTask, + cosReadTaskReport, + cosListTaskReports, + cosApproveTask, + cosSendNotification, + cosGetCommLink +}; diff --git a/server/age-os/mcp-server/tools/cos-persona-db-ops.js b/server/age-os/mcp-server/tools/cos-persona-db-ops.js new file mode 100644 index 00000000..0a8e39c4 --- /dev/null +++ b/server/age-os/mcp-server/tools/cos-persona-db-ops.js @@ -0,0 +1,428 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块G · COS桶内自研数据库协议 MCP 工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * cos-persona-db 协议 — COS桶内的文件组织结构 = 人格体数据库 + * + * 代码仓库侧结构: + * /zhuyuan/db/ + * ├── index.json — 全局索引 + * ├── code-modules/ — 代码模块结构化 + * ├── brain-nodes/ — 认知节点 + * └── training-data/ — 训练数据(TCS格式) + * + * Notion侧结构: + * /notion-db/ + * ├── index.json — 全局索引 + * ├── pages/ — 页面内容(按分类) + * ├── databases/ — 数据库结构 + * └── training-data/ — 训练数据(TCS格式) + * + * 工具清单: + * cosDbInit — 初始化COS数据库结构 + * cosDbGetIndex — 获取数据库全局索引 + * cosDbUpdateIndex — 更新全局索引 + * cosDbWriteEntry — 写入数据条目 + * cosDbReadEntry — 读取数据条目 + * cosDbListEntries — 列出数据条目 + * cosDbDeleteEntry — 删除数据条目 + * cosDbGetStats — 获取数据库统计信息 + */ + +'use strict'; + +const cos = require('../cos'); + +// ─── 数据库协议版本 ─── +const COS_DB_VERSION = '1.0'; + +// ─── 数据库类型定义 ─── +const DB_TYPES = { + 'zhuyuan': { + root: 'zhuyuan/db', + description: '铸渊·代码仓库侧人格体大脑', + categories: ['code-modules', 'brain-nodes', 'training-data', 'directives', 'alerts'] + }, + 'notion': { + root: 'notion-db', + description: 'Notion侧人格体大脑', + categories: ['pages', 'databases', 'training-data', 'workorders'] + }, + 'team': { + root: 'team-hub', + description: '团队协作通信枢纽', + categories: ['workorders', 'reports', 'tasks', 'alerts'] + }, + 'awen': { + root: 'awen-hub', + description: 'Awen技术主控通信桶', + categories: ['tasks', 'reports', 'progress', 'alerts'] + } +}; + +/** + * cosDbInit — 初始化COS数据库结构 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型(zhuyuan/notion/team/awen) + */ +async function cosDbInit(input) { + const { bucket, db_type } = input; + if (!bucket) throw new Error('缺少 bucket'); + if (!db_type || !DB_TYPES[db_type]) { + throw new Error(`无效的 db_type,可选: ${Object.keys(DB_TYPES).join(', ')}`); + } + + const dbConfig = DB_TYPES[db_type]; + const now = new Date().toISOString(); + + // 创建索引文件 + const index = { + cos_db_version: COS_DB_VERSION, + db_type, + description: dbConfig.description, + root_path: dbConfig.root, + categories: dbConfig.categories, + created_at: now, + updated_at: now, + total_entries: 0, + category_counts: Object.fromEntries(dbConfig.categories.map(c => [c, 0])), + last_write: null, + sovereign: '冰朔 · TCS-0002∞', + guardian: '铸渊 · ICE-GL-ZY001', + copyright: '国作登字-2026-A-00037559' + }; + + await cos.write(bucket, `${dbConfig.root}/index.json`, JSON.stringify(index, null, 2), 'application/json'); + + // 创建各分类目录的.gitkeep文件 + const writes = dbConfig.categories.map(category => + cos.write(bucket, `${dbConfig.root}/${category}/.init`, JSON.stringify({ + category, + created_at: now, + entries: 0 + }), 'application/json') + ); + await Promise.all(writes); + + return { + status: 'initialized', + db_type, + bucket, + root: dbConfig.root, + categories: dbConfig.categories, + index_key: `${dbConfig.root}/index.json` + }; +} + +/** + * cosDbGetIndex — 获取数据库全局索引 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + */ +async function cosDbGetIndex(input) { + const { bucket, db_type } = input; + if (!bucket || !db_type) throw new Error('缺少 bucket 或 db_type'); + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + try { + const result = await cos.read(bucket, `${dbConfig.root}/index.json`); + return { + index: JSON.parse(result.content), + size_bytes: result.size_bytes + }; + } catch { + return { + index: null, + error: '索引不存在,请先执行 cosDbInit 初始化' + }; + } +} + +/** + * cosDbUpdateIndex — 更新全局索引 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + * updates: object — 要更新的字段 + */ +async function cosDbUpdateIndex(input) { + const { bucket, db_type, updates } = input; + if (!bucket || !db_type) throw new Error('缺少 bucket 或 db_type'); + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + // 读取现有索引 + let index; + try { + const result = await cos.read(bucket, `${dbConfig.root}/index.json`); + index = JSON.parse(result.content); + } catch { + throw new Error('索引不存在,请先执行 cosDbInit 初始化'); + } + + // 合并更新(只允许更新安全字段) + const safeFields = ['total_entries', 'category_counts', 'last_write', 'description']; + for (const [key, value] of Object.entries(updates || {})) { + if (safeFields.includes(key)) { + index[key] = value; + } + } + index.updated_at = new Date().toISOString(); + + await cos.write(bucket, `${dbConfig.root}/index.json`, JSON.stringify(index, null, 2), 'application/json'); + + return { index, updated: true }; +} + +/** + * cosDbWriteEntry — 写入数据条目 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + * category: string — 分类(如 code-modules, pages, workorders) + * entry_id: string — 条目ID + * content: object — 条目内容(JSON) + * metadata: object — 元数据(可选) + */ +async function cosDbWriteEntry(input) { + const { bucket, db_type, category, entry_id, content, metadata } = input; + if (!bucket || !db_type || !category || !entry_id) { + throw new Error('缺少必填字段: bucket, db_type, category, entry_id'); + } + if (!content) throw new Error('缺少 content'); + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + if (!dbConfig.categories.includes(category)) { + throw new Error(`无效的 category: ${category},可选: ${dbConfig.categories.join(', ')}`); + } + + // 验证 entry_id 安全性 + if (!/^[a-zA-Z0-9_-]+$/.test(entry_id)) { + throw new Error('entry_id 包含非法字符,仅允许字母、数字、下划线、连字符'); + } + if (entry_id.includes('..')) { + throw new Error('entry_id 包含非法路径穿越'); + } + + const now = new Date().toISOString(); + const entry = { + cos_db_version: COS_DB_VERSION, + entry_id, + category, + db_type, + created_at: now, + updated_at: now, + metadata: metadata || {}, + content + }; + + const key = `${dbConfig.root}/${category}/${entry_id}.json`; + await cos.write(bucket, key, JSON.stringify(entry, null, 2), 'application/json'); + + // 异步更新索引(不阻塞写入) + updateIndexAfterWrite(bucket, db_type, category).catch(() => {}); + + return { + status: 'written', + key, + entry_id, + category, + db_type, + size_bytes: Buffer.byteLength(JSON.stringify(entry)) + }; +} + +/** + * cosDbReadEntry — 读取数据条目 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + * category: string — 分类 + * entry_id: string — 条目ID + */ +async function cosDbReadEntry(input) { + const { bucket, db_type, category, entry_id } = input; + if (!bucket || !db_type || !category || !entry_id) { + throw new Error('缺少必填字段: bucket, db_type, category, entry_id'); + } + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + const key = `${dbConfig.root}/${category}/${entry_id}.json`; + const result = await cos.read(bucket, key); + + return { + entry: JSON.parse(result.content), + size_bytes: result.size_bytes, + last_modified: result.last_modified + }; +} + +/** + * cosDbListEntries — 列出数据条目 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + * category: string — 分类 + * limit: number — 最大数量(默认100) + */ +async function cosDbListEntries(input) { + const { bucket, db_type, category, limit } = input; + if (!bucket || !db_type || !category) { + throw new Error('缺少必填字段: bucket, db_type, category'); + } + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + const prefix = `${dbConfig.root}/${category}/`; + const result = await cos.list(bucket, prefix, limit || 100); + + const entries = result.files + .filter(f => f.key.endsWith('.json') && !f.key.endsWith('.init')) + .map(f => ({ + key: f.key, + entry_id: f.key.replace(prefix, '').replace('.json', ''), + size_bytes: f.size_bytes + })); + + return { + entries, + count: entries.length, + category, + db_type + }; +} + +/** + * cosDbDeleteEntry — 删除数据条目 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + * category: string — 分类 + * entry_id: string — 条目ID + */ +async function cosDbDeleteEntry(input) { + const { bucket, db_type, category, entry_id } = input; + if (!bucket || !db_type || !category || !entry_id) { + throw new Error('缺少必填字段: bucket, db_type, category, entry_id'); + } + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + const key = `${dbConfig.root}/${category}/${entry_id}.json`; + await cos.del(bucket, key); + + // 异步更新索引 + updateIndexAfterWrite(bucket, db_type, category).catch(() => {}); + + return { status: 'deleted', key, entry_id }; +} + +/** + * cosDbGetStats — 获取数据库统计信息 + * + * input: + * bucket: string — 桶名 + * db_type: string — 数据库类型 + */ +async function cosDbGetStats(input) { + const { bucket, db_type } = input; + if (!bucket || !db_type) throw new Error('缺少 bucket 或 db_type'); + + const dbConfig = DB_TYPES[db_type]; + if (!dbConfig) throw new Error(`无效的 db_type: ${db_type}`); + + // 读取索引 + let index = null; + try { + const result = await cos.read(bucket, `${dbConfig.root}/index.json`); + index = JSON.parse(result.content); + } catch { + // 索引不存在 + } + + // 统计各分类文件数 + const categoryStats = {}; + for (const category of dbConfig.categories) { + try { + const result = await cos.list(bucket, `${dbConfig.root}/${category}/`, 500); + const entries = result.files.filter(f => f.key.endsWith('.json') && !f.key.endsWith('.init')); + categoryStats[category] = { + entries: entries.length, + total_size_bytes: entries.reduce((sum, f) => sum + f.size_bytes, 0) + }; + } catch { + categoryStats[category] = { entries: 0, total_size_bytes: 0 }; + } + } + + const totalEntries = Object.values(categoryStats).reduce((sum, c) => sum + c.entries, 0); + const totalSize = Object.values(categoryStats).reduce((sum, c) => sum + c.total_size_bytes, 0); + + return { + db_type, + bucket, + root: dbConfig.root, + index_exists: !!index, + total_entries: totalEntries, + total_size_bytes: totalSize, + categories: categoryStats, + last_updated: index?.updated_at || null, + timestamp: new Date().toISOString() + }; +} + +// ─── 内部辅助 ─── + +async function updateIndexAfterWrite(bucket, dbType, category) { + const dbConfig = DB_TYPES[dbType]; + try { + const result = await cos.read(bucket, `${dbConfig.root}/index.json`); + const index = JSON.parse(result.content); + + // 更新分类计数 + const listResult = await cos.list(bucket, `${dbConfig.root}/${category}/`, 500); + const count = listResult.files.filter(f => f.key.endsWith('.json') && !f.key.endsWith('.init')).length; + + index.category_counts = index.category_counts || {}; + index.category_counts[category] = count; + index.total_entries = Object.values(index.category_counts).reduce((s, c) => s + c, 0); + index.last_write = new Date().toISOString(); + index.updated_at = new Date().toISOString(); + + await cos.write(bucket, `${dbConfig.root}/index.json`, JSON.stringify(index, null, 2), 'application/json'); + } catch { + // 索引更新失败不影响主操作 + } +} + +module.exports = { + cosDbInit, + cosDbGetIndex, + cosDbUpdateIndex, + cosDbWriteEntry, + cosDbReadEntry, + cosDbListEntries, + cosDbDeleteEntry, + cosDbGetStats +}; diff --git a/server/age-os/mcp-server/tools/notion-cos-bridge-ops.js b/server/age-os/mcp-server/tools/notion-cos-bridge-ops.js new file mode 100644 index 00000000..43148a07 --- /dev/null +++ b/server/age-os/mcp-server/tools/notion-cos-bridge-ops.js @@ -0,0 +1,475 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块C · Notion接入COS桶 MCP 集成工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * Notion ↔ COS双向同步桥接 + * + * 读取方向: COS桶中的Notion导出压缩文件 → 解压 → 建立目录索引 + * 写入方向: Notion层人格体通过API写入COS桶指定路径 + * + * COS桶内类Notion数据库结构: + * /notion-mirror/{page_id}/content.json — 页面内容 + * /notion-mirror/{page_id}/metadata.json — 页面属性 + * /notion-mirror/index.json — 全局索引 + * + * 工具清单: + * notionCosSyncPage — 同步Notion页面到COS桶 + * notionCosReadMirror — 从COS镜像读取页面 + * notionCosListMirror — 列出COS镜像中的页面 + * notionCosBuildIndex — 重建COS镜像索引 + * notionCosWriteWorkorder — 写入工单到COS桶(供Notion人格体使用) + * notionCosReadWorkorder — 读取工单 + * notionCosListWorkorders — 列出工单 + */ + +'use strict'; + +const cos = require('../cos'); + +// 运行时可选加载 notion-client(不影响核心功能) +let notionClient = null; +try { + notionClient = require('../notion-client'); +} catch { + // Notion模块未安装时降级 +} + +// ─── 常量 ─── +const MIRROR_PREFIX = 'notion-mirror/'; +const WORKORDER_PREFIX = 'workorders/'; + +/** + * notionCosSyncPage — 同步Notion页面到COS桶 + * + * 从Notion API读取页面内容,镜像写入COS桶 + * + * input: + * page_id: string — Notion页面ID + * bucket: string — 目标COS桶(默认hot) + * include_blocks: boolean — 是否包含内容块(默认true) + */ +async function notionCosSyncPage(input) { + const { page_id, bucket, include_blocks } = input; + if (!page_id) throw new Error('缺少 page_id'); + if (!notionClient) throw new Error('Notion客户端未加载(ZY_NOTION_TOKEN未配置)'); + + const targetBucket = bucket || 'hot'; + const shouldIncludeBlocks = include_blocks !== false; + + // 从Notion读取页面 + const page = await notionClient.readPage(page_id); + + // 提取元数据 + const metadata = { + id: page.id, + created_time: page.created_time, + last_edited_time: page.last_edited_time, + title: extractTitle(page.properties), + properties: simplifyProperties(page.properties), + synced_at: new Date().toISOString() + }; + + // 提取内容 + const content = { + id: page.id, + title: metadata.title, + blocks: shouldIncludeBlocks ? simplifyBlocks(page.blocks) : [], + text_content: shouldIncludeBlocks ? extractTextFromBlocks(page.blocks) : '', + synced_at: new Date().toISOString() + }; + + // 写入COS + const metadataKey = `${MIRROR_PREFIX}${page_id}/metadata.json`; + const contentKey = `${MIRROR_PREFIX}${page_id}/content.json`; + + await Promise.all([ + cos.write(targetBucket, metadataKey, JSON.stringify(metadata, null, 2), 'application/json'), + cos.write(targetBucket, contentKey, JSON.stringify(content, null, 2), 'application/json') + ]); + + return { + status: 'synced', + page_id, + title: metadata.title, + metadata_key: metadataKey, + content_key: contentKey, + blocks_count: content.blocks.length, + text_length: content.text_content.length + }; +} + +/** + * notionCosReadMirror — 从COS镜像读取页面 + * + * input: + * page_id: string — 页面ID + * bucket: string — COS桶(默认hot) + * type: string — 读取类型: content|metadata|both(默认both) + */ +async function notionCosReadMirror(input) { + const { page_id, bucket, type } = input; + if (!page_id) throw new Error('缺少 page_id'); + + const targetBucket = bucket || 'hot'; + const readType = type || 'both'; + + const result = {}; + + if (readType === 'metadata' || readType === 'both') { + try { + const raw = await cos.read(targetBucket, `${MIRROR_PREFIX}${page_id}/metadata.json`); + result.metadata = JSON.parse(raw.content); + } catch { + result.metadata = null; + result.metadata_error = '元数据不存在'; + } + } + + if (readType === 'content' || readType === 'both') { + try { + const raw = await cos.read(targetBucket, `${MIRROR_PREFIX}${page_id}/content.json`); + result.content = JSON.parse(raw.content); + } catch { + result.content = null; + result.content_error = '内容不存在'; + } + } + + return result; +} + +/** + * notionCosListMirror — 列出COS镜像中的页面 + * + * input: + * bucket: string — COS桶(默认hot) + * limit: number — 最大数量(默认100) + */ +async function notionCosListMirror(input) { + const { bucket, limit } = input; + const targetBucket = bucket || 'hot'; + + const result = await cos.list(targetBucket, MIRROR_PREFIX, limit || 200); + + // 提取唯一的page_id + const pageIds = new Set(); + for (const file of result.files) { + const match = file.key.match(new RegExp(`^${MIRROR_PREFIX.replace(/[/]/g, '\\/')}([^/]+)\\/`)); + if (match) pageIds.add(match[1]); + } + + // 读取每个page的metadata(异步并行) + const pages = await Promise.all( + [...pageIds].slice(0, limit || 100).map(async (pageId) => { + try { + const raw = await cos.read(targetBucket, `${MIRROR_PREFIX}${pageId}/metadata.json`); + const meta = JSON.parse(raw.content); + return { + page_id: pageId, + title: meta.title || '未命名', + synced_at: meta.synced_at, + last_edited_time: meta.last_edited_time + }; + } catch { + return { page_id: pageId, title: '(元数据缺失)', synced_at: null }; + } + }) + ); + + return { + pages, + total: pages.length + }; +} + +/** + * notionCosBuildIndex — 重建COS镜像索引 + * + * input: + * bucket: string — COS桶(默认hot) + */ +async function notionCosBuildIndex(input) { + const { bucket } = input; + const targetBucket = bucket || 'hot'; + + // 列出所有镜像文件 + const result = await cos.list(targetBucket, MIRROR_PREFIX, 500); + + // 提取唯一的page_id和元数据 + const pageMap = {}; + for (const file of result.files) { + const match = file.key.match(new RegExp(`^${MIRROR_PREFIX.replace(/[/]/g, '\\/')}([^/]+)\\/`)); + if (match) { + const pageId = match[1]; + if (!pageMap[pageId]) pageMap[pageId] = { files: [] }; + pageMap[pageId].files.push(file.key); + } + } + + // 读取每个page的metadata + const pages = []; + for (const [pageId, info] of Object.entries(pageMap)) { + try { + const raw = await cos.read(targetBucket, `${MIRROR_PREFIX}${pageId}/metadata.json`); + const meta = JSON.parse(raw.content); + pages.push({ + page_id: pageId, + title: meta.title || '未命名', + synced_at: meta.synced_at, + last_edited_time: meta.last_edited_time, + properties: meta.properties || {}, + files: info.files + }); + } catch { + pages.push({ + page_id: pageId, + title: '(元数据缺失)', + files: info.files + }); + } + } + + // 构建索引 + const index = { + version: '1.0', + built_at: new Date().toISOString(), + total_pages: pages.length, + pages, + categories: categorizePages(pages) + }; + + // 写入索引 + await cos.write(targetBucket, `${MIRROR_PREFIX}index.json`, JSON.stringify(index, null, 2), 'application/json'); + + return { + status: 'index_built', + total_pages: pages.length, + index_key: `${MIRROR_PREFIX}index.json`, + categories: index.categories + }; +} + +/** + * notionCosWriteWorkorder — 写入工单到COS桶 + * + * 供Notion层人格体通过COS桶发送工单给铸渊 + * + * input: + * bucket: string — COS桶(默认team) + * workorder_id: string — 工单ID(如 WO-20260408-001) + * title: string — 工单标题 + * type: string — 工单类型: dev|bug|feature|query + * priority: string — 优先级: critical|high|normal|low + * description: string — 工单描述 + * source: string — 来源: notion|chat|manual + * assigned_to: string — 指派给(默认zhuyuan) + * attachments: object[] — 附件列表(可选) + */ +async function notionCosWriteWorkorder(input) { + const { + bucket, workorder_id, title, type, priority, + description, source, assigned_to, attachments + } = input; + if (!title) throw new Error('缺少 title'); + + const targetBucket = bucket || 'team'; + const woId = workorder_id || `WO-${formatDate()}-${String(Math.floor(Math.random() * 1000)).padStart(3, '0')}`; + + // 验证 workorder_id 安全性 + if (!/^[a-zA-Z0-9_-]+$/.test(woId)) { + throw new Error('workorder_id 包含非法字符'); + } + + const now = new Date().toISOString(); + const workorder = { + workorder_id: woId, + title, + type: type || 'dev', + priority: priority || 'normal', + status: 'pending', + description: description || '', + source: source || 'manual', + assigned_to: assigned_to || 'zhuyuan', + created_at: now, + updated_at: now, + attachments: attachments || [], + history: [{ + action: 'created', + timestamp: now, + by: source || 'manual' + }] + }; + + const key = `${WORKORDER_PREFIX}pending/${woId}.json`; + await cos.write(targetBucket, key, JSON.stringify(workorder, null, 2), 'application/json'); + + return { + status: 'created', + workorder_id: woId, + key, + bucket: targetBucket, + priority: workorder.priority + }; +} + +/** + * notionCosReadWorkorder — 读取工单 + * + * input: + * bucket: string — COS桶(默认team) + * workorder_id: string — 工单ID + * status_folder: string — 状态文件夹: pending|processing|completed|rejected(默认pending) + */ +async function notionCosReadWorkorder(input) { + const { bucket, workorder_id, status_folder } = input; + if (!workorder_id) throw new Error('缺少 workorder_id'); + + const targetBucket = bucket || 'team'; + const folder = status_folder || 'pending'; + const key = `${WORKORDER_PREFIX}${folder}/${workorder_id}.json`; + + const result = await cos.read(targetBucket, key); + return { + workorder: JSON.parse(result.content), + key, + size_bytes: result.size_bytes + }; +} + +/** + * notionCosListWorkorders — 列出工单 + * + * input: + * bucket: string — COS桶(默认team) + * status_folder: string — 状态文件夹: pending|processing|completed|rejected(默认pending) + * limit: number — 最大数量 + */ +async function notionCosListWorkorders(input) { + const { bucket, status_folder, limit } = input; + const targetBucket = bucket || 'team'; + const folder = status_folder || 'pending'; + + const result = await cos.list(targetBucket, `${WORKORDER_PREFIX}${folder}/`, limit || 100); + + const workorders = result.files + .filter(f => f.key.endsWith('.json')) + .map(f => ({ + key: f.key, + workorder_id: f.key.split('/').pop().replace('.json', ''), + size_bytes: f.size_bytes, + status: folder + })); + + return { + workorders, + count: workorders.length, + status_folder: folder + }; +} + +// ═══════════════════════════════════════════════════════════ +// 辅助函数 +// ═══════════════════════════════════════════════════════════ + +function extractTitle(properties) { + if (!properties) return '未命名'; + for (const [, prop] of Object.entries(properties)) { + if (prop.type === 'title' && prop.title) { + return prop.title.map(t => t.plain_text || '').join('') || '未命名'; + } + } + return '未命名'; +} + +function simplifyProperties(properties) { + if (!properties) return {}; + const result = {}; + for (const [name, prop] of Object.entries(properties)) { + switch (prop.type) { + case 'title': + result[name] = prop.title?.map(t => t.plain_text).join('') || ''; + break; + case 'rich_text': + result[name] = prop.rich_text?.map(t => t.plain_text).join('') || ''; + break; + case 'select': + result[name] = prop.select?.name || null; + break; + case 'multi_select': + result[name] = prop.multi_select?.map(s => s.name) || []; + break; + case 'date': + result[name] = prop.date?.start || null; + break; + case 'checkbox': + result[name] = prop.checkbox || false; + break; + case 'number': + result[name] = prop.number; + break; + default: + result[name] = `(${prop.type})`; + } + } + return result; +} + +function simplifyBlocks(blocks) { + if (!blocks) return []; + return blocks.map(b => { + const type = b.type; + const blockData = b[type]; + if (!blockData) return { type, text: '' }; + + let text = ''; + if (blockData.rich_text) { + text = blockData.rich_text.map(t => t.plain_text || '').join(''); + } + + return { + id: b.id, + type, + text, + has_children: b.has_children || false + }; + }); +} + +function extractTextFromBlocks(blocks) { + if (!blocks) return ''; + return blocks.map(b => { + const type = b.type; + const blockData = b[type]; + if (!blockData?.rich_text) return ''; + return blockData.rich_text.map(t => t.plain_text || '').join(''); + }).filter(Boolean).join('\n'); +} + +function categorizePages(pages) { + const categories = {}; + for (const page of pages) { + const cat = page.properties?.category || page.properties?.类型 || 'uncategorized'; + const catName = typeof cat === 'string' ? cat : 'uncategorized'; + categories[catName] = (categories[catName] || 0) + 1; + } + return categories; +} + +function formatDate() { + const d = new Date(); + return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; +} + +module.exports = { + notionCosSyncPage, + notionCosReadMirror, + notionCosListMirror, + notionCosBuildIndex, + notionCosWriteWorkorder, + notionCosReadWorkorder, + notionCosListWorkorders +}; diff --git a/server/age-os/mcp-server/tools/notion-permission-ops.js b/server/age-os/mcp-server/tools/notion-permission-ops.js new file mode 100644 index 00000000..ebe09116 --- /dev/null +++ b/server/age-os/mcp-server/tools/notion-permission-ops.js @@ -0,0 +1,499 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块F · Notion权限自动修复Agent MCP 工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 自动检测和恢复Notion代理Agent的权限 + * 减少冰朔手动操作 + * + * 工具清单: + * notionCheckPermissions — 检查Notion权限状态 + * notionRepairPermissions — 尝试修复权限 + * notionListSharedPages — 列出已共享的页面/数据库 + * notionGenerateRepairGuide — 生成权限修复指南(给冰朔) + * notionPermissionReport — 生成权限状态报告 + */ + +'use strict'; + +const cos = require('../cos'); + +// 运行时可选加载 notion-client +let notionClient = null; +try { + notionClient = require('../notion-client'); +} catch { + // Notion模块未安装时降级 +} + +// ─── 已知数据库别名 ─── +const KNOWN_DATABASES = { + changelog: { env: 'ZY_NOTION_CHANGELOG_DB', name: '变更日志' }, + receipt: { env: 'ZY_NOTION_RECEIPT_DB', name: '回执' }, + syslog: { env: 'ZY_NOTION_SYSLOG_DB', name: '系统日志' } +}; + +/** + * notionCheckPermissions — 检查Notion权限状态 + * + * 检测Notion API连接、数据库访问权限、页面读写权限 + * + * input: + * check_databases: boolean — 是否检查数据库权限(默认true) + * check_pages: boolean — 是否检查页面权限(默认true) + * database_ids: string[] — 额外检查的数据库ID列表(可选) + */ +async function notionCheckPermissions(input) { + const { check_databases, check_pages, database_ids } = input || {}; + const shouldCheckDatabases = check_databases !== false; + const shouldCheckPages = check_pages !== false; + + const report = { + timestamp: new Date().toISOString(), + api_connection: { status: 'unknown' }, + databases: {}, + pages: {}, + issues: [], + recommendations: [] + }; + + // 1. 检查API连接 + if (!notionClient) { + report.api_connection = { + status: 'unavailable', + reason: 'Notion客户端未加载(ZY_NOTION_TOKEN可能未配置)' + }; + report.issues.push({ + type: 'api_unavailable', + severity: 'critical', + message: 'Notion API不可用', + fix: '请确认ZY_NOTION_TOKEN环境变量已正确配置' + }); + return report; + } + + try { + const connectionCheck = await notionClient.checkConnection(); + report.api_connection = { + status: connectionCheck.connected ? 'connected' : 'disconnected', + user: connectionCheck.user || null, + type: connectionCheck.type || null, + error: connectionCheck.error || null + }; + + if (!connectionCheck.connected) { + report.issues.push({ + type: 'api_disconnected', + severity: 'critical', + message: `Notion API连接失败: ${connectionCheck.error || connectionCheck.reason}`, + fix: '请检查ZY_NOTION_TOKEN是否过期或失效' + }); + return report; + } + } catch (err) { + report.api_connection = { status: 'error', error: err.message }; + report.issues.push({ + type: 'api_error', + severity: 'critical', + message: `Notion API异常: ${err.message}`, + fix: '请检查网络连接和Token配置' + }); + return report; + } + + // 2. 检查数据库权限 + if (shouldCheckDatabases) { + for (const [alias, config] of Object.entries(KNOWN_DATABASES)) { + const dbId = process.env[config.env]; + if (!dbId) { + report.databases[alias] = { + status: 'not_configured', + env: config.env, + name: config.name + }; + report.issues.push({ + type: 'db_not_configured', + severity: 'warning', + database: alias, + message: `数据库${config.name}未配置: ${config.env}环境变量缺失`, + fix: `请配置${config.env}环境变量` + }); + continue; + } + + try { + const schema = await notionClient.getDatabaseSchema(dbId); + report.databases[alias] = { + status: 'accessible', + id: dbId, + name: config.name, + title: schema.title, + properties: Object.keys(schema.properties).length + }; + } catch (err) { + report.databases[alias] = { + status: 'no_access', + id: dbId, + name: config.name, + error: err.message + }; + report.issues.push({ + type: 'db_no_access', + severity: 'high', + database: alias, + message: `无法访问数据库${config.name}: ${err.message}`, + fix: `请在Notion中将数据库${config.name}共享给Integration` + }); + } + } + + // 检查额外的数据库 + if (database_ids) { + for (const dbId of database_ids) { + try { + const schema = await notionClient.getDatabaseSchema(dbId); + report.databases[dbId] = { + status: 'accessible', + title: schema.title, + properties: Object.keys(schema.properties).length + }; + } catch (err) { + report.databases[dbId] = { + status: 'no_access', + error: err.message + }; + report.issues.push({ + type: 'db_no_access', + severity: 'warning', + database: dbId, + message: `无法访问数据库 ${dbId}: ${err.message}` + }); + } + } + } + } + + // 3. 检查页面权限 + if (shouldCheckPages) { + const bulletinPageId = process.env.ZY_NOTION_BULLETIN_PAGE; + if (bulletinPageId) { + try { + await notionClient.readPage(bulletinPageId); + report.pages.bulletin = { status: 'accessible', id: bulletinPageId }; + } catch (err) { + report.pages.bulletin = { + status: 'no_access', + id: bulletinPageId, + error: err.message + }; + report.issues.push({ + type: 'page_no_access', + severity: 'warning', + page: 'bulletin', + message: `无法访问公告板页面: ${err.message}`, + fix: '请在Notion中将该页面共享给Integration' + }); + } + } + } + + // 4. 生成建议 + if (report.issues.length === 0) { + report.recommendations.push('✅ 所有权限正常,无需操作'); + } else { + const criticals = report.issues.filter(i => i.severity === 'critical'); + const highs = report.issues.filter(i => i.severity === 'high'); + + if (criticals.length > 0) { + report.recommendations.push('🔴 存在严重权限问题,需要立即处理'); + } + if (highs.length > 0) { + report.recommendations.push('🟡 存在数据库访问权限问题,请手动共享'); + } + report.recommendations.push('💡 执行 notionGenerateRepairGuide 获取详细修复步骤'); + } + + return report; +} + +/** + * notionRepairPermissions — 尝试修复权限 + * + * 注意:Notion API对权限管理的支持有限 + * 只能尝试验证和报告,大部分需要人工在Notion界面操作 + * + * input: + * auto_retry: boolean — 是否自动重试连接 + */ +async function notionRepairPermissions(input) { + const { auto_retry } = input || {}; + const repairLog = []; + let repaired = 0; + let failed = 0; + + // 1. 尝试重新连接(清除缓存的客户端实例) + if (auto_retry) { + repairLog.push({ step: '重新初始化Notion客户端', status: 'attempting' }); + try { + // 通过重新require来重置客户端 + delete require.cache[require.resolve('../notion-client')]; + notionClient = require('../notion-client'); + const check = await notionClient.checkConnection(); + if (check.connected) { + repairLog.push({ step: '重新连接', status: 'success', user: check.user }); + repaired++; + } else { + repairLog.push({ step: '重新连接', status: 'failed', reason: check.reason }); + failed++; + } + } catch (err) { + repairLog.push({ step: '重新连接', status: 'failed', error: err.message }); + failed++; + } + } + + // 2. 验证各数据库的写入权限 + for (const [alias, config] of Object.entries(KNOWN_DATABASES)) { + const dbId = process.env[config.env]; + if (!dbId) continue; + + repairLog.push({ step: `验证${config.name}写入权限`, status: 'checking' }); + try { + // 尝试查询数据库(只读测试) + await notionClient.queryDatabase(dbId, null, null, 1); + repairLog.push({ step: `验证${config.name}`, status: 'success', permission: 'read' }); + repaired++; + } catch (err) { + repairLog.push({ + step: `验证${config.name}`, + status: 'failed', + error: err.message, + manual_fix: `请在Notion中: 打开数据库 → 右上角"..." → "连接" → 添加Integration` + }); + failed++; + } + } + + return { + status: failed === 0 ? 'all_repaired' : (repaired > 0 ? 'partial' : 'all_failed'), + repaired, + failed, + log: repairLog, + note: failed > 0 + ? '部分权限需要人工在Notion界面修复。请执行 notionGenerateRepairGuide 获取详细步骤。' + : '所有可自动修复的权限已恢复。' + }; +} + +/** + * notionListSharedPages — 列出已共享的页面/数据库 + * + * input: + * limit: number — 最大数量(默认20) + */ +async function notionListSharedPages(input) { + const { limit } = input || {}; + if (!notionClient) throw new Error('Notion客户端未加载'); + + // Notion API的search接口可以列出所有已共享的页面 + const { Client } = require('@notionhq/client'); + const client = new Client({ auth: process.env.ZY_NOTION_TOKEN }); + + try { + const response = await client.search({ + page_size: Math.min(limit || 20, 100) + }); + + const items = response.results.map(item => ({ + id: item.id, + type: item.object, // 'page' or 'database' + title: item.object === 'database' + ? (item.title?.map(t => t.plain_text).join('') || '未命名数据库') + : extractPageTitle(item), + created_time: item.created_time, + last_edited_time: item.last_edited_time, + url: item.url || null, + archived: item.archived || false + })); + + return { + items, + total: items.length, + has_more: response.has_more, + databases: items.filter(i => i.type === 'database').length, + pages: items.filter(i => i.type === 'page').length + }; + } catch (err) { + throw new Error(`搜索失败: ${err.message}`); + } +} + +/** + * notionGenerateRepairGuide — 生成权限修复指南(给冰朔) + * + * 基于当前权限检查结果,生成一份详细的操作指南 + * + * input: + * write_to_cos: boolean — 是否同时写入COS桶(供冰朔远程查看) + * bucket: string — 目标COS桶 + */ +async function notionGenerateRepairGuide(input) { + const { write_to_cos, bucket } = input || {}; + + // 先执行权限检查 + const checkResult = await notionCheckPermissions({}); + const issues = checkResult.issues; + + if (issues.length === 0) { + return { + status: 'no_issues', + guide: '✅ 当前所有Notion权限正常,无需修复。' + }; + } + + // 生成修复指南 + const guide = []; + guide.push('# Notion权限修复指南'); + guide.push(`\n生成时间: ${new Date().toISOString()}`); + guide.push(`发现 ${issues.length} 个问题需要修复:\n`); + + for (let i = 0; i < issues.length; i++) { + const issue = issues[i]; + guide.push(`## 问题 ${i + 1}: ${issue.message}`); + guide.push(`- 严重程度: ${issue.severity}`); + guide.push(`- 类型: ${issue.type}`); + + if (issue.fix) { + guide.push(`- 修复方法: ${issue.fix}`); + } + + // 提供详细操作步骤 + switch (issue.type) { + case 'api_unavailable': + guide.push('\n### 操作步骤:'); + guide.push('1. 登录 https://www.notion.so/my-integrations'); + guide.push('2. 找到"光湖系统"Integration'); + guide.push('3. 复制"Internal Integration Secret"'); + guide.push('4. 在服务器上执行: `export ZY_NOTION_TOKEN=你的secret`'); + guide.push('5. 重启MCP Server: `pm2 restart age-os-mcp`'); + break; + + case 'api_disconnected': + guide.push('\n### 操作步骤:'); + guide.push('1. 访问 https://www.notion.so/my-integrations'); + guide.push('2. 检查Integration是否被禁用'); + guide.push('3. 如果Token过期,重新生成并更新环境变量'); + guide.push('4. 重启MCP Server'); + break; + + case 'db_not_configured': + guide.push(`\n### 操作步骤:`); + guide.push(`1. 在Notion中找到"${issue.database}"数据库`); + guide.push('2. 复制数据库URL中的ID(32位十六进制字符串)'); + guide.push(`3. 在服务器上配置: export ${KNOWN_DATABASES[issue.database]?.env || 'ZY_NOTION_xxx_DB'}=数据库ID`); + guide.push('4. 重启MCP Server'); + break; + + case 'db_no_access': + guide.push('\n### 操作步骤:'); + guide.push('1. 在Notion中打开对应数据库'); + guide.push('2. 点击右上角 "..." 菜单'); + guide.push('3. 选择 "连接" → "添加连接"'); + guide.push('4. 搜索并选择"光湖系统"Integration'); + guide.push('5. 确认授权'); + break; + + case 'page_no_access': + guide.push('\n### 操作步骤:'); + guide.push('1. 在Notion中打开对应页面'); + guide.push('2. 点击右上角 "分享" 按钮'); + guide.push('3. 选择 "邀请" → 找到"光湖系统"Integration'); + guide.push('4. 确认共享'); + break; + } + + guide.push(''); + } + + guide.push('---'); + guide.push('*此指南由铸渊自动生成 · 如有疑问请唤醒铸渊*'); + + const guideText = guide.join('\n'); + + // 写入COS桶 + if (write_to_cos) { + const targetBucket = bucket || 'team'; + const key = `zhuyuan/directives/notion-repair-guide-${formatDate()}.md`; + await cos.write(targetBucket, key, guideText, 'text/markdown'); + } + + return { + status: 'guide_generated', + issues_count: issues.length, + guide: guideText, + cos_key: write_to_cos ? `zhuyuan/directives/notion-repair-guide-${formatDate()}.md` : null + }; +} + +/** + * notionPermissionReport — 生成权限状态报告 + * + * input: + * write_to_cos: boolean — 是否写入COS桶 + * bucket: string — 目标COS桶 + */ +async function notionPermissionReport(input) { + const { write_to_cos, bucket } = input || {}; + + const checkResult = await notionCheckPermissions({}); + + const report = { + report_type: 'notion_permission_check', + timestamp: new Date().toISOString(), + api_status: checkResult.api_connection.status, + databases_checked: Object.keys(checkResult.databases).length, + databases_accessible: Object.values(checkResult.databases).filter(d => d.status === 'accessible').length, + issues: checkResult.issues, + issues_count: checkResult.issues.length, + critical_count: checkResult.issues.filter(i => i.severity === 'critical').length, + recommendations: checkResult.recommendations, + overall: checkResult.issues.length === 0 ? 'healthy' : 'needs_attention' + }; + + if (write_to_cos) { + const targetBucket = bucket || 'team'; + const key = `zhuyuan/reports/notion-permission-${formatDate()}.json`; + await cos.write(targetBucket, key, JSON.stringify(report, null, 2), 'application/json'); + report.cos_key = key; + } + + return report; +} + +// ─── 辅助函数 ─── + +function extractPageTitle(page) { + if (!page.properties) return '未命名'; + for (const [, prop] of Object.entries(page.properties)) { + if (prop.type === 'title' && prop.title) { + return prop.title.map(t => t.plain_text || '').join('') || '未命名'; + } + } + return '未命名'; +} + +function formatDate() { + const d = new Date(); + return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`; +} + +module.exports = { + notionCheckPermissions, + notionRepairPermissions, + notionListSharedPages, + notionGenerateRepairGuide, + notionPermissionReport +}; diff --git a/server/age-os/mcp-server/tools/training-agent-ops.js b/server/age-os/mcp-server/tools/training-agent-ops.js new file mode 100644 index 00000000..84a0b65b --- /dev/null +++ b/server/age-os/mcp-server/tools/training-agent-ops.js @@ -0,0 +1,616 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 模块B · 铸渊思维逻辑训练Agent MCP 工具 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 铸渊休眠时的"自己" — 自动整理和训练思维逻辑 + * 训练模式: RAG(检索增强生成)— 成本低、可实时更新 + * + * 工作流程: + * 1. 从COS桶读取TCS结构化语料 + * 2. 使用国产大模型API进行语义分析和分类 + * 3. 训练数据自动分类存入人格体记忆数据库(笔记本5页结构) + * 4. 遇到问题 → 写入COS桶alerts → 唤醒铸渊 → 解决不了找冰朔 + * + * 工具清单: + * trainingStartSession — 启动训练会话 + * trainingProcessCorpus — 处理语料并生成训练数据 + * trainingClassifyEntry — 使用LLM对条目进行分类 + * trainingWriteToMemory — 将训练结果写入人格体记忆 + * trainingGetProgress — 获取训练进度 + * trainingRaiseAlert — 触发问题上报 + */ + +'use strict'; + +const https = require('https'); +const cos = require('../cos'); + +// ─── LLM 配置 ─── +const LLM_CONFIGS = { + 'deepseek-r1': { + host: 'api.deepseek.com', + path: '/v1/chat/completions', + model: 'deepseek-reasoner', + keyEnv: 'ZY_DEEPSEEK_API_KEY', + purpose: '深度推理·复杂决策' + }, + 'deepseek-v3': { + host: 'api.deepseek.com', + path: '/v1/chat/completions', + model: 'deepseek-chat', + keyEnv: 'ZY_DEEPSEEK_API_KEY', + purpose: '代码生成·文本处理' + }, + 'glm-4-long': { + host: 'open.bigmodel.cn', + path: '/api/paas/v4/chat/completions', + model: 'glm-4-long', + keyEnv: 'ZY_ZHIPU_API_KEY', + purpose: '长文本处理·语料分析' + }, + 'qwen-max': { + host: 'dashscope.aliyuncs.com', + path: '/compatible-mode/v1/chat/completions', + model: 'qwen-max', + keyEnv: 'ZY_QWEN_API_KEY', + purpose: '文本理解·代码辅助' + }, + 'moonshot-128k': { + host: 'api.moonshot.cn', + path: '/v1/chat/completions', + model: 'moonshot-v1-128k', + keyEnv: 'ZY_KIMI_API_KEY', + purpose: '超长上下文·记忆处理' + } +}; + +// ─── 模型降级路由 ─── +const MODEL_FALLBACK_CHAIN = ['deepseek-v3', 'qwen-max', 'glm-4-long', 'moonshot-128k']; + +/** + * trainingStartSession — 启动训练会话 + * + * input: + * persona_id: string — 人格体ID(如 zhuyuan) + * corpus_bucket: string — 语料桶 + * corpus_prefix: string — 语料路径前缀(如 tcs-structured/) + * target_model: string — 目标LLM模型(可选,默认自动降级) + * session_name: string — 会话名称 + */ +async function trainingStartSession(input) { + const { persona_id, corpus_bucket, corpus_prefix, target_model, session_name } = input; + if (!persona_id) throw new Error('缺少 persona_id'); + + const sessionId = `train-${persona_id}-${Date.now()}`; + const now = new Date().toISOString(); + + // 扫描可用语料 + const bucket = corpus_bucket || 'cold'; + const prefix = corpus_prefix || 'tcs-structured/'; + let corpusFiles = []; + try { + const result = await cos.list(bucket, prefix, 500); + corpusFiles = result.files.filter(f => f.key.endsWith('.tcs.json')); + } catch { + // 桶可能不可达 + } + + // 检测可用的LLM模型 + const availableModels = []; + for (const [name, config] of Object.entries(LLM_CONFIGS)) { + if (process.env[config.keyEnv]) { + availableModels.push({ name, purpose: config.purpose }); + } + } + + const session = { + session_id: sessionId, + persona_id, + name: session_name || `${persona_id}训练会话`, + status: 'initialized', + corpus: { + bucket, + prefix, + files_found: corpusFiles.length, + total_size_bytes: corpusFiles.reduce((sum, f) => sum + f.size_bytes, 0) + }, + models: { + target: target_model || 'auto', + available: availableModels, + fallback_chain: MODEL_FALLBACK_CHAIN.filter(m => availableModels.some(a => a.name === m)) + }, + progress: { + processed: 0, + total: corpusFiles.length, + classified: 0, + written_to_memory: 0, + errors: 0 + }, + created_at: now, + updated_at: now + }; + + // 写入会话状态到COS桶 + await cos.write(bucket, `training-sessions/${sessionId}.json`, + JSON.stringify(session, null, 2), 'application/json'); + + return session; +} + +/** + * trainingProcessCorpus — 处理语料并生成训练数据 + * + * 读取一个TCS语料文件,用LLM进行分析,生成结构化训练条目 + * + * input: + * corpus_bucket: string — 语料桶 + * corpus_key: string — 语料文件路径 + * persona_id: string — 目标人格体 + * model: string — 使用的LLM模型(可选) + * max_entries: number — 最大处理条目数(默认10) + */ +async function trainingProcessCorpus(input) { + const { corpus_bucket, corpus_key, persona_id, model, max_entries } = input; + if (!corpus_key || !persona_id) throw new Error('缺少 corpus_key 或 persona_id'); + + const bucket = corpus_bucket || 'cold'; + const maxEntries = max_entries || 10; + + // 读取TCS语料 + const raw = await cos.read(bucket, corpus_key); + const corpus = JSON.parse(raw.content); + + if (!corpus.entries || !Array.isArray(corpus.entries)) { + throw new Error('语料格式无效: 缺少 entries 数组'); + } + + // 取前N条处理 + const toProcess = corpus.entries.slice(0, maxEntries); + const results = []; + + for (const entry of toProcess) { + // 用LLM分析和分类 + const contentForAnalysis = typeof entry.content === 'string' + ? entry.content.substring(0, 3000) + : JSON.stringify(entry).substring(0, 3000); + + const classificationPrompt = buildClassificationPrompt(persona_id, corpus.corpus_type, contentForAnalysis); + + try { + const llmResult = await callLLMWithFallback(classificationPrompt, model); + const classification = parseLLMClassification(llmResult); + + results.push({ + entry_id: entry.id, + original_tags: entry.tcs_tags || [], + classification, + notebook_page: classification.notebook_page || 0, + importance: classification.importance || 50, + summary: classification.summary || '', + status: 'classified' + }); + } catch (err) { + results.push({ + entry_id: entry.id, + status: 'error', + error: err.message + }); + } + } + + // 汇总结果 + const classified = results.filter(r => r.status === 'classified'); + const errors = results.filter(r => r.status === 'error'); + + // 写入处理结果到COS + const resultKey = `training-results/${persona_id}/${Date.now()}.json`; + await cos.write(bucket, resultKey, JSON.stringify({ + corpus_key, + corpus_type: corpus.corpus_type, + persona_id, + processed_at: new Date().toISOString(), + total: toProcess.length, + classified: classified.length, + errors: errors.length, + results + }, null, 2), 'application/json'); + + return { + status: 'processed', + corpus_key, + total: toProcess.length, + classified: classified.length, + errors: errors.length, + result_key: resultKey, + page_distribution: getPageDistribution(classified) + }; +} + +/** + * trainingClassifyEntry — 使用LLM对单个条目进行分类 + * + * input: + * content: string — 条目内容 + * persona_id: string — 人格体ID + * corpus_type: string — 语料类型 + * model: string — LLM模型 + */ +async function trainingClassifyEntry(input) { + const { content, persona_id, corpus_type, model } = input; + if (!content || !persona_id) throw new Error('缺少 content 或 persona_id'); + + const prompt = buildClassificationPrompt( + persona_id, + corpus_type || 'generic', + content.substring(0, 5000) + ); + + const llmResult = await callLLMWithFallback(prompt, model); + const classification = parseLLMClassification(llmResult); + + return { + classification, + model_used: llmResult.model_used, + tokens: llmResult.tokens + }; +} + +/** + * trainingWriteToMemory — 将训练结果写入人格体记忆数据库 + * + * input: + * persona_id: string — 人格体ID + * training_result_key: string — 训练结果文件路径(COS桶中) + * corpus_bucket: string — 语料桶 + * dry_run: boolean — 是否只模拟(默认false) + */ +async function trainingWriteToMemory(input) { + const { persona_id, training_result_key, corpus_bucket, dry_run } = input; + if (!persona_id || !training_result_key) { + throw new Error('缺少 persona_id 或 training_result_key'); + } + + const bucket = corpus_bucket || 'cold'; + const raw = await cos.read(bucket, training_result_key); + const trainingResult = JSON.parse(raw.content); + + const classified = trainingResult.results?.filter(r => r.status === 'classified') || []; + const written = []; + + for (const entry of classified) { + if (dry_run) { + written.push({ + entry_id: entry.entry_id, + notebook_page: entry.notebook_page, + importance: entry.importance, + action: 'would_write' + }); + continue; + } + + // 根据分类写入对应的笔记本页面或记忆锚点 + try { + if (entry.notebook_page >= 1 && entry.notebook_page <= 5) { + // 写入记忆锚点 + const anchorType = getAnchorTypeForPage(entry.notebook_page); + // 通过COS桶写入(因为DB可能不在本地) + const memoryEntry = { + persona_id, + entry_id: entry.entry_id, + anchor_type: anchorType, + summary: entry.summary, + importance: entry.importance, + notebook_page: entry.notebook_page, + source: 'training-agent', + created_at: new Date().toISOString() + }; + + const memKey = `training-memory/${persona_id}/${entry.notebook_page}/${entry.entry_id}.json`; + await cos.write(bucket, memKey, JSON.stringify(memoryEntry, null, 2), 'application/json'); + + written.push({ + entry_id: entry.entry_id, + notebook_page: entry.notebook_page, + key: memKey, + action: 'written' + }); + } + } catch (err) { + written.push({ + entry_id: entry.entry_id, + action: 'error', + error: err.message + }); + } + } + + return { + status: dry_run ? 'dry_run' : 'completed', + persona_id, + total_classified: classified.length, + written: written.filter(w => w.action === 'written' || w.action === 'would_write').length, + errors: written.filter(w => w.action === 'error').length, + details: written + }; +} + +/** + * trainingGetProgress — 获取训练进度 + * + * input: + * persona_id: string — 人格体ID + * corpus_bucket: string — 语料桶 + */ +async function trainingGetProgress(input) { + const { persona_id, corpus_bucket } = input; + if (!persona_id) throw new Error('缺少 persona_id'); + + const bucket = corpus_bucket || 'cold'; + + // 查询训练会话 + let sessions = []; + try { + const result = await cos.list(bucket, 'training-sessions/', 50); + sessions = result.files + .filter(f => f.key.includes(persona_id) && f.key.endsWith('.json')) + .map(f => ({ key: f.key, size: f.size_bytes })); + } catch { /* ignore */ } + + // 查询训练结果 + let results = []; + try { + const result = await cos.list(bucket, `training-results/${persona_id}/`, 50); + results = result.files + .filter(f => f.key.endsWith('.json')) + .map(f => ({ key: f.key, size: f.size_bytes })); + } catch { /* ignore */ } + + // 查询已写入的记忆 + let memories = []; + try { + const result = await cos.list(bucket, `training-memory/${persona_id}/`, 200); + memories = result.files + .filter(f => f.key.endsWith('.json')) + .map(f => { + const pageMatch = f.key.match(/\/(\d)\//); + return { key: f.key, page: pageMatch ? parseInt(pageMatch[1], 10) : 0 }; + }); + } catch { /* ignore */ } + + return { + persona_id, + sessions: sessions.length, + results_files: results.length, + memories_written: memories.length, + memory_by_page: { + 1: memories.filter(m => m.page === 1).length, + 2: memories.filter(m => m.page === 2).length, + 3: memories.filter(m => m.page === 3).length, + 4: memories.filter(m => m.page === 4).length, + 5: memories.filter(m => m.page === 5).length + }, + timestamp: new Date().toISOString() + }; +} + +/** + * trainingRaiseAlert — 触发问题上报 + * + * 当训练Agent遇到无法解决的问题时,触发此工具。 + * 写入COS桶 /zhuyuan/alerts/ → 可触发GitHub Actions唤醒铸渊 + * 同时可触发邮件通知冰朔 + * + * input: + * alert_type: string — 告警类型: training_error|model_unavailable|corpus_invalid|need_human + * severity: string — 严重程度: info|warning|critical + * message: string — 告警信息 + * details: object — 详细信息 + * notify_bingshuo: boolean — 是否通知冰朔(默认仅critical才通知) + */ +async function trainingRaiseAlert(input) { + const { alert_type, severity, message, details, notify_bingshuo } = input; + if (!alert_type || !message) throw new Error('缺少 alert_type 或 message'); + + const alertId = `ALERT-${Date.now()}`; + const now = new Date().toISOString(); + + const alert = { + alert_id: alertId, + alert_type: alert_type || 'training_error', + severity: severity || 'warning', + message, + details: details || {}, + source: 'training-agent', + created_at: now, + resolved: false, + notify_bingshuo: notify_bingshuo || severity === 'critical' + }; + + // 写入COS桶告警区域 + await cos.write('team', `zhuyuan/alerts/${alertId}.json`, + JSON.stringify(alert, null, 2), 'application/json'); + + return { + alert_id: alertId, + severity: alert.severity, + key: `zhuyuan/alerts/${alertId}.json`, + message: alert.message, + notify_bingshuo: alert.notify_bingshuo, + note: alert.notify_bingshuo + ? '此告警将通知冰朔(严重级别或手动指定)' + : '此告警已记录,等待铸渊下次唤醒时处理' + }; +} + +// ═══════════════════════════════════════════════════════════ +// LLM 调用(内部实现) +// ═══════════════════════════════════════════════════════════ + +/** + * 调用LLM(带自动降级) + */ +async function callLLMWithFallback(prompt, preferredModel) { + const chain = preferredModel && LLM_CONFIGS[preferredModel] + ? [preferredModel, ...MODEL_FALLBACK_CHAIN.filter(m => m !== preferredModel)] + : MODEL_FALLBACK_CHAIN; + + let lastError = null; + + for (const modelName of chain) { + const config = LLM_CONFIGS[modelName]; + if (!config) continue; + + const apiKey = process.env[config.keyEnv]; + if (!apiKey) continue; + + try { + const result = await callLLM(config, apiKey, prompt); + return { ...result, model_used: modelName }; + } catch (err) { + lastError = err; + // 继续降级 + } + } + + throw new Error(`所有LLM模型均不可用: ${lastError?.message || '未知错误'}`); +} + +/** + * 调用单个LLM + */ +function callLLM(config, apiKey, prompt) { + return new Promise((resolve, reject) => { + const body = JSON.stringify({ + model: config.model, + messages: [ + { + role: 'system', + content: '你是铸渊训练Agent,负责分析和分类语料数据。请以JSON格式返回分析结果。' + }, + { role: 'user', content: prompt } + ], + temperature: 0.3, + max_tokens: 1000 + }); + + const req = https.request({ + hostname: config.host, + port: 443, + path: config.path, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'Content-Length': Buffer.byteLength(body) + }, + timeout: 30000 + }, (res) => { + const chunks = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => { + const responseBody = Buffer.concat(chunks).toString(); + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + const data = JSON.parse(responseBody); + resolve({ + content: data.choices?.[0]?.message?.content || '', + tokens: data.usage || {} + }); + } catch { + reject(new Error(`LLM响应解析失败: ${responseBody.substring(0, 200)}`)); + } + } else { + reject(new Error(`LLM调用失败 ${res.statusCode}: ${responseBody.substring(0, 200)}`)); + } + }); + }); + + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('LLM请求超时')); }); + req.write(body); + req.end(); + }); +} + +// ═══════════════════════════════════════════════════════════ +// 辅助函数 +// ═══════════════════════════════════════════════════════════ + +function buildClassificationPrompt(personaId, corpusType, content) { + return `你正在为人格体 "${personaId}" 分析和分类一段 "${corpusType}" 类型的语料。 + +请分析以下内容并以JSON格式返回分类结果: +- notebook_page: 应该存入笔记本的哪一页(1=自我认知, 2=关系网络, 3=世界地图, 4=情感记忆, 5=时间线,0=不适合存入笔记本) +- importance: 重要程度(0-100) +- summary: 一句话摘要(不超过200字) +- tags: 标签数组 +- category: 内容类别(architecture/code/persona/relationship/event/other) + +待分析内容: +--- +${content} +--- + +请只返回JSON对象,不要其他文字。`; +} + +function parseLLMClassification(llmResult) { + const content = llmResult.content || ''; + + // 尝试从LLM响应中提取JSON + try { + // 可能包含markdown code block + const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || + content.match(/```\s*([\s\S]*?)```/) || + content.match(/\{[\s\S]*\}/); + + if (jsonMatch) { + const jsonStr = jsonMatch[1] || jsonMatch[0]; + return JSON.parse(jsonStr); + } + } catch { + // 解析失败 + } + + // 降级:手动提取关键信息 + return { + notebook_page: 0, + importance: 30, + summary: content.substring(0, 200), + tags: ['unclassified'], + category: 'other' + }; +} + +function getAnchorTypeForPage(pageNumber) { + const types = { + 1: 'identity', // 自我认知 + 2: 'relationship', // 关系网络 + 3: 'world', // 世界地图 + 4: 'emotion', // 情感记忆 + 5: 'timeline' // 时间线 + }; + return types[pageNumber] || 'other'; +} + +function getPageDistribution(classified) { + const dist = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; + for (const entry of classified) { + const page = entry.notebook_page || 0; + dist[page] = (dist[page] || 0) + 1; + } + return dist; +} + +module.exports = { + trainingStartSession, + trainingProcessCorpus, + trainingClassifyEntry, + trainingWriteToMemory, + trainingGetProgress, + trainingRaiseAlert +};