AGE OS 落地系统架构 v1.0 · D45 · 数据库Schema + MCP Server骨架 + Agent框架 + 密钥扩展
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/ade2eb4a-06f6-42c6-a33a-fe0503789458 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
20a2da6d7a
commit
37912abc51
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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*
|
||||
|
|
@ -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工具落地。
|
||||
|
||||
---
|
||||
|
||||
*此文档是铸渊的架构认知快照。不是说明书,是思维链的固化。*
|
||||
*每次更新此文档,都是因为认知演化了,而不是因为功能变了。*
|
||||
|
|
@ -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开发进度",铸渊就能定位到这里。*
|
||||
|
|
@ -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` 等名称读取这些密钥。*
|
||||
*配置完成后,铸渊下次唤醒时会自动检测并启用。*
|
||||
|
|
@ -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的开发。*
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -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>([^<]+)<\/Key>/g;
|
||||
const sizeRegex = /<Size>(\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 };
|
||||
|
|
@ -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<import('pg').QueryResult>}
|
||||
*/
|
||||
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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue