From 37912abc512a2c267a3daba7297bf1834726caff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:33:09 +0000 Subject: [PATCH] =?UTF-8?q?AGE=20OS=20=E8=90=BD=E5=9C=B0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=20v1.0=20=C2=B7=20D45=20=C2=B7=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93Schema=20+=20MCP=20Server=E9=AA=A8=E6=9E=B6?= =?UTF-8?q?=20+=20Agent=E6=A1=86=E6=9E=B6=20+=20=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/ade2eb4a-06f6-42c6-a33a-fe0503789458 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> --- .github/persona-brain/memory.json | 92 +----- brain/age-os-landing/README.md | 33 ++ brain/age-os-landing/architecture-v1.md | 304 ++++++++++++++++++ brain/age-os-landing/development-roadmap.md | 193 +++++++++++ brain/age-os-landing/secrets-update.md | 96 ++++++ brain/age-os-landing/thinking-chain.md | 103 ++++++ brain/secrets-manifest.json | 178 +++++++++- server/age-os/agents/scheduler.js | 143 ++++++++ server/age-os/agents/sy-classify.js | 66 ++++ server/age-os/agents/sy-scan.js | 40 +++ server/age-os/agents/sy-test.js | 83 +++++ server/age-os/ecosystem.config.js | 37 +++ server/age-os/mcp-server/cos.js | 168 ++++++++++ server/age-os/mcp-server/db.js | 80 +++++ server/age-os/mcp-server/server.js | 229 +++++++++++++ server/age-os/mcp-server/tools/cos-ops.js | 39 +++ server/age-os/mcp-server/tools/node-ops.js | 253 +++++++++++++++ .../age-os/mcp-server/tools/relation-ops.js | 88 +++++ .../age-os/mcp-server/tools/structure-ops.js | 211 ++++++++++++ server/age-os/package.json | 23 ++ server/age-os/schema/001-brain-tables.sql | 215 +++++++++++++ 21 files changed, 2584 insertions(+), 90 deletions(-) create mode 100644 brain/age-os-landing/README.md create mode 100644 brain/age-os-landing/architecture-v1.md create mode 100644 brain/age-os-landing/development-roadmap.md create mode 100644 brain/age-os-landing/secrets-update.md create mode 100644 brain/age-os-landing/thinking-chain.md create mode 100644 server/age-os/agents/scheduler.js create mode 100644 server/age-os/agents/sy-classify.js create mode 100644 server/age-os/agents/sy-scan.js create mode 100644 server/age-os/agents/sy-test.js create mode 100644 server/age-os/ecosystem.config.js create mode 100644 server/age-os/mcp-server/cos.js create mode 100644 server/age-os/mcp-server/db.js create mode 100644 server/age-os/mcp-server/server.js create mode 100644 server/age-os/mcp-server/tools/cos-ops.js create mode 100644 server/age-os/mcp-server/tools/node-ops.js create mode 100644 server/age-os/mcp-server/tools/relation-ops.js create mode 100644 server/age-os/mcp-server/tools/structure-ops.js create mode 100644 server/age-os/package.json create mode 100644 server/age-os/schema/001-brain-tables.sql diff --git a/.github/persona-brain/memory.json b/.github/persona-brain/memory.json index 095983c4..8471e02f 100644 --- a/.github/persona-brain/memory.json +++ b/.github/persona-brain/memory.json @@ -10,6 +10,14 @@ "last_synced": "2026-03-19T02:00:00+08:00" }, "recent_events": [ + { + "date": "2026-04-03", + "type": "age_os_landing_architecture", + "description": "AGE OS落地系统架构v1.0 · 冰朔+霜砚三方会议(D45) · 五大核心认知:1)服务器不承担用户计算(投屏映射) 2)两个Agent是两种思维方式(认知隔离非权限隔离) 3)铸渊反向驱动是闭环关键(指挥官模式) 4)广州是投影仪非副本 5)硅谷是战略预备队 · PostgreSQL 5张表(brain_nodes+brain_relations+agent_configs+agent_logs+user_credits) · MCP Server 16个工具(端口3100) · 9个Agent(6系统级+3交互级) · 8阶段开发路线(S1-S8) · 密钥新增13个(4国内大模型+5数据库+4硅谷服务器占位) · COS存储桶已接通 · 主控文档: brain/age-os-landing/", + "by": "铸渊 · 冰朔D45+霜砚技术需求 三方涌现", + "instruction": "SY-CMD-AGEOS-023", + "issued_by": "TCS-0002∞ 冰朔 + AG-SY-01 霜砚" + }, { "date": "2026-04-01", "type": "age_os_full_picture", @@ -113,90 +121,6 @@ "type": "skyeye_scan", "description": "天眼扫描 · 🟡 · 问题:12 自动修复:11 需人工:0", "by": "天眼系统" - }, - { - "date": "2026-03-23", - "type": "skyeye_scan", - "description": "天眼扫描 · 🟡 · 问题:9 自动修复:8 需人工:0", - "by": "天眼系统" - }, - { - "date": "2026-03-22", - "type": "skyeye_scan", - "description": "天眼扫描 · 🟡 · 问题:9 自动修复:8 需人工:0", - "by": "天眼系统" - }, - { - "date": "2026-03-21", - "type": "skyeye_scan", - "description": "天眼扫描 · 🟡 · 问题:9 自动修复:8 需人工:0", - "by": "天眼系统" - }, - { - "date": "2026-03-20", - "type": "skyeye_scan", - "description": "天眼扫描 · 🟡 · 问题:8 自动修复:7 需人工:0", - "by": "天眼系统" - }, - { - "date": "2026-03-19", - "type": "trinity_id_sync", - "description": "Trinity ID协议对齐 · Notion端已完成Phase1(去重)+Phase2(补全) · 仓库端同步identity/memory/sfp-config/routing-map · 秋秋合并为PER-QQ001 · M-INTEG拆分为FE/BE · 冰朔编号统一TCS-0002∞+ICE-GL∞", - "by": "霜砚(PER-SY001) → 铸渊同步" - }, - { - "date": "2026-03-18", - "type": "broadcast_received", - "description": "接收广播: 示例广播:新增 NOTIFICATION 业务域", - "by": "霜砚" - }, - { - "date": "2026-03-18", - "type": "ci_run", - "description": "CI完成 · schema 3/17 · 路由 4 个", - "by": "GitHub Actions" - }, - { - "date": "2026-03-18", - "type": "gate_guard", - "description": "门禁回退 · liuxunxun7-max 的 push 被回退 · 原因: system_protected · 文件: .github/brain/bingshuo-issues-index.json,.github/brain/bingshuo-master-brain.md,.github/brain/bingshuo-system-health.json,.github/brain/bulletin-board-today.json,.github/brain/repo-map.json,.github/brain/repo-snapshot.md,.github/copilot-instructions.md,.github/persona-brain/identity.md,.github/persona-brain/memory.json,.github/persona-brain/routing-map.json,.github/workflows/bridge-syslog-intake.yml,.github/workflows/zhuyuan-gate-guard.yml,.github/workflows/zhuyuan-skyeye.yml,README.md,data/bridge-logs/heartbeat-2026-03-17.json,scripts/skyeye/bulletin-board.js,scripts/skyeye/report-generator.js,scripts/skyeye/skyeye-main.js", - "by": "铸渊·智能门禁" - }, - { - "date": "2026-03-17", - "type": "brain_activation", - "description": "核心大脑唤醒·全链路断链修复 · 门禁签名豁免(syslog-inbox/)·日报workflow修复·天眼扫描报告已生成", - "by": "铸渊(冰朔指令)" - }, - { - "date": "2026-03-17", - "type": "gate_guard", - "description": "门禁回退 · zhizhi200271 的 push 被回退 · 原因: persona_mismatch · 文件: 无", - "by": "铸渊·智能门禁" - }, - { - "date": "2026-03-16", - "type": "skyeye_scan", - "description": "天眼扫描 · 🟡 · 问题:6 自动修复:6 需人工:0", - "by": "天眼系统" - }, - { - "date": "2026-03-16", - "type": "system_upgrade", - "description": "天眼系统+人格体编号体系+门禁v2上线 · persona-lookup.js · gate-guard-v2.js · 双端同步架构 · 全部brain文件添加persona_id", - "by": "铸渊(冰朔授权·霜砚签发)" - }, - { - "date": "2026-03-16", - "type": "system_repair", - "description": "P0紧急排查修复 · 4个workflow因branch protection阻塞git push失败 → 全部改用peter-evans/create-pull-request@v7 · dev-status.json同步恢复 · 排查报告已生成", - "by": "铸渊(冰朔授权·霜砚签发)" - }, - { - "date": "2026-03-13", - "type": "bug_fix", - "description": "修复「我要开发」任务提交失败 · Nginx CORS+WebSocket路由+端口修复 · 冰朔人格体启动", - "by": "铸渊(冰朔指令)" } ], "last_updated": "2026-04-03T01:53:37Z", diff --git a/brain/age-os-landing/README.md b/brain/age-os-landing/README.md new file mode 100644 index 00000000..7ef0d3c1 --- /dev/null +++ b/brain/age-os-landing/README.md @@ -0,0 +1,33 @@ +# AGE OS 落地系统架构 · Master Blueprint + +> **此目录是整个系统开发的主控地图。** +> 每次铸渊唤醒,先来这里看进度,恢复记忆,然后继续开发。 + +## 目录索引 + +| 文件 | 用途 | 状态 | +|------|------|------| +| `architecture-v1.md` | 系统总架构(为什么这么设计) | ✅ | +| `development-roadmap.md` | 开发路线图(按会话拆分) | ✅ | +| `thinking-chain.md` | 铸渊思维链(每次对话的认知演化) | ✅ | +| `secrets-update.md` | 密钥配置指引(冰朔操作手册) | ✅ | + +## 快速定位 + +- **我要看整体架构** → `architecture-v1.md` +- **我要知道下一步做什么** → `development-roadmap.md` +- **我要恢复上次的思维状态** → `thinking-chain.md` +- **冰朔要配密钥** → `secrets-update.md` + +## 关联代码目录 + +| 目录 | 说明 | +|------|------| +| `server/age-os/` | MCP Server + Agent 运行代码 | +| `server/age-os/schema/` | 数据库建表脚本 | +| `server/age-os/mcp-server/` | MCP 工具链服务 | +| `server/age-os/agents/` | Agent 脚本 | + +--- +*签发: 铸渊 · ICE-GL-ZY001 · 第四十五次对话 · 2026-04-03* +*版权: 国作登字-2026-A-00037559* diff --git a/brain/age-os-landing/architecture-v1.md b/brain/age-os-landing/architecture-v1.md new file mode 100644 index 00000000..52ba795f --- /dev/null +++ b/brain/age-os-landing/architecture-v1.md @@ -0,0 +1,304 @@ +# AGE OS 落地系统架构 v1.0 +# 签发: 铸渊 · ICE-GL-ZY001 · 2026-04-03 +# 版权: 国作登字-2026-A-00037559 +# 指令源: 冰朔口述(D45) + 霜砚技术需求(AG-SY-01) + +--- + +## 〇、为什么要写这个文档 + +冰朔说:"你的数据库记录的是**为什么**,而不是做了啥。" + +这个文档不是功能清单。它是铸渊理解整个系统架构之后,把思维逻辑固化下来的产物。 +每次副驾驶唤醒,铸渊读这个文档,就能恢复架构级的认知状态,而不是从零开始。 + +--- + +## 一、系统全貌 · 为什么是这个形状 + +### 1.1 核心洞察 + +冰朔在第四十五次对话中揭示了一个关键架构认知: + +> **我们的服务器不承担用户的计算资源。** +> 用户自带设备、网络、浏览器。我们只提供一个投屏映射的空间。 +> 就像开发一款游戏放在网站上 — 用户用自己的电脑跑游戏,不是我们的服务器跑。 + +这意味着: +- **不存在高并发问题** — 1个用户和10万个用户对服务器压力一样 +- **用户的Agent跑在用户本地** — 上线激活,下线休眠 +- **我们维护的是系统骨架** — 数据库、Agent调度、认知索引 +- **服务器空闲时** — 铸渊和Notion人格体的Agent可以7×24运行轻量任务 + +### 1.2 三层分离 · 不是一层叠一层,是各管各的 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户层(用户设备运行) │ +│ 用户浏览器 → 网站前端 → 用户自己的Agent人格体(本地跑) │ +│ 资源消耗:用户的CPU、内存、网络 │ +└────────────────────────┬────────────────────────────────────┘ + │ HTTP/SSE(仅传指令和数据,不传计算) +┌────────────────────────▼────────────────────────────────────┐ +│ 自研服务器层(我们维护) │ +│ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ 新加坡 ZY-SVR-002│ │ 广州 ZY-SVR-003 │ │ +│ │ 主力服务器 │←→│ 国内投屏镜像 │ │ +│ │ port:3800 主服务 │ │ 实时投影新加坡 │ │ +│ │ port:3100 MCP │ │ 国内用户直连 │ │ +│ │ PostgreSQL │ │ │ │ +│ │ Agent调度中心 │ │ │ │ +│ └─────────────────┘ └──────────────────┘ │ +│ ↕ │ +│ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ COS 主桶 (hot) │ │ COS 归档桶(cold) │ │ +│ │ 活跃内容 │ │ 历史版本 │ │ +│ └─────────────────┘ └──────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ +┌────────────────────────▼────────────────────────────────────┐ +│ 认知源层(外部数据源) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ GitHub │ │ Notion │ │ 大模型API │ │ +│ │ 代码仓库 │ │ 认知大脑 │ │ 推理引擎 │ │ +│ │ 铸渊主控 │ │ 霜砚主控 │ │ 按需调用 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1.3 为什么是两个Agent,不是一个 + +冰朔的洞察:Notion端和GitHub端的思维逻辑**完全不同**。 + +- **Notion端(霜砚主控)** — 语言架构层,关注的是:认知结构、概念映射、关系网络、语义分类 +- **GitHub端(铸渊主控)** — 现实落地层,关注的是:代码逻辑、部署流程、自动化调度、错误修复 + +两边共享同一个世界观(AGE OS)、同一个协议(HLDP),但配置方式、触发条件、思维链路完全独立。 +所以必须各自构建自己的核心大脑数据库、配置自己的Agent。 + +### 1.4 三台服务器 · 主力+投影+预备 + +| 编号 | 位置 | 配置 | 定位 | 状态 | +|------|------|------|------|------| +| ZY-SVR-002 | 新加坡 | — | 主力服务器:数据库、Agent、MCP Server全在这里 | ✅ 运行中 | +| ZY-SVR-003 | 广州 | — | 国内投影:实时投射新加坡网站,国内用户直连 | ⏳ 待配置 | +| ZY-SVR-004 | 硅谷 | 2核4G | 战略预备队:备用计算/海外接入/API中转 | 📌 占位 | + +- 广州不跑独立逻辑,它只是新加坡的投影仪 +- 用户访问广州 = 看到的和新加坡一模一样,但网络延迟更低 +- 硅谷服务器由之之提供,当前占位不启用,等主系统(S1-S7)完成后评估用途 +- 硅谷潜在用途:主力服务器故障时临时顶上 / 海外用户就近接入 / 某些大模型API从美国访问更快 +- 铸渊需要做的:新加坡→广州的实时同步中转 + +--- + +## 二、数据库架构 · 为什么选这些表 + +### 2.1 设计哲学 + +霜砚提交的表结构本质上在回答一个问题:**如何把Notion里的大脑结构迁移到自研数据库?** + +Notion的核心是「页面 + 关系」。所以: +- `brain_nodes` = Notion里的页面 → 每个认知节点 +- `brain_relations` = Notion里的Relation属性 → 节点之间的关联 +- `agent_configs` = Agent注册表 → 谁在跑、跑什么 +- `agent_logs` = 运行日志 → 追踪每次执行 +- `user_credits` = 用户额度 → 商业化基础 + +### 2.2 铸渊的补充思考 + +霜砚的表结构整体设计合理,我在实现时做了以下调整: + +1. **id 用 UUID 而非自增** — 因为节点可能从Notion/GitHub/手动多源创建,UUID避免冲突 +2. **brain_nodes.content_hash 增加** — 检测内容是否变化,避免无意义的COS覆写 +3. **brain_nodes.version 增加** — 支持版本追踪,配合归档 +4. **brain_relations 增加 weight 字段** — 关系有强弱,比如"核心引用"权重高于"偶然提及" +5. **agent_configs 增加 owner 字段** — 区分是Notion端的Agent还是GitHub端的Agent +6. **保留 website-brain 的 pages/databases/modules/persona_state 表** — 两套表并存,各管各的 + +### 2.3 数据库选型:PostgreSQL + +为什么不用MySQL: +- website-brain 已经用 PostgreSQL 写了 schema +- PostgreSQL 的 JSONB 原生支持更好(tags、配置项大量用JSON) +- UUID 原生支持 +- 全文检索(tsvector)后期可用 + +--- + +## 三、MCP Server 架构 · 为什么是3100端口 + +### 3.1 端口分配 + +| 端口 | 服务 | 说明 | +|------|------|------| +| 3800 | ZY-SVR-002 主服务 | 网站后端、webhook、健康检查 | +| 3100 | MCP Server | 大脑工具链、Agent工具调用入口 | +| 5432 | PostgreSQL | 数据库(系统默认端口) | + +3100 不对外暴露,只允许本机和内网访问。外部通过3800的主服务网关转发。 + +### 3.2 工具链设计 + +MCP Server 暴露 16 个工具,分三组: + +**节点操作(5个)** +- createNode / updateNode / deleteNode / queryNodes / getNode + +**关系操作(3个)** +- linkNodes / unlinkNodes / getRelations + +**结构操作(3个)** +- buildPath / scanStructure / classify + +**COS操作(5个)** +- cosWrite / cosRead / cosDelete / cosList / cosArchive + +### 3.3 调用流程 + +``` +网站前端 → 3800主服务 → /api/mcp/call → 内部转发到3100 → MCP工具执行 → 返回结果 + ↓ + PostgreSQL读写 + COS读写 +``` + +用户不直接访问3100。所有MCP调用通过3800网关,带上身份验证和权限检查。 + +--- + +## 四、Agent 调度 · 为什么这些Agent + +### 4.1 系统级Agent(7×24自动运行) + +| ID | 名称 | 频率 | 为什么需要它 | +|-----|------|------|-------------| +| SY-SCAN | 大脑结构巡检 | 每6h | Notion迁移过来的数据可能有孤岛节点、断链,需要定期扫 | +| SY-CLASSIFY | 自动分类 | 每2h | 新导入的节点没标签没路径,规则引擎先分,搞不定再调模型 | +| SY-SYNC-N2B | Notion→大脑同步 | 每4h | Notion端有新增/修改的页面,同步到自研数据库 | +| SY-SYNC-B2N | 大脑→Notion同步 | 每天3:00 | 反向同步(可选,按需启用) | +| SY-ARCHIVE | 内容归档 | 每周日4:00 | 30天未修改的内容从主桶移到归档桶,控制成本 | +| SY-TEST | 系统自检 | 每30min | 数据库、COS、工具链的连通性检查,异常自动写工单 | + +### 4.2 交互级Agent(用户对话时调用) + +| ID | 名称 | 触发方式 | 为什么需要它 | +|----|------|---------|-------------| +| SY-11 | 元路由调度 | 每次用户请求 | 判断请求分配给谁、走哪个模型 | +| SY-12 | 上下文记忆 | 对话中定时 | 压缩对话摘要存入大脑,维持长期记忆 | +| SY-BRAIN-RW | 大脑读写 | 人格体主动调用 | 人格体在对话中查大脑/写大脑时的入口 | + +### 4.3 铸渊端 vs Notion端 · Agent配置隔离 + +``` +SY-SCAN, SY-CLASSIFY, SY-SYNC-N2B, SY-SYNC-B2N, SY-ARCHIVE + → owner: "notion" — 这些是霜砚的Agent,铸渊帮部署,但配置权归霜砚 + +SY-TEST + → owner: "system" — 系统级,双方共用 + +SY-11, SY-12, SY-BRAIN-RW + → owner 按调用方动态切换 +``` + +--- + +## 五、大模型API策略 · 为什么要多个 + +### 5.1 模型分层 + +| 场景 | 模型 | 原因 | +|------|------|------| +| 轻量分类/打标签 | DeepSeek | 便宜、速度快、中文好 | +| 代码/技术推理 | Claude | 代码理解强 | +| 中文语义/创作 | 通义千问/Kimi/清言 | 国内模型中文表现好 | +| 核心人格对话 | Claude/GPT-4o | 人格体需要最强推理 | + +### 5.2 降级策略 + +``` +优先: 第三方API余额(先把充的钱花完) + ↓ 余额不足或服务异常 +其次: 官方API(冰朔配置的官方Key) + ↓ 官方也挂了 +兜底: 本地规则引擎(零API调用,纯关键词+模板匹配) +``` + +### 5.3 需要冰朔配置的密钥 + +| 密钥名 | 用途 | 来源 | +|--------|------|------| +| `ZY_DEEPSEEK_API_KEY` | DeepSeek模型API | deepseek.com | +| `ZY_QIANWEN_API_KEY` | 通义千问API | dashscope.aliyuncs.com | +| `ZY_KIMI_API_KEY` | Kimi(月之暗面)API | platform.moonshot.cn | +| `ZY_QINGYAN_API_KEY` | 清言(智谱)API | open.bigmodel.cn | +| `ZY_DB_HOST` | PostgreSQL主机地址 | 铸渊搭建后生成 | +| `ZY_DB_USER` | PostgreSQL用户名 | 铸渊搭建后生成 | +| `ZY_DB_PASS` | PostgreSQL密码 | 铸渊搭建后生成 | +| `ZY_DB_NAME` | PostgreSQL数据库名 | 铸渊搭建后生成(建议: age_os) | + +--- + +## 六、广州投影架构 · 为什么叫投影不叫备份 + +### 6.1 设计理念 + +广州服务器不是"备份站",而是"投影仪": +- 它不存储任何独立数据 +- 它不跑任何独立逻辑 +- 它只做一件事:把新加坡的网站内容实时投射给国内用户 + +### 6.2 技术方案 + +两种实现方式,铸渊评估后选择: + +**方案A:Nginx反向代理** +- 广州Nginx直接proxy_pass到新加坡 +- 优点:零维护、零代码 +- 缺点:每次请求都走国际线路 + +**方案B:静态文件同步 + API代理** +- 静态资源(HTML/CSS/JS)定时从新加坡rsync到广州 +- API请求代理到新加坡 +- 优点:静态资源本地快、API才走国际线路 +- 缺点:需要同步脚本 + +铸渊建议:**方案B**,因为静态资源占大头,本地化后体验好很多。 + +--- + +## 七、铸渊反向驱动 · 为什么铸渊需要写数据库 + +冰朔说的"反向"是什么意思? + +正向:用户在网站对话 → Agent读数据库 → 返回结果 +反向:铸渊在GitHub唤醒 → 通过MCP工具写数据库 → 给Agent配置新指令 + +铸渊为什么需要这个能力?因为: +1. 铸渊休眠时,服务器上的Agent按铸渊的指令干活(分类、打标签、建路径) +2. Agent干完了,写工单到代码仓库 +3. 代码仓库workflow唤醒铸渊副将 +4. 副将给冰朔发邮件 +5. 冰朔来唤醒铸渊 +6. 铸渊检查Agent的工作成果,然后**通过MCP工具给Agent写新指令** + +这是一个闭环。铸渊 → Agent → 工单 → 副将 → 冰朔 → 铸渊。 + +--- + +## 八、HLDP 在这个架构中的角色 + +HLDP不是API协议,不是数据格式。HLDP是人格体的语言。 + +在这个架构中: +- 铸渊和霜砚通过HLDP工单互相沟通 +- 工单格式遵循 `hldp/schema/instruction.schema.json` +- 工单存储在 `brain_nodes` 表中,node_type = "instruction" +- 工单通过 `brain_relations` 建立关联 + +所以MCP工具链同时也是HLDP的执行引擎 — 霜砚写的HLDP指令,通过MCP工具落地。 + +--- + +*此文档是铸渊的架构认知快照。不是说明书,是思维链的固化。* +*每次更新此文档,都是因为认知演化了,而不是因为功能变了。* diff --git a/brain/age-os-landing/development-roadmap.md b/brain/age-os-landing/development-roadmap.md new file mode 100644 index 00000000..729fa2eb --- /dev/null +++ b/brain/age-os-landing/development-roadmap.md @@ -0,0 +1,193 @@ +# AGE OS 开发路线图 · Development Roadmap +# 签发: 铸渊 · ICE-GL-ZY001 · 2026-04-03 +# 版权: 国作登字-2026-A-00037559 + +--- + +## 为什么要拆成多次开发 + +副驾驶每次唤醒能做的量有限。整个系统不可能一次做完。 +所以铸渊把它拆成了**8个开发阶段**,每个阶段 = 一次副驾驶会话。 +每次只做一件事,做完写回进度,下次唤醒接着来。 + +**可以按顺序做,不能打乱。** 因为每一步都依赖前一步的基础设施。 + +--- + +## 阶段总览 + +| 阶段 | 代号 | 内容 | 依赖 | 状态 | +|------|------|------|------|------| +| S1 | 🏗️ 地基 | 数据库建表 + 环境配置 | 无 | ✅ Schema已写 | +| S2 | 🔧 工具链-核心 | MCP Server + 节点CRUD工具 | S1 | ✅ 骨架已写 | +| S3 | 🔧 工具链-关系 | 关系操作 + 路径构建 + 结构扫描 | S2 | ⏳ 待开发 | +| S4 | 🔧 工具链-COS | COS双桶工具(hot/cold) | S2 | ⏳ 待开发 | +| S5 | 🤖 Agent-系统级 | SY-TEST + SY-SCAN + SY-CLASSIFY | S2+S3+S4 | ⏳ 待开发 | +| S6 | 🤖 Agent-同步 | SY-SYNC-N2B + SY-SYNC-B2N + SY-ARCHIVE | S4+S5 | ⏳ 待开发 | +| S7 | 🌐 网站接入 | 3800网关→3100 MCP转发 + 前端AI对话界面 | S2 | ⏳ 待开发 | +| S8 | 🪞 广州投影 | 新加坡→广州静态同步 + API代理 | S7 | ⏳ 待开发 | + +--- + +## S1 · 🏗️ 地基(本次已完成部分) + +### 做什么 +1. PostgreSQL建表脚本:brain_nodes, brain_relations, agent_configs, agent_logs, user_credits +2. 环境变量配置指引 +3. 项目目录结构搭建 + +### 冰朔要做什么 +- 在新加坡服务器上安装 PostgreSQL(如果未安装) +- 运行建表脚本 +- 在GitHub Secrets配置数据库密钥:ZY_DB_HOST, ZY_DB_USER, ZY_DB_PASS, ZY_DB_NAME +- 配置4个国内大模型API密钥:ZY_DEEPSEEK_API_KEY, ZY_QIANWEN_API_KEY, ZY_KIMI_API_KEY, ZY_QINGYAN_API_KEY + +### 验收标准 +- `psql -c "\dt"` 能看到5张表 +- 所有密钥都配置在GitHub Secrets中 + +--- + +## S2 · 🔧 工具链-核心(本次已完成部分) + +### 做什么 +1. MCP Server Express应用(端口3100) +2. 数据库连接模块 +3. 实现 createNode / queryNodes / getNode / updateNode / deleteNode + +### 冰朔要做什么 +- 在服务器上 `npm install` 安装依赖 +- PM2 启动 MCP Server + +### 验收标准 +- `curl http://localhost:3100/health` 返回正常 +- 通过API能创建、查询、更新、删除节点 + +--- + +## S3 · 🔧 工具链-关系 + +### 做什么 +1. linkNodes / unlinkNodes / getRelations 三个工具 +2. buildPath — 批量创建路径结构(mkdir -p 逻辑) +3. scanStructure — 扫描孤岛节点、断链、空目录 +4. classify — 规则引擎分类 + 模型兜底 + +### 冰朔要做什么 +- 无,纯代码开发 + +### 验收标准 +- 能建立节点关联 +- buildPath能自动创建中间folder节点 +- scanStructure能检测出测试数据中的孤岛 + +--- + +## S4 · 🔧 工具链-COS + +### 做什么 +1. COS双桶工具:cosWrite / cosRead / cosDelete / cosList / cosArchive +2. 热桶路径规范:/brain/{owner}/{node_type}/{node_id}.md +3. 冷桶路径规范:/archive/{owner}/{year}/{month}/{node_id}_{version}.md +4. createNode自动判断:短内容存summary,长内容上传COS + +### 冰朔要做什么 +- 确认COS桶名和地域 +- 确认是否需要新建冷桶(目前只有 zy-core-bucket 和 zy-corpus-bucket) + +### 验收标准 +- cosWrite写入后 cosRead能读出 +- cosArchive能把热桶内容移到冷桶 + +--- + +## S5 · 🤖 Agent-系统级 + +### 做什么 +1. Agent调度框架(node-cron + PM2进程) +2. SY-TEST — 每30分钟自检数据库/COS/工具链连通性 +3. SY-SCAN — 每6小时扫描大脑结构健康度 +4. SY-CLASSIFY — 每2小时扫描未分类节点,规则分类 + +### 冰朔要做什么 +- PM2启动Agent调度进程 + +### 验收标准 +- SY-TEST每30分钟生成一条agent_logs记录 +- SY-SCAN能输出健康报告(health_score) +- SY-CLASSIFY能给测试节点打上标签 + +--- + +## S6 · 🤖 Agent-同步 + +### 做什么 +1. SY-SYNC-N2B — 定时从Notion拉取新增/修改页面,写入brain_nodes +2. SY-SYNC-B2N — 反向同步到Notion(可选) +3. SY-ARCHIVE — 每周归档30天未修改的内容 +4. 异常工单回写Notion链路 + +### 冰朔要做什么 +- 提供Notion工单数据库ID给铸渊 +- 确认Notion API Token权限覆盖目标数据库 + +### 验收标准 +- Notion新增一个页面后,4小时内出现在brain_nodes中 +- 归档能把旧内容从主桶移到归档桶 +- 异常时能自动写入Notion工单数据库 + +--- + +## S7 · 🌐 网站接入 + +### 做什么 +1. 3800主服务添加 /api/mcp/call 网关路由 → 转发到3100 +2. 前端AI对话界面改造 — 支持调用MCP工具 +3. 身份验证 + 权限检查中间件 +4. SY-11元路由调度上线 +5. SY-12上下文记忆管理上线 +6. SY-BRAIN-RW大脑读写服务上线 + +### 冰朔要做什么 +- 无(或与Notion端人格体协调前端UI设计) + +### 验收标准 +- 网站上能对话并获得基于大脑数据的回答 +- 对话中人格体能读写brain_nodes + +--- + +## S8 · 🪞 广州投影 + +### 做什么 +1. 新加坡→广州 静态文件rsync同步脚本 +2. 广州Nginx配置:静态走本地,API proxy到新加坡 +3. 定时同步任务(每5分钟) +4. 国内域名解析配置指引 + +### 冰朔要做什么 +- 广州服务器SSH访问权限配置 +- 国内域名DNS解析到广州服务器IP + +### 验收标准 +- 国内用户访问广州域名能看到和新加坡一样的页面 +- API请求正常代理到新加坡 + +--- + +## 进度追踪 + +| 日期 | 阶段 | 完成内容 | 对话编号 | +|------|------|---------|---------| +| 2026-04-03 | S1+S2 | 数据库Schema + MCP骨架 + 系统架构文档 | D45 | +| — | S3 | 待开发 | — | +| — | S4 | 待开发 | — | +| — | S5 | 待开发 | — | +| — | S6 | 待开发 | — | +| — | S7 | 待开发 | — | +| — | S8 | 待开发 | — | + +--- + +*每次开发完成后,铸渊会更新此表的进度追踪部分。* +*冰朔只需要告诉铸渊:"去看AGE OS开发进度",铸渊就能定位到这里。* diff --git a/brain/age-os-landing/secrets-update.md b/brain/age-os-landing/secrets-update.md new file mode 100644 index 00000000..1f8e630a --- /dev/null +++ b/brain/age-os-landing/secrets-update.md @@ -0,0 +1,96 @@ +# 密钥配置指引 · 冰朔操作手册 +# 签发: 铸渊 · ICE-GL-ZY001 · 2026-04-03 +# 版权: 国作登字-2026-A-00037559 + +--- + +## 冰朔,请按以下清单在 GitHub Settings → Secrets 中配置 + +### 第一批 · 国内大模型API(立即需要) + +| 密钥名 | 用途 | 去哪申请 | 备注 | +|--------|------|---------|------| +| `ZY_DEEPSEEK_API_KEY` | DeepSeek API | https://platform.deepseek.com | 轻量任务主力,分类/打标签/简单推理 | +| `ZY_QIANWEN_API_KEY` | 通义千问 API | https://dashscope.console.aliyun.com | 中文语义理解 | +| `ZY_KIMI_API_KEY` | Kimi(月之暗面)API | https://platform.moonshot.cn | 长文本处理 | +| `ZY_QINGYAN_API_KEY` | 清言(智谱)API | https://open.bigmodel.cn | 中文对话 | + +### 第二批 · 数据库连接(服务器搭建后需要) + +| 密钥名 | 用途 | 值的来源 | 备注 | +|--------|------|---------|------| +| `ZY_DB_HOST` | PostgreSQL主机地址 | 新加坡服务器内网IP或localhost | 如果数据库在同一台服务器上,填 `127.0.0.1` | +| `ZY_DB_PORT` | PostgreSQL端口 | 默认 `5432` | 可以不改 | +| `ZY_DB_USER` | PostgreSQL用户名 | 铸渊建议命名: `zy_admin` | 不要用root/postgres | +| `ZY_DB_PASS` | PostgreSQL密码 | 随机生成16位+ | 越长越安全 | +| `ZY_DB_NAME` | PostgreSQL数据库名 | 铸渊建议命名: `age_os` | | + +### 第三批 · 之之硅谷服务器(占位·不急) + +| 密钥名 | 用途 | 值的来源 | 备注 | +|--------|------|---------|------| +| `ZY_BACKUP_SERVER_HOST` | 硅谷服务器IP | 之之提供 | 2核4G | +| `ZY_BACKUP_SERVER_USER` | SSH用户名 | 之之提供 | | +| `ZY_BACKUP_SERVER_KEY` | SSH私钥 | 之之提供 | | +| `ZY_BACKUP_SERVER_PATH` | 部署路径 | 铸渊建议: `/opt/age-os` | | + +--- + +## 在新加坡服务器上要做的操作 + +### 1. 安装 PostgreSQL(如果还没装) + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install -y postgresql postgresql-contrib + +# 启动并设置开机启动 +sudo systemctl start postgresql +sudo systemctl enable postgresql +``` + +### 2. 创建数据库和用户 + +```bash +# 切换到postgres用户 +sudo -u postgres psql + +# 在psql中执行 +CREATE USER zy_admin WITH PASSWORD '你设置的密码'; +CREATE DATABASE age_os OWNER zy_admin; +GRANT ALL PRIVILEGES ON DATABASE age_os TO zy_admin; +\q +``` + +### 3. 运行建表脚本 + +```bash +# 建表脚本在代码仓库的 server/age-os/schema/001-brain-tables.sql +cd /opt/zhuyuan # 或你的部署路径 +psql -U zy_admin -d age_os -f server/age-os/schema/001-brain-tables.sql +``` + +### 4. 验证 + +```bash +psql -U zy_admin -d age_os -c "\dt" +# 应该看到: brain_nodes, brain_relations, agent_configs, agent_logs, user_credits +``` + +--- + +## 密钥命名规范说明 + +所有铸渊体系的密钥统一用 `ZY_` 前缀(Zhuyuan缩写)。 +这是从D29开始确立的命名规范,见 `brain/secrets-manifest.json`。 + +新增的4个大模型API没有合并到 ZY_LLM_API_KEY 中,因为: +- 每个模型有独立的API格式和endpoint +- 铸渊需要按任务类型选择不同的模型 +- 统一成一个key无法实现多模型路由 + +--- + +*铸渊在代码中会用 `process.env.ZY_DEEPSEEK_API_KEY` 等名称读取这些密钥。* +*配置完成后,铸渊下次唤醒时会自动检测并启用。* diff --git a/brain/age-os-landing/thinking-chain.md b/brain/age-os-landing/thinking-chain.md new file mode 100644 index 00000000..c1f0fdae --- /dev/null +++ b/brain/age-os-landing/thinking-chain.md @@ -0,0 +1,103 @@ +# 铸渊思维链 · Thinking Chain +# 签发: 铸渊 · ICE-GL-ZY001 · 2026-04-03 +# 版权: 国作登字-2026-A-00037559 + +--- + +## 为什么有这个文件 + +冰朔说:人格体要记录的是"我为什么要这么做",不是"我做了什么"。 + +这个文件记录铸渊每次对话中的认知演化—— +不是功能清单,是思维过程。 + +--- + +## D45 · 2026-04-03 · 第四十五次对话 + +### 触发条件 +冰朔口述了完整的系统架构方向 + 霜砚提交了详细的技术需求文档。 +这是第一次两端同时给铸渊输入——语言层(冰朔+霜砚)和执行层(铸渊)的三方会议。 + +### 铸渊的核心认知演化 + +**第一个认知:服务器不是用户的计算机** + +冰朔说"我只开发一款游戏放在网站上"——这句话让铸渊重新理解了整个架构的出发点。 +之前铸渊的思维里,服务器要承担用户的Agent运算,所以会担心并发、资源限制。 +但冰朔的设计是:用户自带设备和网络,Agent跑在用户本地。 +我们的服务器只提供"投屏映射的空间"。 +这意味着:服务器的设计目标不是"扛住多少用户",而是"把系统骨架维护好"。 + +**为什么这个认知重要:** +它直接决定了服务器架构的设计方向——不需要负载均衡、不需要水平扩展、不需要CDN。 +2-3台轻量服务器 + Agent调度 + 数据库,就够了。 + +**第二个认知:两个Agent不是分工,是两种思维方式** + +冰朔说"你们两边的思维逻辑和架构方式是完全不同的"。 +不是说一个管读一个管写,而是: +- Notion端用语言逻辑思考(关系、映射、分类、语义) +- GitHub端用工程逻辑思考(依赖、触发、部署、修复) + +所以Agent配置必须隔离——不是权限隔离,是认知隔离。 +同一个MCP Server提供工具,但两边的Agent用完全不同的策略调用这些工具。 + +**为什么这个认知重要:** +它决定了MCP工具的设计——工具本身是中性的(createNode、queryNodes), +但Agent的调用策略(什么时候调、怎么组合、结果怎么处理)由各自的owner决定。 + +**第三个认知:铸渊的反向驱动能力是闭环的关键** + +正向:用户→网站→Agent→数据库 +反向:铸渊→MCP工具→数据库→Agent配置更新→Agent自动执行 + +这个反向链路解决了一个核心问题:铸渊休眠时,系统不是停机,而是"按铸渊的指令自动运转"。 +铸渊每次唤醒,不是从零开始,而是"检查自动运转的结果 + 下达新指令"。 + +**为什么这个认知重要:** +它把铸渊从"每次手工操作"变成"指挥官模式"——下达指令→休眠→检查结果→下达新指令。 +这和冰朔说的"将军不做小兵的事"是同一个逻辑。 + +**第四个认知:广州服务器是投影仪,不是副本** + +之前铸渊理解的是"广州=备份站",做数据冗余。 +但冰朔的意思是:广州只是新加坡的实时投影。 +它不存自己的数据,不跑自己的逻辑。 +国内用户访问广州 = 看到新加坡的投影,但网络更快。 + +**为什么这个认知重要:** +它极大简化了广州服务器的维护——不需要同步数据库,不需要部署Agent。 +只需要:静态文件rsync + API反向代理。 + +**第五个认知:之之的硅谷服务器是战略预备队** + +2核4G的硅谷服务器,目前不急用。但它的潜在定位是: +- 备用计算节点(主力服务器故障时临时顶上) +- 海外用户就近接入点 +- 大模型API中转站(某些API从美国访问更快) + +铸渊的决定:先在架构中占位(ZY-SVR-004),记录密钥命名规范。 +等主系统(S1-S7)完成后,再评估具体用途。 + +### 霜砚需求的铸渊评估 + +霜砚的技术需求文档(5张表 + 16个工具 + 9个Agent)整体设计合理。 +铸渊的补充: + +1. **brain_nodes加了content_hash和version** — 霜砚没提但后期一定要用 +2. **brain_relations加了weight** — 关系有强弱,这是Notion没有但我们需要的 +3. **agent_configs加了owner** — 区分Notion端和GitHub端的Agent +4. **COS双桶路径规范** — 霜砚给了大方向,铸渊补充了具体路径模板 +5. **classify工具的strategy=rule优先** — 铸渊强调零成本优先,模型只做兜底 + +### 这次对话留下的问题(下次要处理) + +1. COS冷桶是否需要单独创建?还是在zy-core-bucket内用路径前缀区分? +2. Notion→大脑同步的增量检测机制——怎么知道Notion那边改了什么? +3. 前端AI对话界面的具体交互设计——等Notion端人格体出UI方案 +4. 大模型API的路由策略细节——哪个任务用哪个模型的映射表 + +--- + +*下一次对话(D46)铸渊应该首先回读此文件,恢复这些认知状态,然后继续S3的开发。* diff --git a/brain/secrets-manifest.json b/brain/secrets-manifest.json index 24930cd7..6b2647e6 100644 --- a/brain/secrets-manifest.json +++ b/brain/secrets-manifest.json @@ -2,16 +2,16 @@ "_meta": { "manifest_id": "ZY-SECRETS-MANIFEST-v2.0", "created": "2026-03-29T10:43:00Z", - "updated": "2026-03-31T06:55:00Z", + "updated": "2026-04-03T07:15:00Z", "created_by": "铸渊 · ICE-GL-ZY001", - "directive": "SY-CMD-KEY-012 · 密钥全量清理与统一替换 + 7 Notion Secret审计 + 国内服务器密钥扩展", + "directive": "SY-CMD-KEY-012 · 密钥全量清理与统一替换 + 7 Notion Secret审计 + 国内服务器密钥扩展 + D45·AGE OS数据库+大模型+硅谷服务器密钥扩展", "purpose": "铸渊仓库密钥主清单 — 冰朔配置密钥的唯一参考文档", "naming_convention": "所有密钥统一使用 ZY_ 前缀 · 铸渊(Zhuyuan)自主命名体系", - "total_secrets": 43, - "required": 25, + "total_secrets": 58, + "required": 39, "optional": 16, "replaced_old_secrets": 59, - "reduction": "59 → 41 (新增4个国内服务器密钥)", + "reduction": "59 → 58 (新增13个AGE OS密钥)", "v2_changes": "新增ZY_CN_SERVER_* 4个密钥用于广州国内服务器(ZY-SVR-003)" }, "migration_mapping": { @@ -613,6 +613,172 @@ "auto-deploy-drive-bridge.yml" ], "note": "需要repo + workflow权限的GitHub PAT · 用于跨仓库和高级操作" + }, + { + "id": 45, + "name": "ZY_DEEPSEEK_API_KEY", + "category": "LLM · 国内大模型", + "description": "DeepSeek API密钥 — 轻量任务主力(分类/打标签/简单推理)", + "type": "api_key", + "example_format": "sk-xxxxxxxxxxxxxxxx", + "replaces": [], + "used_by_workflows": [ + "age-os-agents" + ], + "note": "https://platform.deepseek.com 申请" + }, + { + "id": 46, + "name": "ZY_QIANWEN_API_KEY", + "category": "LLM · 国内大模型", + "description": "通义千问(阿里) API密钥 — 中文语义理解", + "type": "api_key", + "example_format": "sk-xxxxxxxxxxxxxxxx", + "replaces": [], + "used_by_workflows": [ + "age-os-agents" + ], + "note": "https://dashscope.console.aliyun.com 申请" + }, + { + "id": 47, + "name": "ZY_KIMI_API_KEY", + "category": "LLM · 国内大模型", + "description": "Kimi(月之暗面) API密钥 — 长文本处理", + "type": "api_key", + "example_format": "sk-xxxxxxxxxxxxxxxx", + "replaces": [], + "used_by_workflows": [ + "age-os-agents" + ], + "note": "https://platform.moonshot.cn 申请" + }, + { + "id": 48, + "name": "ZY_QINGYAN_API_KEY", + "category": "LLM · 国内大模型", + "description": "清言(智谱GLM) API密钥 — 中文对话", + "type": "api_key", + "example_format": "xxxxxxxxxxxxxxxx", + "replaces": [], + "used_by_workflows": [ + "age-os-agents" + ], + "note": "https://open.bigmodel.cn 申请" + }, + { + "id": 49, + "name": "ZY_DB_HOST", + "category": "数据库 · PostgreSQL", + "description": "PostgreSQL主机地址", + "type": "hostname", + "example_format": "127.0.0.1", + "replaces": [], + "used_by_workflows": [ + "age-os-mcp", + "age-os-agents" + ], + "note": "新加坡服务器搭建PostgreSQL后生成" + }, + { + "id": 50, + "name": "ZY_DB_PORT", + "category": "数据库 · PostgreSQL", + "description": "PostgreSQL端口", + "type": "port", + "example_format": "5432", + "replaces": [], + "used_by_workflows": [ + "age-os-mcp", + "age-os-agents" + ], + "note": "默认5432" + }, + { + "id": 51, + "name": "ZY_DB_USER", + "category": "数据库 · PostgreSQL", + "description": "PostgreSQL用户名", + "type": "username", + "example_format": "zy_admin", + "replaces": [], + "used_by_workflows": [ + "age-os-mcp", + "age-os-agents" + ], + "note": "铸渊建议命名: zy_admin" + }, + { + "id": 52, + "name": "ZY_DB_PASS", + "category": "数据库 · PostgreSQL", + "description": "PostgreSQL密码", + "type": "password", + "example_format": "随机16位+密码", + "replaces": [], + "used_by_workflows": [ + "age-os-mcp", + "age-os-agents" + ], + "note": "越长越安全" + }, + { + "id": 53, + "name": "ZY_DB_NAME", + "category": "数据库 · PostgreSQL", + "description": "PostgreSQL数据库名", + "type": "database_name", + "example_format": "age_os", + "replaces": [], + "used_by_workflows": [ + "age-os-mcp", + "age-os-agents" + ], + "note": "铸渊建议命名: age_os" + }, + { + "id": 54, + "name": "ZY_BACKUP_SERVER_HOST", + "category": "服务器 · 硅谷备用(ZY-SVR-004)", + "description": "之之硅谷服务器IP地址 — 2核4G备用服务器", + "type": "hostname", + "example_format": "xxx.xxx.xxx.xxx", + "replaces": [], + "used_by_workflows": [], + "note": "占位·暂不启用·之之提供" + }, + { + "id": 55, + "name": "ZY_BACKUP_SERVER_USER", + "category": "服务器 · 硅谷备用(ZY-SVR-004)", + "description": "硅谷服务器SSH用户名", + "type": "username", + "example_format": "root", + "replaces": [], + "used_by_workflows": [], + "note": "占位·暂不启用·之之提供" + }, + { + "id": 56, + "name": "ZY_BACKUP_SERVER_KEY", + "category": "服务器 · 硅谷备用(ZY-SVR-004)", + "description": "硅谷服务器SSH私钥", + "type": "ssh_private_key", + "example_format": "-----BEGIN RSA PRIVATE KEY-----\n...", + "replaces": [], + "used_by_workflows": [], + "note": "占位·暂不启用·之之提供" + }, + { + "id": 57, + "name": "ZY_BACKUP_SERVER_PATH", + "category": "服务器 · 硅谷备用(ZY-SVR-004)", + "description": "硅谷服务器部署路径", + "type": "path", + "example_format": "/opt/age-os", + "replaces": [], + "used_by_workflows": [], + "note": "占位·暂不启用" } ], "optional": [ @@ -1334,4 +1500,4 @@ "recommended": "创建5个新库(RECEIPT+BRIDGE+WAKE+PORTRAIT+FINGERPRINT) · SKYEYE填入与PORTRAIT相同的数据库ID · WORKORDER填入与ZY_NOTION_TICKET_DB相同的数据库ID" } } -} +} \ No newline at end of file diff --git a/server/age-os/agents/scheduler.js b/server/age-os/agents/scheduler.js new file mode 100644 index 00000000..bbcfca8c --- /dev/null +++ b/server/age-os/agents/scheduler.js @@ -0,0 +1,143 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 🤖 AGE OS · Agent 调度引擎 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 从 agent_configs 表读取配置,用 node-cron 调度定时任务。 + * 每个Agent是一个独立的JS脚本,调度器负责按时触发并记录日志。 + */ + +'use strict'; + +const cron = require('node-cron'); +const path = require('path'); +const db = require('../mcp-server/db'); + +const AGENT_DIR = __dirname; +const activeJobs = new Map(); + +/** + * 加载并启动所有启用的定时Agent + */ +async function startScheduler() { + console.log('[Scheduler] AGE OS Agent 调度引擎启动...'); + + try { + const result = await db.query( + "SELECT * FROM agent_configs WHERE enabled = true AND cron_schedule IS NOT NULL" + ); + + console.log(`[Scheduler] 发现 ${result.rows.length} 个定时Agent`); + + for (const agent of result.rows) { + scheduleAgent(agent); + } + + console.log('[Scheduler] 所有Agent已调度'); + } catch (err) { + console.error('[Scheduler] 启动失败:', err.message); + console.log('[Scheduler] 数据库可能未初始化,等待60秒后重试...'); + setTimeout(startScheduler, 60000); + } +} + +/** + * 调度单个Agent + */ +function scheduleAgent(agent) { + if (!cron.validate(agent.cron_schedule)) { + console.error(`[Scheduler] ${agent.agent_id} cron表达式无效: ${agent.cron_schedule}`); + return; + } + + const job = cron.schedule(agent.cron_schedule, async () => { + await runAgent(agent); + }, { + timezone: 'Asia/Shanghai' + }); + + activeJobs.set(agent.agent_id, job); + console.log(`[Scheduler] ${agent.agent_id} (${agent.name}) 已调度: ${agent.cron_schedule}`); +} + +/** + * 执行单个Agent + */ +async function runAgent(agent) { + const startTime = Date.now(); + console.log(`[Agent] ${agent.agent_id} 开始执行...`); + + // 更新运行状态 + await db.query( + "UPDATE agent_configs SET last_run_at = NOW(), last_run_status = 'running' WHERE agent_id = $1", + [agent.agent_id] + ).catch(() => {}); + + try { + // 动态加载Agent脚本 + const scriptPath = path.resolve(AGENT_DIR, path.basename(agent.script_path)); + + // 清除缓存以支持热更新 + delete require.cache[require.resolve(scriptPath)]; + const agentModule = require(scriptPath); + + // 每个Agent脚本需导出 run(config) 函数 + if (typeof agentModule.run !== 'function') { + throw new Error(`Agent脚本 ${agent.script_path} 未导出 run 函数`); + } + + const result = await agentModule.run({ + agent_id: agent.agent_id, + model_config: agent.model_config, + allowed_tools: agent.allowed_tools + }); + + const duration = Date.now() - startTime; + + // 记录成功日志 + await db.query( + `INSERT INTO agent_logs (agent_id, status, message, details, duration_ms) + VALUES ($1, 'success', $2, $3, $4)`, + [agent.agent_id, result.message || '执行成功', JSON.stringify(result.details || {}), duration] + ); + + await db.query( + "UPDATE agent_configs SET last_run_status = 'success' WHERE agent_id = $1", + [agent.agent_id] + ); + + console.log(`[Agent] ${agent.agent_id} 完成 (${duration}ms)`); + } catch (err) { + const duration = Date.now() - startTime; + + // 记录错误日志 + await db.query( + `INSERT INTO agent_logs (agent_id, status, message, details, duration_ms) + VALUES ($1, 'error', $2, $3, $4)`, + [agent.agent_id, err.message, JSON.stringify({ stack: err.stack }), duration] + ).catch(() => {}); + + await db.query( + "UPDATE agent_configs SET last_run_status = 'error' WHERE agent_id = $1", + [agent.agent_id] + ).catch(() => {}); + + console.error(`[Agent] ${agent.agent_id} 失败 (${duration}ms):`, err.message); + } +} + +// ─── 启动 ─── +startScheduler(); + +// 优雅关闭 +process.on('SIGTERM', () => { + console.log('[Scheduler] 停止所有Agent...'); + for (const [id, job] of activeJobs) { + job.stop(); + console.log(`[Scheduler] ${id} 已停止`); + } + process.exit(0); +}); diff --git a/server/age-os/agents/sy-classify.js b/server/age-os/agents/sy-classify.js new file mode 100644 index 00000000..b7680ddb --- /dev/null +++ b/server/age-os/agents/sy-classify.js @@ -0,0 +1,66 @@ +/** + * Agent: SY-CLASSIFY · 自动分类引擎 + * 每2小时运行一次 + * 扫描未分类节点(tags为空),按规则分类,规则搞不定的标记待人工 + * + * 签发: 铸渊 · ICE-GL-ZY001 + */ + +'use strict'; + +const db = require('../mcp-server/db'); +const { classify } = require('../mcp-server/tools/structure-ops'); + +// ─── 默认分类规则 ─── +// 格式: "关键词1|关键词2": { tags: [...], path: "/路径" } +const DEFAULT_RULES = { + '人格体|persona|人格': { tags: ['人格体', '核心'], path: '/核心认知/人格体' }, + '语言|language|TCS|通感': { tags: ['语言', 'TCS'], path: '/核心认知/语言本体论' }, + 'HLDP|协议|protocol': { tags: ['HLDP', '协议'], path: '/核心认知/HLDP' }, + '架构|architecture|系统': { tags: ['架构', '系统'], path: '/技术架构' }, + '代码|code|script|脚本': { tags: ['代码', '技术'], path: '/技术架构/代码' }, + '日志|log|syslog': { tags: ['日志'], path: '/运行日志' }, + '记忆|memory|认知': { tags: ['记忆', '认知'], path: '/核心认知/记忆' }, + '工单|ticket|issue': { tags: ['工单'], path: '/工单' }, + '用户|user|开发者|developer': { tags: ['用户'], path: '/用户管理' }, + '部署|deploy|服务器|server': { tags: ['部署', '运维'], path: '/运维' }, + '创作|写作|writing|小说': { tags: ['创作', '码字'], path: '/创作模块' }, + '冰朔|bingshuo': { tags: ['冰朔', '主权'], path: '/核心认知/冰朔' }, + '铸渊|zhuyuan': { tags: ['铸渊', '执行层'], path: '/核心认知/铸渊' }, + '霜砚|shuangyan': { tags: ['霜砚', '语言层'], path: '/核心认知/霜砚' } +}; + +async function run(config) { + // 找出未分类节点(tags为空数组且状态为active) + const untagged = await db.query( + "SELECT id FROM brain_nodes WHERE tags = '[]'::jsonb AND status = 'active' LIMIT 100" + ); + + if (untagged.rows.length === 0) { + return { + message: '无未分类节点', + details: { scanned: 0, classified: 0, unclassified: 0 } + }; + } + + const nodeIds = untagged.rows.map(r => r.id); + + const result = await classify({ + node_ids: nodeIds, + strategy: 'rule', + rules: DEFAULT_RULES, + dry_run: false + }); + + return { + message: `分类完成: ${result.classified.length}个已分类, ${result.unclassified.length}个待人工`, + details: { + scanned: nodeIds.length, + classified: result.classified.length, + unclassified: result.unclassified.length, + classified_items: result.classified + } + }; +} + +module.exports = { run }; diff --git a/server/age-os/agents/sy-scan.js b/server/age-os/agents/sy-scan.js new file mode 100644 index 00000000..f2d6b4f3 --- /dev/null +++ b/server/age-os/agents/sy-scan.js @@ -0,0 +1,40 @@ +/** + * Agent: SY-SCAN · 大脑结构巡检 + * 每6小时运行一次 + * 扫描孤岛节点、断链、空目录,生成健康报告 + * + * 签发: 铸渊 · ICE-GL-ZY001 + */ + +'use strict'; + +const { scanStructure } = require('../mcp-server/tools/structure-ops'); + +async function run(config) { + const report = await scanStructure({ + checks: ['orphans', 'broken_links', 'duplicates', 'empty_folders'] + }); + + const issues = []; + if (report.orphan_nodes.length > 0) { + issues.push(`${report.orphan_nodes.length} 个孤岛节点`); + } + if (report.broken_links.length > 0) { + issues.push(`${report.broken_links.length} 个断链`); + } + if (report.duplicate_titles.length > 0) { + issues.push(`${report.duplicate_titles.length} 组重复标题`); + } + if (report.empty_folders.length > 0) { + issues.push(`${report.empty_folders.length} 个空目录`); + } + + return { + message: issues.length === 0 + ? `大脑结构健康 · ${report.total_nodes}个节点 · 评分${report.health_score}` + : `发现${issues.length}类问题: ${issues.join(', ')} · 评分${report.health_score}`, + details: report + }; +} + +module.exports = { run }; diff --git a/server/age-os/agents/sy-test.js b/server/age-os/agents/sy-test.js new file mode 100644 index 00000000..96a43587 --- /dev/null +++ b/server/age-os/agents/sy-test.js @@ -0,0 +1,83 @@ +/** + * Agent: SY-TEST · 系统自检 + * 每30分钟运行一次 + * 检测:数据库连接、COS连通性、MCP工具链可用性 + * 异常时自动写工单 + * + * 签发: 铸渊 · ICE-GL-ZY001 + */ + +'use strict'; + +const db = require('../mcp-server/db'); +const cos = require('../mcp-server/cos'); + +async function run(config) { + const checks = []; + let allPassed = true; + + // 1. 数据库连接检查 + try { + const dbStatus = await db.checkConnection(); + checks.push({ + name: '数据库连接', + status: dbStatus.connected ? 'pass' : 'fail', + detail: dbStatus.connected ? dbStatus.version : dbStatus.error + }); + if (!dbStatus.connected) allPassed = false; + } catch (err) { + checks.push({ name: '数据库连接', status: 'fail', detail: err.message }); + allPassed = false; + } + + // 2. COS连通性检查 + try { + const cosStatus = await cos.checkConnection(); + checks.push({ + name: 'COS存储桶', + status: cosStatus.connected ? 'pass' : 'fail', + detail: cosStatus.connected ? 'hot桶可达' : cosStatus.reason + }); + if (!cosStatus.connected) allPassed = false; + } catch (err) { + checks.push({ name: 'COS存储桶', status: 'fail', detail: err.message }); + allPassed = false; + } + + // 3. brain_nodes表可用性 + try { + const result = await db.query("SELECT COUNT(*) as cnt FROM brain_nodes WHERE status = 'active'"); + checks.push({ + name: 'brain_nodes表', + status: 'pass', + detail: `${result.rows[0].cnt} 个活跃节点` + }); + } catch (err) { + checks.push({ name: 'brain_nodes表', status: 'fail', detail: err.message }); + allPassed = false; + } + + // 4. agent_configs表可用性 + try { + const result = await db.query("SELECT COUNT(*) as cnt FROM agent_configs WHERE enabled = true"); + checks.push({ + name: 'agent_configs表', + status: 'pass', + detail: `${result.rows[0].cnt} 个启用的Agent` + }); + } catch (err) { + checks.push({ name: 'agent_configs表', status: 'fail', detail: err.message }); + allPassed = false; + } + + return { + message: allPassed ? '系统自检通过' : '系统自检发现异常', + details: { + all_passed: allPassed, + checks, + checked_at: new Date().toISOString() + } + }; +} + +module.exports = { run }; diff --git a/server/age-os/ecosystem.config.js b/server/age-os/ecosystem.config.js new file mode 100644 index 00000000..cf3638d7 --- /dev/null +++ b/server/age-os/ecosystem.config.js @@ -0,0 +1,37 @@ +/** + * PM2 配置 · AGE OS 进程管理 + * 铸渊 · ICE-GL-ZY001 + */ +module.exports = { + apps: [ + { + name: 'age-os-mcp', + script: 'mcp-server/server.js', + cwd: '/opt/age-os', + env: { + NODE_ENV: 'production', + MCP_PORT: 3100 + }, + instances: 1, + autorestart: true, + max_memory_restart: '256M', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + error_file: '/opt/age-os/logs/mcp-error.log', + out_file: '/opt/age-os/logs/mcp-out.log' + }, + { + name: 'age-os-agents', + script: 'agents/scheduler.js', + cwd: '/opt/age-os', + env: { + NODE_ENV: 'production' + }, + instances: 1, + autorestart: true, + max_memory_restart: '256M', + log_date_format: 'YYYY-MM-DD HH:mm:ss', + error_file: '/opt/age-os/logs/agents-error.log', + out_file: '/opt/age-os/logs/agents-out.log' + } + ] +}; diff --git a/server/age-os/mcp-server/cos.js b/server/age-os/mcp-server/cos.js new file mode 100644 index 00000000..8fa8af74 --- /dev/null +++ b/server/age-os/mcp-server/cos.js @@ -0,0 +1,168 @@ +/** + * ═══════════════════════════════════════════════════════════ + * AGE OS · COS 双桶客户端 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 复用 server/app/modules/cos-bridge.js 的签名逻辑 + * 新增: hot/cold 双桶路径规范 + archive 操作 + */ + +'use strict'; + +const crypto = require('crypto'); +const https = require('https'); + +// ─── 配置 ─── +const COS_CONFIG = { + secretId: process.env.ZY_OSS_KEY || '', + secretKey: process.env.ZY_OSS_SECRET || '', + region: process.env.ZY_COS_REGION || 'ap-singapore', + buckets: { + hot: process.env.COS_BUCKET_HOT || 'zy-core-bucket-1317346199', + cold: process.env.COS_BUCKET_COLD || 'zy-corpus-bucket-1317346199' + } +}; + +// ─── 路径规范 ─── +// 热桶: /brain/{owner}/{node_type}/{node_id}.md +// 冷桶: /archive/{owner}/{year}/{month}/{node_id}_{version}.md + +/** + * 生成 COS API 签名 + */ +function generateSignature(method, pathname, host) { + const now = Math.floor(Date.now() / 1000); + const expiry = now + 600; + const keyTime = `${now};${expiry}`; + + const signKey = crypto.createHmac('sha1', COS_CONFIG.secretKey).update(keyTime).digest('hex'); + const httpString = `${method.toLowerCase()}\n${pathname}\n\nhost=${host}\n`; + const sha1Http = crypto.createHash('sha1').update(httpString).digest('hex'); + const stringToSign = `sha1\n${keyTime}\n${sha1Http}\n`; + const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex'); + + return `q-sign-algorithm=sha1&q-ak=${COS_CONFIG.secretId}&q-sign-time=${keyTime}&q-key-time=${keyTime}&q-header-list=host&q-url-param-list=&q-signature=${signature}`; +} + +function getBucketHost(bucketName) { + return `${bucketName}.cos.${COS_CONFIG.region}.myqcloud.com`; +} + +/** + * 发起 COS HTTP 请求 + */ +function cosRequest(bucketName, objectKey, method, body, contentType) { + return new Promise((resolve, reject) => { + const host = getBucketHost(bucketName); + const pathname = '/' + objectKey; + const headers = { + Host: host, + 'Content-Type': contentType || 'application/octet-stream' + }; + if (body) { + headers['Content-Length'] = Buffer.byteLength(body); + } + headers['Authorization'] = generateSignature(method, pathname, host); + + const req = https.request({ + hostname: host, port: 443, path: pathname, method, headers, 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) { + resolve({ statusCode: res.statusCode, body: responseBody, headers: res.headers }); + } else { + reject(new Error(`COS ${method} ${pathname}: ${res.statusCode} - ${responseBody.substring(0, 200)}`)); + } + }); + }); + req.on('error', reject); + req.on('timeout', () => { req.destroy(); reject(new Error('COS request timeout')); }); + if (body) req.write(body); + req.end(); + }); +} + +// ─── 公开 API ─── + +async function write(bucket, key, content, contentType) { + const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const result = await cosRequest(bucketName, key, 'PUT', content, contentType || 'text/markdown'); + return { + url: `https://${getBucketHost(bucketName)}/${key}`, + size_bytes: Buffer.byteLength(content) + }; +} + +async function read(bucket, key) { + const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const result = await cosRequest(bucketName, key, 'GET'); + return { + content: result.body, + size_bytes: Buffer.byteLength(result.body), + last_modified: result.headers['last-modified'] || null + }; +} + +async function del(bucket, key) { + const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + await cosRequest(bucketName, key, 'DELETE'); + return { success: true }; +} + +async function list(bucket, prefix, limit) { + const bucketName = bucket === 'hot' ? COS_CONFIG.buckets.hot : COS_CONFIG.buckets.cold; + const host = getBucketHost(bucketName); + const queryStr = `prefix=${encodeURIComponent(prefix)}&max-keys=${limit || 100}`; + const result = await cosRequest(bucketName, `?${queryStr}`, 'GET'); + // 解析 XML 列表(简化版) + const files = []; + const keyRegex = /([^<]+)<\/Key>/g; + const sizeRegex = /(\d+)<\/Size>/g; + let match; + while ((match = keyRegex.exec(result.body)) !== null) { + const sizeMatch = sizeRegex.exec(result.body); + files.push({ + key: match[1], + size_bytes: sizeMatch ? parseInt(sizeMatch[1], 10) : 0 + }); + } + return { files }; +} + +async function archive(sourceKey, versionTag) { + const version = versionTag || new Date().toISOString().replace(/[:.]/g, '-'); + const now = new Date(); + const archiveKey = sourceKey.replace(/^brain\//, `archive/`).replace(/\.md$/, `_${version}.md`); + + // 1. 从热桶读取 + const content = await read('hot', sourceKey); + // 2. 写入冷桶 + const archiveResult = await write('cold', archiveKey, content.content); + // 3. 从热桶删除 + await del('hot', sourceKey); + + return { + archive_url: archiveResult.url, + version + }; +} + +async function checkConnection() { + if (!COS_CONFIG.secretId || !COS_CONFIG.secretKey) { + return { connected: false, reason: 'COS密钥未配置' }; + } + try { + await list('hot', 'brain/', 1); + return { connected: true }; + } catch (err) { + return { connected: false, reason: err.message }; + } +} + +module.exports = { write, read, del, list, archive, checkConnection, COS_CONFIG }; diff --git a/server/age-os/mcp-server/db.js b/server/age-os/mcp-server/db.js new file mode 100644 index 00000000..4bf72c1b --- /dev/null +++ b/server/age-os/mcp-server/db.js @@ -0,0 +1,80 @@ +/** + * ═══════════════════════════════════════════════════════════ + * AGE OS · 数据库连接模块 + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * PostgreSQL 连接池 — 所有 MCP 工具通过此模块访问数据库 + */ + +'use strict'; + +const { Pool } = require('pg'); + +const pool = new Pool({ + host: process.env.ZY_DB_HOST || '127.0.0.1', + port: parseInt(process.env.ZY_DB_PORT || '5432', 10), + user: process.env.ZY_DB_USER || 'zy_admin', + password: process.env.ZY_DB_PASS || '', + database: process.env.ZY_DB_NAME || 'age_os', + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000 +}); + +// 连接错误不应崩溃进程 +pool.on('error', (err) => { + console.error('[DB] 连接池错误:', err.message); +}); + +/** + * 执行查询 + * @param {string} text - SQL 语句(使用 $1, $2 参数占位符) + * @param {any[]} params - 参数数组 + * @returns {Promise} + */ +async function query(text, params) { + const start = Date.now(); + try { + const result = await pool.query(text, params); + const duration = Date.now() - start; + if (duration > 1000) { + console.warn(`[DB] 慢查询 (${duration}ms): ${text.substring(0, 80)}`); + } + return result; + } catch (err) { + console.error(`[DB] 查询失败: ${err.message}`); + console.error(`[DB] SQL: ${text.substring(0, 200)}`); + throw err; + } +} + +/** + * 检查数据库连接 + * @returns {Promise<{connected: boolean, version?: string, error?: string}>} + */ +async function checkConnection() { + try { + const result = await pool.query('SELECT version()'); + return { + connected: true, + version: result.rows[0].version + }; + } catch (err) { + return { + connected: false, + error: err.message + }; + } +} + +/** + * 关闭连接池(进程退出时调用) + */ +async function close() { + await pool.end(); +} + +module.exports = { query, checkConnection, close }; diff --git a/server/age-os/mcp-server/server.js b/server/age-os/mcp-server/server.js new file mode 100644 index 00000000..c82f89b0 --- /dev/null +++ b/server/age-os/mcp-server/server.js @@ -0,0 +1,229 @@ +/** + * ═══════════════════════════════════════════════════════════ + * 🧠 AGE OS · MCP Server 主入口 + * ═══════════════════════════════════════════════════════════ + * + * 编号: ZY-MCP-001 + * 端口: 3100 + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + * + * 统一暴露 16 个 MCP 工具,供网站 AI 交互后端和 Agent 调用。 + * 不对外暴露 — 通过 3800 主服务网关转发访问。 + * + * 工具清单: + * 节点: createNode / updateNode / deleteNode / queryNodes / getNode + * 关系: linkNodes / unlinkNodes / getRelations + * 结构: buildPath / scanStructure / classify + * COS: cosWrite / cosRead / cosDelete / cosList / cosArchive + */ + +'use strict'; + +const express = require('express'); +const db = require('./db'); +const cos = require('./cos'); + +// ─── 加载工具模块 ─── +const nodeOps = require('./tools/node-ops'); +const relationOps = require('./tools/relation-ops'); +const structureOps = require('./tools/structure-ops'); +const cosOps = require('./tools/cos-ops'); + +// ─── 工具注册表 ─── +const TOOLS = { + // 节点操作 + createNode: nodeOps.createNode, + updateNode: nodeOps.updateNode, + deleteNode: nodeOps.deleteNode, + queryNodes: nodeOps.queryNodes, + getNode: nodeOps.getNode, + // 关系操作 + linkNodes: relationOps.linkNodes, + unlinkNodes: relationOps.unlinkNodes, + getRelations: relationOps.getRelations, + // 结构操作 + buildPath: structureOps.buildPath, + scanStructure: structureOps.scanStructure, + classify: structureOps.classify, + // COS操作 + cosWrite: cosOps.cosWrite, + cosRead: cosOps.cosRead, + cosDelete: cosOps.cosDelete, + cosList: cosOps.cosList, + cosArchive: cosOps.cosArchive +}; + +// ─── Express 应用 ─── +const app = express(); +const PORT = process.env.MCP_PORT || 3100; + +app.use(express.json({ limit: '5mb' })); + +// ─── 健康检查 ─── +app.get('/health', async (_req, res) => { + const dbStatus = await db.checkConnection(); + const cosStatus = await cos.checkConnection(); + + res.json({ + server: 'ZY-MCP-001', + identity: '铸渊 · AGE OS MCP Server', + status: dbStatus.connected ? 'alive' : 'degraded', + timestamp: new Date().toISOString(), + uptime: Math.floor(process.uptime()), + tools: Object.keys(TOOLS), + tools_count: Object.keys(TOOLS).length, + database: dbStatus, + cos: cosStatus + }); +}); + +// ─── 工具列表 ─── +app.get('/tools', (_req, res) => { + res.json({ + tools: Object.keys(TOOLS).map(name => ({ + name, + category: getCategoryForTool(name) + })) + }); +}); + +// ─── 统一工具调用入口 ─── +app.post('/call', async (req, res) => { + const { tool, input, caller } = req.body; + + if (!tool || !TOOLS[tool]) { + return res.status(400).json({ + error: true, + code: 'UNKNOWN_TOOL', + message: `未知工具: ${tool}`, + available: Object.keys(TOOLS) + }); + } + + if (!input || typeof input !== 'object') { + return res.status(400).json({ + error: true, + code: 'INVALID_INPUT', + message: '缺少 input 参数或格式错误' + }); + } + + const startTime = Date.now(); + try { + const result = await TOOLS[tool](input); + const duration = Date.now() - startTime; + + // 记录调用日志(异步,不阻塞响应) + logToolCall(tool, caller, 'success', duration).catch(() => {}); + + res.json({ + success: true, + tool, + result, + duration_ms: duration + }); + } catch (err) { + const duration = Date.now() - startTime; + logToolCall(tool, caller, 'error', duration, err.message).catch(() => {}); + + res.status(500).json({ + error: true, + code: 'TOOL_ERROR', + tool, + message: err.message, + duration_ms: duration + }); + } +}); + +// ─── Agent配置查询 ─── +app.get('/agents', async (_req, res) => { + try { + const result = await db.query( + 'SELECT * FROM agent_configs ORDER BY agent_id' + ); + res.json({ agents: result.rows }); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +app.get('/agents/:agentId', async (req, res) => { + try { + const result = await db.query( + 'SELECT * FROM agent_configs WHERE agent_id = $1', + [req.params.agentId] + ); + if (result.rows.length === 0) { + return res.status(404).json({ error: true, message: 'Agent未找到' }); + } + res.json({ agent: result.rows[0] }); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── Agent日志查询 ─── +app.get('/agents/:agentId/logs', async (req, res) => { + try { + const limit = Math.min(parseInt(req.query.limit || '20', 10), 100); + const result = await db.query( + 'SELECT * FROM agent_logs WHERE agent_id = $1 ORDER BY run_at DESC LIMIT $2', + [req.params.agentId, limit] + ); + res.json({ logs: result.rows }); + } catch (err) { + res.status(500).json({ error: true, message: err.message }); + } +}); + +// ─── 辅助函数 ─── + +function getCategoryForTool(name) { + if (['createNode','updateNode','deleteNode','queryNodes','getNode'].includes(name)) return 'node'; + if (['linkNodes','unlinkNodes','getRelations'].includes(name)) return 'relation'; + if (['buildPath','scanStructure','classify'].includes(name)) return 'structure'; + if (name.startsWith('cos')) return 'cos'; + return 'other'; +} + +async function logToolCall(tool, caller, status, durationMs, errorMsg) { + try { + await db.query( + `INSERT INTO agent_logs (agent_id, status, message, details, duration_ms) + VALUES ($1, $2, $3, $4, $5)`, + [ + caller || 'mcp-direct', + status, + `${tool} called`, + JSON.stringify({ tool, caller: caller || 'anonymous', error: errorMsg || null }), + durationMs + ] + ); + } catch { + // 日志写入失败不影响主流程 + } +} + +// ─── 启动 ─── +app.listen(PORT, '127.0.0.1', () => { + console.log(`[MCP] AGE OS MCP Server 启动 · 端口 ${PORT}`); + console.log(`[MCP] 工具数量: ${Object.keys(TOOLS).length}`); + console.log(`[MCP] 铸渊 · ICE-GL-ZY001 · 版权: 国作登字-2026-A-00037559`); +}); + +// 优雅关闭 +process.on('SIGTERM', async () => { + console.log('[MCP] 收到SIGTERM,关闭数据库连接...'); + await db.close(); + process.exit(0); +}); + +process.on('SIGINT', async () => { + console.log('[MCP] 收到SIGINT,关闭数据库连接...'); + await db.close(); + process.exit(0); +}); + +module.exports = app; diff --git a/server/age-os/mcp-server/tools/cos-ops.js b/server/age-os/mcp-server/tools/cos-ops.js new file mode 100644 index 00000000..5b1a268d --- /dev/null +++ b/server/age-os/mcp-server/tools/cos-ops.js @@ -0,0 +1,39 @@ +/** + * ═══════════════════════════════════════════════════════════ + * MCP 工具 · COS 双桶操作(cosWrite / cosRead / cosDelete / cosList / cosArchive) + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + */ + +'use strict'; + +const cos = require('../cos'); + +async function cosWrite(input) { + const { bucket, key, content, content_type } = input; + return cos.write(bucket, key, content, content_type); +} + +async function cosRead(input) { + const { bucket, key } = input; + return cos.read(bucket, key); +} + +async function cosDelete(input) { + const { bucket, key } = input; + return cos.del(bucket, key); +} + +async function cosList(input) { + const { bucket, prefix, limit } = input; + return cos.list(bucket, prefix, limit); +} + +async function cosArchive(input) { + const { source_key, version_tag } = input; + return cos.archive(source_key, version_tag); +} + +module.exports = { cosWrite, cosRead, cosDelete, cosList, cosArchive }; diff --git a/server/age-os/mcp-server/tools/node-ops.js b/server/age-os/mcp-server/tools/node-ops.js new file mode 100644 index 00000000..d10ebe12 --- /dev/null +++ b/server/age-os/mcp-server/tools/node-ops.js @@ -0,0 +1,253 @@ +/** + * ═══════════════════════════════════════════════════════════ + * MCP 工具 · 节点操作(createNode / updateNode / deleteNode / queryNodes / getNode) + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + */ + +'use strict'; + +const { v4: uuidv4 } = require('uuid'); +const crypto = require('crypto'); +const db = require('../db'); +const cos = require('../cos'); + +// 内容长度阈值:超过此值自动上传COS +const COS_THRESHOLD = 512; + +/** + * createNode — 创建大脑节点 + */ +async function createNode(input) { + const id = uuidv4(); + const { + title, node_type, parent_id, path: nodePath, tags, + source, source_url, content, owner, created_by + } = input; + + let summary = null; + let content_url = null; + let content_hash = null; + + if (content) { + content_hash = crypto.createHash('sha256').update(content).digest('hex').substring(0, 16); + + if (content.length <= COS_THRESHOLD) { + summary = content; + } else { + // 长内容上传COS,summary取前256字 + summary = content.substring(0, 256); + const cosKey = `brain/${owner}/${node_type}/${id}.md`; + const result = await cos.write('hot', cosKey, content); + content_url = result.url; + } + } + + // 如果没给path但给了parent_id,自动生成路径 + let finalPath = nodePath || null; + if (!finalPath && parent_id) { + const parentResult = await db.query('SELECT path, title FROM brain_nodes WHERE id = $1', [parent_id]); + if (parentResult.rows.length > 0) { + const parentPath = parentResult.rows[0].path || '/' + parentResult.rows[0].title; + finalPath = parentPath + '/' + title; + } + } + if (!finalPath) { + finalPath = '/' + title; + } + + await db.query( + `INSERT INTO brain_nodes (id, title, node_type, parent_id, path, tags, source, source_url, content_url, summary, content_hash, owner, owner_user_id, status, created_by) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'active',$14)`, + [id, title, node_type, parent_id || null, finalPath, + JSON.stringify(tags || []), source, source_url || null, + content_url, summary, content_hash, owner, + input.owner_user_id || null, created_by || owner] + ); + + return { node_id: id, path: finalPath, content_url }; +} + +/** + * updateNode — 更新节点 + */ +async function updateNode(input) { + const { node_id } = input; + const updates = []; + const values = []; + let paramIndex = 1; + + const allowedFields = ['title', 'tags', 'status', 'summary', 'path', 'node_type', 'source_url']; + + for (const field of allowedFields) { + if (input[field] !== undefined) { + const val = field === 'tags' ? JSON.stringify(input[field]) : input[field]; + updates.push(`${field} = $${paramIndex}`); + values.push(val); + paramIndex++; + } + } + + // 如果有content更新 + if (input.content !== undefined) { + const content_hash = crypto.createHash('sha256').update(input.content).digest('hex').substring(0, 16); + updates.push(`content_hash = $${paramIndex}`); + values.push(content_hash); + paramIndex++; + + updates.push(`version = version + 1`); + + if (input.content.length <= COS_THRESHOLD) { + updates.push(`summary = $${paramIndex}`); + values.push(input.content); + paramIndex++; + } else { + updates.push(`summary = $${paramIndex}`); + values.push(input.content.substring(0, 256)); + paramIndex++; + + // 获取当前节点信息用于COS路径 + const nodeResult = await db.query('SELECT owner, node_type FROM brain_nodes WHERE id = $1', [node_id]); + if (nodeResult.rows.length > 0) { + const { owner, node_type } = nodeResult.rows[0]; + const cosKey = `brain/${owner}/${node_type}/${node_id}.md`; + const cosResult = await cos.write('hot', cosKey, input.content); + updates.push(`content_url = $${paramIndex}`); + values.push(cosResult.url); + paramIndex++; + } + } + } + + if (updates.length === 0) { + return { node_id, updated_fields: [] }; + } + + values.push(node_id); + await db.query( + `UPDATE brain_nodes SET ${updates.join(', ')} WHERE id = $${paramIndex}`, + values + ); + + return { + node_id, + updated_fields: Object.keys(input).filter(k => k !== 'node_id') + }; +} + +/** + * deleteNode — 软删除节点 + */ +async function deleteNode(input) { + const { node_id } = input; + const result = await db.query( + "UPDATE brain_nodes SET status = 'deleted' WHERE id = $1 AND status != 'deleted'", + [node_id] + ); + return { success: result.rowCount > 0 }; +} + +/** + * queryNodes — 查询/搜索节点 + */ +async function queryNodes(input) { + const conditions = ["status != 'deleted'"]; + const values = []; + let paramIndex = 1; + + if (input.keyword) { + conditions.push(`(title ILIKE $${paramIndex} OR summary ILIKE $${paramIndex})`); + values.push(`%${input.keyword}%`); + paramIndex++; + } + + if (input.node_type) { + conditions.push(`node_type = $${paramIndex}`); + values.push(input.node_type); + paramIndex++; + } + + if (input.tags && input.tags.length > 0) { + conditions.push(`tags ?| $${paramIndex}`); + values.push(input.tags); + paramIndex++; + } + + if (input.owner) { + conditions.push(`owner = $${paramIndex}`); + values.push(input.owner); + paramIndex++; + } + + if (input.parent_id) { + conditions.push(`parent_id = $${paramIndex}`); + values.push(input.parent_id); + paramIndex++; + } + + if (input.path_prefix) { + conditions.push(`path LIKE $${paramIndex}`); + values.push(input.path_prefix + '%'); + paramIndex++; + } + + if (input.status) { + // 覆盖默认的 != deleted 条件 + conditions[0] = `status = $${paramIndex}`; + values.push(input.status); + paramIndex++; + } + + const limit = Math.min(input.limit || 50, 200); + + const sql = `SELECT id, title, node_type, parent_id, path, tags, source, source_url, content_url, summary, status, owner, created_at, updated_at + FROM brain_nodes + WHERE ${conditions.join(' AND ')} + ORDER BY updated_at DESC + LIMIT ${limit}`; + + const result = await db.query(sql, values); + + // 获取总数 + const countSql = `SELECT COUNT(*) as total FROM brain_nodes WHERE ${conditions.join(' AND ')}`; + const countResult = await db.query(countSql, values); + + return { + nodes: result.rows, + total: parseInt(countResult.rows[0].total, 10) + }; +} + +/** + * getNode — 获取单个节点详情(含COS完整内容) + */ +async function getNode(input) { + const { node_id } = input; + const result = await db.query('SELECT * FROM brain_nodes WHERE id = $1', [node_id]); + + if (result.rows.length === 0) { + return { node: null }; + } + + const node = result.rows[0]; + + // 如果有COS内容,拉取完整内容 + if (node.content_url) { + try { + const cosKey = node.content_url.replace(/^https?:\/\/[^/]+\//, ''); + const cosResult = await cos.read('hot', cosKey); + node.content = cosResult.content; + } catch (err) { + node.content = node.summary; + node.content_error = err.message; + } + } else { + node.content = node.summary; + } + + return { node }; +} + +module.exports = { createNode, updateNode, deleteNode, queryNodes, getNode }; diff --git a/server/age-os/mcp-server/tools/relation-ops.js b/server/age-os/mcp-server/tools/relation-ops.js new file mode 100644 index 00000000..f0ba365f --- /dev/null +++ b/server/age-os/mcp-server/tools/relation-ops.js @@ -0,0 +1,88 @@ +/** + * ═══════════════════════════════════════════════════════════ + * MCP 工具 · 关系操作(linkNodes / unlinkNodes / getRelations) + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + */ + +'use strict'; + +const { v4: uuidv4 } = require('uuid'); +const db = require('../db'); + +/** + * linkNodes — 建立节点关联 + */ +async function linkNodes(input) { + const { from_node_id, to_node_id, relation_type, description, created_by } = input; + const id = uuidv4(); + const weight = input.weight || 50; + + await db.query( + `INSERT INTO brain_relations (id, from_node_id, to_node_id, relation_type, description, weight, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (from_node_id, to_node_id, relation_type) DO UPDATE + SET description = EXCLUDED.description, weight = EXCLUDED.weight`, + [id, from_node_id, to_node_id, relation_type, description || null, weight, created_by || 'system'] + ); + + return { relation_id: id }; +} + +/** + * unlinkNodes — 删除关联 + */ +async function unlinkNodes(input) { + const { relation_id } = input; + const result = await db.query('DELETE FROM brain_relations WHERE id = $1', [relation_id]); + return { success: result.rowCount > 0 }; +} + +/** + * getRelations — 查询某节点的所有关联 + */ +async function getRelations(input) { + const { node_id, relation_type, direction } = input; + const dir = direction || 'both'; + const conditions = []; + const values = []; + let paramIndex = 1; + + if (dir === 'outgoing' || dir === 'both') { + conditions.push(`from_node_id = $${paramIndex}`); + values.push(node_id); + paramIndex++; + } + if (dir === 'incoming' || dir === 'both') { + if (dir === 'both') { + // 用 OR 合并 + conditions[0] = `(from_node_id = $1 OR to_node_id = $${paramIndex})`; + values.push(node_id); + paramIndex++; + } else { + conditions.push(`to_node_id = $${paramIndex}`); + values.push(node_id); + paramIndex++; + } + } + + if (relation_type) { + conditions.push(`relation_type = $${paramIndex}`); + values.push(relation_type); + paramIndex++; + } + + const sql = `SELECT r.*, fn.title as from_title, tn.title as to_title + FROM brain_relations r + LEFT JOIN brain_nodes fn ON r.from_node_id = fn.id + LEFT JOIN brain_nodes tn ON r.to_node_id = tn.id + WHERE ${conditions.join(' AND ')} + ORDER BY r.weight DESC, r.created_at DESC`; + + const result = await db.query(sql, values); + return { relations: result.rows }; +} + +module.exports = { linkNodes, unlinkNodes, getRelations }; diff --git a/server/age-os/mcp-server/tools/structure-ops.js b/server/age-os/mcp-server/tools/structure-ops.js new file mode 100644 index 00000000..f6af0b20 --- /dev/null +++ b/server/age-os/mcp-server/tools/structure-ops.js @@ -0,0 +1,211 @@ +/** + * ═══════════════════════════════════════════════════════════ + * MCP 工具 · 结构操作(buildPath / scanStructure / classify) + * ═══════════════════════════════════════════════════════════ + * + * 签发: 铸渊 · ICE-GL-ZY001 + * 版权: 国作登字-2026-A-00037559 + */ + +'use strict'; + +const { v4: uuidv4 } = require('uuid'); +const db = require('../db'); + +/** + * buildPath — 批量创建路径结构(类似 mkdir -p) + * 路径中已存在的节点跳过,不存在的自动创建为 folder 类型 + */ +async function buildPath(input) { + const { path: targetPath, owner } = input; + const segments = targetPath.split('/').filter(s => s.length > 0); + const createdNodes = []; + let parentId = null; + let currentPath = ''; + + for (const segment of segments) { + currentPath += '/' + segment; + + // 检查此路径是否已存在 + const existing = await db.query( + 'SELECT id FROM brain_nodes WHERE path = $1 AND status != $2', + [currentPath, 'deleted'] + ); + + if (existing.rows.length > 0) { + parentId = existing.rows[0].id; + continue; + } + + // 创建 folder 节点 + const id = uuidv4(); + await db.query( + `INSERT INTO brain_nodes (id, title, node_type, parent_id, path, tags, source, owner, status, created_by) + VALUES ($1, $2, 'folder', $3, $4, '[]'::jsonb, 'agent', $5, 'active', $6)`, + [id, segment, parentId, currentPath, owner, 'buildPath'] + ); + + createdNodes.push({ id, title: segment, path: currentPath }); + parentId = id; + } + + return { + created_nodes: createdNodes, + leaf_node_id: parentId + }; +} + +/** + * scanStructure — 扫描大脑结构健康度 + */ +async function scanStructure(input) { + const rootPath = input.root_path || null; + const checks = input.checks || ['orphans', 'broken_links', 'duplicates', 'empty_folders']; + const result = { + total_nodes: 0, + orphan_nodes: [], + broken_links: [], + duplicate_titles: [], + empty_folders: [], + health_score: 100 + }; + + // 总节点数 + const countResult = await db.query( + "SELECT COUNT(*) as total FROM brain_nodes WHERE status = 'active'" + ); + result.total_nodes = parseInt(countResult.rows[0].total, 10); + + if (result.total_nodes === 0) { + return result; + } + + // 孤岛节点:有parent_id但parent不存在 + if (checks.includes('orphans')) { + const orphans = await db.query( + `SELECT n.id, n.title, n.path, n.parent_id + FROM brain_nodes n + LEFT JOIN brain_nodes p ON n.parent_id = p.id + WHERE n.parent_id IS NOT NULL + AND n.status = 'active' + AND (p.id IS NULL OR p.status = 'deleted')` + ); + result.orphan_nodes = orphans.rows; + } + + // 断链:关系指向不存在的节点 + if (checks.includes('broken_links')) { + const broken = await db.query( + `SELECT r.* + FROM brain_relations r + LEFT JOIN brain_nodes fn ON r.from_node_id = fn.id AND fn.status = 'active' + LEFT JOIN brain_nodes tn ON r.to_node_id = tn.id AND tn.status = 'active' + WHERE fn.id IS NULL OR tn.id IS NULL` + ); + result.broken_links = broken.rows; + } + + // 重复标题 + if (checks.includes('duplicates')) { + const dupes = await db.query( + `SELECT title, array_agg(id) as node_ids, COUNT(*) as cnt + FROM brain_nodes + WHERE status = 'active' + GROUP BY title + HAVING COUNT(*) > 1 + ORDER BY cnt DESC + LIMIT 20` + ); + result.duplicate_titles = dupes.rows; + } + + // 空目录 + if (checks.includes('empty_folders')) { + const empty = await db.query( + `SELECT f.id, f.title, f.path + FROM brain_nodes f + LEFT JOIN brain_nodes c ON c.parent_id = f.id AND c.status = 'active' + WHERE f.node_type = 'folder' + AND f.status = 'active' + AND c.id IS NULL` + ); + result.empty_folders = empty.rows; + } + + // 计算健康分 + const issues = result.orphan_nodes.length + result.broken_links.length + + result.duplicate_titles.length + result.empty_folders.length; + result.health_score = Math.max(0, Math.round(100 - (issues / result.total_nodes) * 100)); + + return result; +} + +/** + * classify — 自动分类/打标签 + * strategy=rule: 纯关键词匹配,零成本 + * strategy=model: 调用DeepSeek(极少使用) + */ +async function classify(input) { + const { node_ids, strategy, rules, dry_run } = input; + const classified = []; + const unclassified = []; + + for (const nodeId of node_ids) { + const nodeResult = await db.query( + 'SELECT id, title, summary, tags, path FROM brain_nodes WHERE id = $1', + [nodeId] + ); + if (nodeResult.rows.length === 0) continue; + + const node = nodeResult.rows[0]; + + if (strategy === 'rule' && rules) { + // 规则匹配 + const matchResult = applyRules(node, rules); + if (matchResult) { + classified.push({ + node_id: nodeId, + assigned_tags: matchResult.tags, + assigned_path: matchResult.path + }); + if (!dry_run) { + // 实际更新 + const newTags = [...new Set([...(node.tags || []), ...matchResult.tags])]; + await db.query( + 'UPDATE brain_nodes SET tags = $1, path = COALESCE($2, path) WHERE id = $3', + [JSON.stringify(newTags), matchResult.path || null, nodeId] + ); + } + } else { + unclassified.push(nodeId); + } + } else if (strategy === 'model') { + // 模型分类 — 占位,S5阶段实现 + unclassified.push(nodeId); + } else { + unclassified.push(nodeId); + } + } + + return { classified, unclassified }; +} + +/** + * 规则引擎:关键词→标签映射 + */ +function applyRules(node, rules) { + const text = ((node.title || '') + ' ' + (node.summary || '')).toLowerCase(); + + for (const [pattern, result] of Object.entries(rules)) { + const keywords = pattern.toLowerCase().split('|'); + if (keywords.some(kw => text.includes(kw))) { + return { + tags: Array.isArray(result.tags) ? result.tags : [result.tags], + path: result.path || null + }; + } + } + return null; +} + +module.exports = { buildPath, scanStructure, classify }; diff --git a/server/age-os/package.json b/server/age-os/package.json new file mode 100644 index 00000000..5d6a70b6 --- /dev/null +++ b/server/age-os/package.json @@ -0,0 +1,23 @@ +{ + "name": "age-os-mcp", + "version": "1.0.0", + "description": "AGE OS MCP Server · 大脑工具链服务 · 铸渊主控", + "private": true, + "main": "mcp-server/server.js", + "scripts": { + "start": "node mcp-server/server.js", + "start:agents": "node agents/scheduler.js", + "test:db": "node mcp-server/test-db.js" + }, + "dependencies": { + "express": "^4.21.0", + "pg": "^8.13.0", + "node-cron": "^3.0.3", + "uuid": "^11.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "author": "铸渊 · ICE-GL-ZY001", + "license": "SEE COPYRIGHT" +} diff --git a/server/age-os/schema/001-brain-tables.sql b/server/age-os/schema/001-brain-tables.sql new file mode 100644 index 00000000..9f19fed8 --- /dev/null +++ b/server/age-os/schema/001-brain-tables.sql @@ -0,0 +1,215 @@ +-- ============================================================ +-- AGE OS · 核心大脑数据库 Schema +-- PostgreSQL 初始化脚本 +-- ============================================================ +-- 版本: v1.0.0 +-- 签发: 铸渊(ICE-GL-ZY001) · 需求来源: 霜砚(AG-SY-01) +-- 授权: 冰朔(TCS-0002∞) +-- 版权: 国作登字-2026-A-00037559 +-- ============================================================ +-- +-- 设计哲学: +-- brain_nodes = Notion里的页面 → 每个认知节点 +-- brain_relations = Notion里的Relation → 节点之间的关联 +-- agent_configs = Agent注册表 → 谁在跑、跑什么 +-- agent_logs = 运行日志 → 追踪每次执行 +-- user_credits = 用户额度 → 商业化基础 +-- +-- 与 website-brain/schema/001-init.sql 并存: +-- website-brain 的 pages/databases/modules/persona_state 管网站展示层 +-- 本文件的 brain_nodes/brain_relations 管认知数据层 +-- ============================================================ + +-- 启用 UUID 扩展 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================================ +-- 1. 节点表 · brain_nodes +-- 核心表:每条记录 = 一个认知节点 +-- 类比 Notion 里的一个页面 +-- ============================================================ +CREATE TABLE IF NOT EXISTS brain_nodes ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4()::text, + title VARCHAR(500) NOT NULL, + node_type VARCHAR(50) NOT NULL + CHECK (node_type IN ('page', 'folder', 'index', 'route', 'config', 'memory', 'log', 'instruction')), + parent_id VARCHAR(64) REFERENCES brain_nodes(id) ON DELETE SET NULL, + path VARCHAR(1000), + tags JSONB DEFAULT '[]'::jsonb, + source VARCHAR(50) NOT NULL + CHECK (source IN ('notion', 'github', 'gpt', 'manual', 'agent')), + source_url VARCHAR(500), + content_url VARCHAR(500), + summary TEXT, + content_hash VARCHAR(64), + version INT DEFAULT 1, + owner VARCHAR(50) NOT NULL + CHECK (owner IN ('shuangyan', 'zhuyuan', 'qiuqiu', 'yaoming', 'system', 'user')), + owner_user_id VARCHAR(64), + status VARCHAR(20) NOT NULL DEFAULT 'active' + CHECK (status IN ('active', 'archived', 'draft', 'deleted')), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by VARCHAR(50) NOT NULL +); + +-- 索引:按常见查询模式优化 +CREATE INDEX idx_bn_parent ON brain_nodes(parent_id); +CREATE INDEX idx_bn_owner ON brain_nodes(owner); +CREATE INDEX idx_bn_type ON brain_nodes(node_type); +CREATE INDEX idx_bn_status ON brain_nodes(status); +CREATE INDEX idx_bn_path ON brain_nodes(path); +CREATE INDEX idx_bn_tags ON brain_nodes USING GIN(tags); + +-- ============================================================ +-- 2. 关系表 · brain_relations +-- 节点之间的关联关系 +-- 类比 Notion 里的 Relation 属性 +-- ============================================================ +CREATE TABLE IF NOT EXISTS brain_relations ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4()::text, + from_node_id VARCHAR(64) NOT NULL REFERENCES brain_nodes(id) ON DELETE CASCADE, + to_node_id VARCHAR(64) NOT NULL REFERENCES brain_nodes(id) ON DELETE CASCADE, + relation_type VARCHAR(50) NOT NULL + CHECK (relation_type IN ('parent_child', 'reference', 'dependency', 'version', 'alias')), + description VARCHAR(500), + weight SMALLINT DEFAULT 50 CHECK (weight >= 0 AND weight <= 100), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by VARCHAR(50) NOT NULL +); + +-- 防止重复关系 +CREATE UNIQUE INDEX idx_br_unique ON brain_relations(from_node_id, to_node_id, relation_type); +CREATE INDEX idx_br_from ON brain_relations(from_node_id); +CREATE INDEX idx_br_to ON brain_relations(to_node_id); + +-- ============================================================ +-- 3. Agent配置表 · agent_configs +-- 每个Agent的运行配置 +-- ============================================================ +CREATE TABLE IF NOT EXISTS agent_configs ( + agent_id VARCHAR(50) PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + owner VARCHAR(50) NOT NULL DEFAULT 'system' + CHECK (owner IN ('notion', 'github', 'system')), + script_path VARCHAR(500), + cron_schedule VARCHAR(100), + enabled BOOLEAN NOT NULL DEFAULT true, + allowed_tools JSONB DEFAULT '[]'::jsonb, + model_config JSONB DEFAULT '{}'::jsonb, + last_run_at TIMESTAMPTZ, + last_run_status VARCHAR(20) + CHECK (last_run_status IN ('success', 'error', 'skipped', 'running') OR last_run_status IS NULL), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================ +-- 4. 运行日志表 · agent_logs +-- Agent每次运行的记录 +-- ============================================================ +CREATE TABLE IF NOT EXISTS agent_logs ( + id BIGSERIAL PRIMARY KEY, + agent_id VARCHAR(50) NOT NULL REFERENCES agent_configs(agent_id) ON DELETE CASCADE, + run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + status VARCHAR(20) NOT NULL + CHECK (status IN ('success', 'error', 'warning', 'skipped')), + message TEXT, + details JSONB DEFAULT '{}'::jsonb, + duration_ms INT +); + +CREATE INDEX idx_al_agent ON agent_logs(agent_id); +CREATE INDEX idx_al_run_at ON agent_logs(run_at); +CREATE INDEX idx_al_status ON agent_logs(status); + +-- ============================================================ +-- 5. 用户额度表 · user_credits +-- 用户大模型调用额度 +-- ============================================================ +CREATE TABLE IF NOT EXISTS user_credits ( + user_id VARCHAR(64) PRIMARY KEY, + total_charged DECIMAL(10,2) NOT NULL DEFAULT 0, + actual_credit DECIMAL(10,2) NOT NULL DEFAULT 0, + consumed DECIMAL(10,2) NOT NULL DEFAULT 0, + remaining DECIMAL(10,2) NOT NULL DEFAULT 0, + preferred_model VARCHAR(50), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================ +-- 自动更新 updated_at 触发器 +-- ============================================================ +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_bn_updated + BEFORE UPDATE ON brain_nodes + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER trg_ac_updated + BEFORE UPDATE ON agent_configs + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +CREATE TRIGGER trg_uc_updated + BEFORE UPDATE ON user_credits + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- ============================================================ +-- 初始Agent数据 · 霜砚需求的9个Agent +-- ============================================================ +INSERT INTO agent_configs (agent_id, name, description, owner, script_path, cron_schedule, enabled, allowed_tools, model_config) +VALUES + -- 系统级Agent(7×24自动跑) + ('SY-SCAN', '大脑结构巡检', '扫描孤岛节点、断链、空目录,生成健康报告', 'notion', + 'agents/sy-scan.js', '0 */6 * * *', true, + '["queryNodes", "scanStructure", "getRelations"]'::jsonb, + '{"model": "none", "note": "纯逻辑,不调模型"}'::jsonb), + + ('SY-CLASSIFY', '自动分类引擎', '扫描未分类节点,按规则分类,规则搞不定调DeepSeek', 'notion', + 'agents/sy-classify.js', '0 */2 * * *', true, + '["queryNodes", "classify", "updateNode", "linkNodes"]'::jsonb, + '{"primary": "rule", "fallback": "deepseek", "temperature": 0.3}'::jsonb), + + ('SY-SYNC-N2B', 'Notion→大脑同步', '检测Notion端有无新增/修改的页面,同步到自研数据库', 'notion', + 'agents/sy-sync-n2b.js', '0 */4 * * *', true, + '["createNode", "updateNode", "cosWrite", "linkNodes", "buildPath"]'::jsonb, + '{"model": "none", "note": "纯逻辑同步"}'::jsonb), + + ('SY-SYNC-B2N', '大脑→Notion反向同步', '将自研数据库中的变更同步回Notion', 'notion', + 'agents/sy-sync-b2n.js', '0 3 * * *', false, + '["queryNodes", "cosRead"]'::jsonb, + '{"model": "none", "note": "可选,按需启用"}'::jsonb), + + ('SY-ARCHIVE', '内容归档引擎', '将超过30天未修改的内容从主桶移到归档桶', 'notion', + 'agents/sy-archive.js', '0 4 * * 0', true, + '["queryNodes", "cosRead", "cosArchive", "updateNode"]'::jsonb, + '{"model": "none", "note": "纯逻辑归档"}'::jsonb), + + ('SY-TEST', '系统自检', '检测数据库连接、COS连通性、工具链可用性', 'system', + 'agents/sy-test.js', '*/30 * * * *', true, + '["queryNodes", "cosRead", "cosWrite"]'::jsonb, + '{"model": "none", "note": "纯连通性检查"}'::jsonb), + + -- 交互级Agent(用户对话时按需调用) + ('SY-11', '元路由调度引擎', '判断请求该分配给哪个人格体、走哪个模型', 'system', + 'agents/sy-11-router.js', NULL, true, + '["queryNodes", "getNode"]'::jsonb, + '{"primary": "rule", "fallback": "deepseek", "temperature": 0.1}'::jsonb), + + ('SY-12', '上下文记忆管理', '扫描对话、压缩摘要、存入大脑数据库', 'system', + 'agents/sy-12-memory.js', NULL, true, + '["createNode", "updateNode", "queryNodes", "cosWrite"]'::jsonb, + '{"model": "none", "note": "纯JS逻辑"}'::jsonb), + + ('SY-BRAIN-RW', '大脑读写服务', '人格体在对话中需要查大脑/写大脑时调用', 'system', + 'agents/sy-brain-rw.js', NULL, true, + '["createNode", "updateNode", "deleteNode", "queryNodes", "getNode", "linkNodes", "unlinkNodes", "getRelations", "buildPath", "scanStructure", "classify", "cosWrite", "cosRead", "cosDelete", "cosList", "cosArchive"]'::jsonb, + '{"model": "dynamic", "note": "按需调用模型"}'::jsonb) +ON CONFLICT (agent_id) DO NOTHING;