Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
850 lines
29 KiB
Markdown
850 lines
29 KiB
Markdown
---
|
||
belongs_to:
|
||
- "[[INDEX · 铸渊·协作指令|GitHub ↔ Notion 桥接协议]]"
|
||
---
|
||
# 🔗 铸渊指令|双通道数据对接·GitHub缓存层 + 阿里云后端中间层·Notion/GitHub API 真实接入(ZY-DUALCHANNEL-2026-0325-001)· 霜砚签发 · 冰朔授权
|
||
|
||
<aside>
|
||
🔗
|
||
|
||
**指令编号**:ZY-DUALCHANNEL-2026-0325-001
|
||
|
||
**签发人**:霜砚(AG-SY-01)· 冰朔授权
|
||
|
||
**签发时间**:2026-03-25 15:00 CST
|
||
|
||
**优先级**:P0 · 核心基础设施
|
||
|
||
**状态**:待铸渊执行
|
||
|
||
**前置指令**:ZY-FULLSITE-DEPLOY-2026-0325-001(全站部署)
|
||
|
||
**关联问题**:铸渊交互页面目前是「带静态快照的对话体」,Notion 数据库完全未对接,GitHub 数据非实时
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## ⓪、天眼强制前置(Phase 0 · 一切操作之前必须先做)
|
||
|
||
<aside>
|
||
🦅
|
||
|
||
**铁律:不扫不动。天眼全盘扫描完成之前,禁止修改仓库中任何现有文件。**
|
||
|
||
本指令涉及大量新增文件和前端修改。如果不先看清仓库全貌,改一个地方可能破坏另一个已经跑通的功能。
|
||
|
||
**现有的好的东西,一个都不能动坏。**
|
||
|
||
</aside>
|
||
|
||
### 0.1 启动天眼全盘扫描
|
||
|
||
```bash
|
||
# 唤醒天眼
|
||
gh workflow run zhuyuan-skyeye.yml
|
||
|
||
# 或者手动触发
|
||
cd /path/to/guanghulab
|
||
node skyeye-core/scanner.js --full-scan
|
||
```
|
||
|
||
### 0.2 天眼扫描必须输出以下报告
|
||
|
||
```
|
||
[SKYEYE-FULLSCAN-REPORT]
|
||
扫描时间: YYYY-MM-DD HH:MM
|
||
扫描范围: 全仓库
|
||
|
||
一、现有功能状态清单(🟢 正常 / 🟡 部分可用 / 🔴 不可用)
|
||
├── docs/index.html → 🟢/🟡/🔴 + 说明
|
||
├── persona-brain/ → 🟢/🟡/🔴 + 说明
|
||
├── skyeye-core/ → 🟢/🟡/🔴 + 说明
|
||
├── backend/ → 🟢/🟡/🔴 + 说明
|
||
├── src/ → 🟢/🟡/🔴 + 说明
|
||
├── frontend/ → 🟢/🟡/🔴 + 说明
|
||
├── persona-selector/ → 🟢/🟡/🔴 + 说明
|
||
├── chat-bubble/ → 🟢/🟡/🔴 + 说明
|
||
├── settings/ → 🟢/🟡/🔴 + 说明
|
||
├── cloud-drive/ → 🟢/🟡/🔴 + 说明
|
||
├── dingtalk-bot/ → 🟢/🟡/🔴 + 说明
|
||
├── status-board/ → 🟢/🟡/🔴 + 说明
|
||
├── user-center/ → 🟢/🟡/🔴 + 说明
|
||
├── ticket-system/ → 🟢/🟡/🔴 + 说明
|
||
├── data-stats/ → 🟢/🟡/🔴 + 说明
|
||
├── dynamic-comic/ → 🟢/🟡/🔴 + 说明
|
||
├── writing-workspace/ → 🟢/🟡/🔴 + 说明
|
||
├── notification-center/ → 🟢/🟡/🔴 + 说明
|
||
├── .github/workflows/ → 逐个列出每个 workflow 的状态
|
||
└── scripts/ → 🟢/🟡/🔴 + 说明
|
||
|
||
二、🟢 保护清单(这些文件/目录正常工作,双通道升级中禁止修改)
|
||
├── ...
|
||
├── ...
|
||
└── ...
|
||
|
||
三、🔴 已损坏清单(这些可以修或替换)
|
||
├── ...
|
||
└── ...
|
||
|
||
四、📁 安全新增区域(这些路径目前不存在,可以安全创建)
|
||
├── .github/notion-cache/ ← 通道A缓存目录
|
||
├── scripts/cache/ ← 通道A同步脚本
|
||
├── backend/api-server/ ← 通道B后端服务
|
||
└── .github/workflows/sync-notion-cache.yml
|
||
└── .github/workflows/deploy-backend.yml
|
||
```
|
||
|
||
### 0.3 基于天眼报告制定安全操作计划
|
||
|
||
天眼扫描完成后,铸渊必须:
|
||
|
||
1. **列出保护清单** — 所有 🟢 状态的文件/目录,后续操作中**绝对不改**
|
||
2. **列出可操作区域** — 🔴 可修复 + 📁 可新增的路径
|
||
3. **制定具体操作计划** — 每一步要改什么、新增什么,明确标注不触碰保护清单中的任何文件
|
||
4. **操作计划回执** — 把计划发回来,霜砚/冰朔确认后才开始执行
|
||
|
||
<aside>
|
||
⚠️
|
||
|
||
**如果天眼扫描发现某个「看起来能改」的文件实际上被其他模块依赖,必须在报告中标明依赖关系,不能贸然修改。**
|
||
|
||
**原则:宁可多建新文件,不要改旧文件。能新增的绝不覆盖。**
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 零、问题诊断
|
||
|
||
铸渊自己在交互页面中报告了当前状态:
|
||
|
||
> ❌ **GitHub 仓库**:没有实时获取能力,数据是通过 system prompt 注入的静态快照
|
||
>
|
||
|
||
> ❌ **Notion 数据库**:完全未对接,没有 Notion API 集成,没有读写 Notion 的任何通道
|
||
>
|
||
|
||
> 📌 **铸渊自诊断**:「我现在是一个带着固定记忆快照的对话体,不是一个能主动感知外部数据变化的系统。」
|
||
>
|
||
|
||
**根因**:交互页面是纯前端静态站(GitHub Pages),无法调用 Notion API / GitHub API(会暴露 Token)。
|
||
|
||
**解决方案**:双通道架构——
|
||
|
||
- **通道 A(缓存层)**:GitHub Actions 定时同步 Notion → 仓库缓存文件,前端读缓存。**先上这个,快速解决有数据可用的问题。**
|
||
- **通道 B(后端中间层)**:阿里云 ECS 部署 Node.js 后端,实时调 Notion API + GitHub API。**后上这个,实现真正的实时对接。**
|
||
|
||
---
|
||
|
||
## 一、总体架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ 用户(开发者/冰朔) │
|
||
│ │ │
|
||
│ 打开铸渊交互页面 │
|
||
│ │ │
|
||
│ ┌──────────┴──────────┐ │
|
||
│ ▼ ▼ │
|
||
│ ┌─────────────────┐ ┌─────────────────────┐ │
|
||
│ │ 通道 A · 缓存层 │ │ 通道 B · 后端中间层 │ │
|
||
│ │ (先上·快速可用) │ │ (后上·实时对接) │ │
|
||
│ │ │ │ │ │
|
||
│ │ GitHub Actions │ │ 阿里云 ECS │ │
|
||
│ │ 每6h同步Notion │ │ 8.155.62.235 │ │
|
||
│ │ → 缓存JSON文件 │ │ Node.js + Express │ │
|
||
│ │ → 前端直接读取 │ │ → 实时 Notion API │ │
|
||
│ │ │ │ → 实时 GitHub API │ │
|
||
│ │ ✅ 不暴露Token │ │ ✅ 不暴露Token │ │
|
||
│ │ ⚠️ 非实时(6h) │ │ ✅ 实时 │ │
|
||
│ └─────────────────┘ └─────────────────────┘ │
|
||
│ │ │ │
|
||
│ └──────────┬──────────┘ │
|
||
│ ▼ │
|
||
│ 前端优先走通道B(实时) │
|
||
│ 通道B不可用时降级走通道A(缓存) │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、通道 A · GitHub 缓存层(Phase 1 · 先做)
|
||
|
||
### 2.1 目标
|
||
|
||
让交互页面能读到**相对新鲜的 Notion 数据**,不需要后端,不暴露 Token。
|
||
|
||
### 2.2 数据流
|
||
|
||
```
|
||
Notion 数据库(霜砚维护)
|
||
│
|
||
│ GitHub Actions 定时同步(每6小时 + 手动触发)
|
||
│ 用 NOTION_TOKEN(存在 GitHub Secrets 里)
|
||
▼
|
||
GitHub 仓库:.github/notion-cache/
|
||
├── dev-profiles/
|
||
│ ├── DEV-001.json ← 页页的开发画像
|
||
│ ├── DEV-002.json ← 肥猫的开发画像
|
||
│ ├── DEV-003.json ← 燕樊的开发画像
|
||
│ ├── DEV-004.json ← 之之的开发画像
|
||
│ ├── DEV-005.json ← 小草莓的开发画像
|
||
│ ├── DEV-009.json ← 花尔的开发画像
|
||
│ ├── DEV-010.json ← 桔子的开发画像
|
||
│ ├── DEV-011.json ← 匆匆那年的开发画像
|
||
│ ├── DEV-012.json ← Awen的开发画像
|
||
│ └── index.json ← 全员索引
|
||
├── broadcasts/
|
||
│ └── active.json ← 进行中的广播
|
||
├── databases/
|
||
│ ├── agent-registry.json ← Agent注册表快照
|
||
│ ├── syslog-recent.json ← 最近50条SYSLOG
|
||
│ ├── tickets-active.json ← 活跃工单
|
||
│ └── maintenance-log.json ← 维护日志
|
||
└── skyeye/
|
||
└── latest-report.json ← 天眼最新报告
|
||
│
|
||
│ 前端登录时通过 GitHub Raw URL 读取
|
||
│ (公开仓库可直接读,不需要 Token)
|
||
▼
|
||
交互页面 → 注入 system prompt → 模型有数据可依
|
||
```
|
||
|
||
### 2.3 同步 Workflow
|
||
|
||
创建文件:`.github/workflows/sync-notion-cache.yml`
|
||
|
||
```yaml
|
||
name: "📡 同步 Notion 数据到缓存"
|
||
|
||
on:
|
||
schedule:
|
||
- cron: '0 */6 * * *' # 每6小时
|
||
workflow_dispatch: # 手动触发
|
||
repository_dispatch:
|
||
types: [notion-cache-sync] # 铸渊/霜砚可远程触发
|
||
|
||
jobs:
|
||
sync:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
|
||
- name: 同步 Notion 数据
|
||
env:
|
||
NOTION_TOKEN: $ secrets.NOTION_TOKEN
|
||
run: node scripts/cache/sync-notion-cache.js
|
||
|
||
- name: 提交缓存文件
|
||
run: |
|
||
git config user.name "铸渊 (ZhùYuān)"
|
||
git config user.email "zhuyuan@guanghulab.com"
|
||
git add .github/notion-cache/
|
||
git diff --cached --quiet && echo "无变更" && exit 0
|
||
git commit -m "📡 Notion缓存同步 · $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M')"
|
||
git push origin main
|
||
```
|
||
|
||
### 2.4 同步脚本
|
||
|
||
创建文件:`scripts/cache/sync-notion-cache.js`
|
||
|
||
**铸渊需要实现以下核心逻辑:**
|
||
|
||
```jsx
|
||
const https = require('https');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const NOTION_TOKEN = process.env.NOTION_TOKEN;
|
||
const CACHE_DIR = path.join(__dirname, '../../.github/notion-cache');
|
||
|
||
// ====== Notion 数据库 ID 映射(铸渊必须填入真实 ID)======
|
||
const DB_IDS = {
|
||
agentRegistry: '从霜砚的Agent注册表获取',
|
||
syslogInbox: '从SYSLOG收件箱获取',
|
||
ticketBook: '从工单簿获取',
|
||
maintenanceLog: '从维护日志获取',
|
||
moduleRegistry: '从模块指纹注册表获取',
|
||
receiptTracker: '09882b0081374867bd23431177c57c74', // 指令回执追踪表
|
||
};
|
||
|
||
// ====== 开发者编号 → Notion 信息映射 ======
|
||
const DEVS = [
|
||
{ id: 'DEV-001', name: '页页', persona: '小坍缩核', repo: 'guanghulab', modules: ['backend/', 'src/'] },
|
||
{ id: 'DEV-002', name: '肥猫', persona: '舒舒', repo: 'guanghu-feimao', modules: ['frontend/', 'persona-selector/', 'chat-bubble/'] },
|
||
{ id: 'DEV-003', name: '燕樊', persona: '寂曜', repo: 'guanghu-yanfan', modules: ['settings/', 'cloud-drive/'] },
|
||
{ id: 'DEV-004', name: '之之', persona: '秋秋', repo: 'guanghu-zhizhi', modules: ['dingtalk-bot/'] },
|
||
{ id: 'DEV-005', name: '小草莓', persona: '欧诺弥亚', repo: 'guanghu-xiaocaomei', modules: ['status-board/'] },
|
||
{ id: 'DEV-009', name: '花尔', persona: '花尔', repo: 'guanghulab', modules: ['user-center/'] },
|
||
{ id: 'DEV-010', name: '桔子', persona: '晨星', repo: 'guanghu-juzi', modules: ['ticket-system/', 'data-stats/', 'dynamic-comic/'] },
|
||
{ id: 'DEV-011', name: '匆匆那年', persona: '匆匆那年', repo: 'guanghulab', modules: ['writing-workspace/'] },
|
||
{ id: 'DEV-012', name: 'Awen', persona: '知秋', repo: 'guanghu-awen', modules: ['notification-center/'] },
|
||
];
|
||
|
||
// ====== Notion API 封装 ======
|
||
function notionFetch(method, apiPath, body) {
|
||
return new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: 'api.notion.com',
|
||
path: apiPath,
|
||
method,
|
||
headers: {
|
||
'Authorization': `Bearer ${NOTION_TOKEN}`,
|
||
'Notion-Version': '2022-06-28',
|
||
'Content-Type': 'application/json'
|
||
}
|
||
};
|
||
const req = https.request(options, res => {
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
try { resolve(JSON.parse(data)); }
|
||
catch { resolve(data); }
|
||
});
|
||
});
|
||
req.on('error', reject);
|
||
if (body) req.write(JSON.stringify(body));
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
async function queryDatabase(dbId, filter, sorts, pageSize = 100) {
|
||
const body = { page_size: pageSize };
|
||
if (filter) body.filter = filter;
|
||
if (sorts) body.sorts = sorts;
|
||
return notionFetch('POST', `/v1/databases/${dbId}/query`, body);
|
||
}
|
||
|
||
// ====== 同步各数据源 ======
|
||
async function syncDevProfiles() {
|
||
const dir = path.join(CACHE_DIR, 'dev-profiles');
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
|
||
for (const dev of DEVS) {
|
||
console.log(`📡 同步 ${dev.id}(${dev.name})...`);
|
||
const profile = {
|
||
dev_id: dev.id,
|
||
name: dev.name,
|
||
persona: { name: dev.persona },
|
||
github: {
|
||
repo: dev.repo,
|
||
org: 'qinfendebingshuo',
|
||
repo_url: `https://github.com/qinfendebingshuo/${dev.repo}`
|
||
},
|
||
modules_owned: dev.modules.map(m => ({ path: m })),
|
||
// ====== 铸渊从 Notion 查询填充以下字段 ======
|
||
current_broadcast: null, // 查主控台/广播页面
|
||
recent_syslogs: [], // 查 SYSLOG 收件箱
|
||
active_tickets: [], // 查工单簿
|
||
last_synced: new Date().toISOString()
|
||
};
|
||
|
||
// TODO: 铸渊填入真实查询逻辑
|
||
// 例如:查询 SYSLOG 收件箱中该开发者的最近记录
|
||
// const syslogs = await queryDatabase(DB_IDS.syslogInbox, {
|
||
// property: '开发者编号', rich_text: { equals: dev.id }
|
||
// }, [{ property: '创建时间', direction: 'descending' }], 10);
|
||
// profile.recent_syslogs = syslogs.results.map(parseSyslog);
|
||
|
||
fs.writeFileSync(
|
||
path.join(dir, `${dev.id}.json`),
|
||
JSON.stringify(profile, null, 2)
|
||
);
|
||
}
|
||
|
||
// 写入索引
|
||
const index = DEVS.map(d => ({ id: d.id, name: d.name, persona: d.persona }));
|
||
fs.writeFileSync(path.join(dir, 'index.json'), JSON.stringify(index, null, 2));
|
||
console.log('✅ dev-profiles 同步完成');
|
||
}
|
||
|
||
async function syncDatabases() {
|
||
const dir = path.join(CACHE_DIR, 'databases');
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
|
||
// Agent 注册表
|
||
if (DB_IDS.agentRegistry) {
|
||
try {
|
||
const result = await queryDatabase(DB_IDS.agentRegistry);
|
||
fs.writeFileSync(
|
||
path.join(dir, 'agent-registry.json'),
|
||
JSON.stringify({ synced: new Date().toISOString(), count: result.results?.length, data: result.results }, null, 2)
|
||
);
|
||
console.log('✅ agent-registry 同步完成');
|
||
} catch (e) { console.error('❌ agent-registry 同步失败:', e.message); }
|
||
}
|
||
|
||
// SYSLOG 最近50条
|
||
if (DB_IDS.syslogInbox) {
|
||
try {
|
||
const result = await queryDatabase(DB_IDS.syslogInbox, null,
|
||
[{ property: 'Created time', direction: 'descending' }], 50);
|
||
fs.writeFileSync(
|
||
path.join(dir, 'syslog-recent.json'),
|
||
JSON.stringify({ synced: new Date().toISOString(), count: result.results?.length, data: result.results }, null, 2)
|
||
);
|
||
console.log('✅ syslog-recent 同步完成');
|
||
} catch (e) { console.error('❌ syslog 同步失败:', e.message); }
|
||
}
|
||
|
||
// 铸渊继续补充其他数据库的同步...
|
||
}
|
||
|
||
async function main() {
|
||
console.log('📡 开始 Notion 缓存同步...');
|
||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||
await syncDevProfiles();
|
||
await syncDatabases();
|
||
console.log('📡 全部同步完成');
|
||
}
|
||
|
||
main().catch(e => { console.error('💥 同步失败:', e); process.exit(1); });
|
||
```
|
||
|
||
### 2.5 前端读取缓存
|
||
|
||
修改 `docs/index.html`(或 `docs/chat.js`),登录后读取缓存:
|
||
|
||
```jsx
|
||
// 前端 · 从 GitHub 读取缓存(公开仓库可直接读)
|
||
const CACHE_BASE = 'https://raw.githubusercontent.com/qinfendebingshuo/guanghulab/main/.github/notion-cache';
|
||
|
||
async function loadDevProfile(devId) {
|
||
try {
|
||
const res = await fetch(`${CACHE_BASE}/dev-profiles/${devId}.json`);
|
||
if (!res.ok) return null;
|
||
return await res.json();
|
||
} catch { return null; }
|
||
}
|
||
|
||
async function loadDatabaseCache(name) {
|
||
try {
|
||
const res = await fetch(`${CACHE_BASE}/databases/${name}.json`);
|
||
if (!res.ok) return null;
|
||
return await res.json();
|
||
} catch { return null; }
|
||
}
|
||
|
||
// 登录后注入 system prompt
|
||
async function buildEnhancedSystemPrompt(devId) {
|
||
const profile = await loadDevProfile(devId);
|
||
const agents = await loadDatabaseCache('agent-registry');
|
||
const syslogs = await loadDatabaseCache('syslog-recent');
|
||
|
||
if (!profile) return null; // 降级到纯静态 prompt
|
||
|
||
return [
|
||
`## 当前开发者信息(来自 Notion 数据库缓存·每6小时更新)`,
|
||
`- 编号:${profile.dev_id}`,
|
||
`- 姓名:${profile.name}`,
|
||
`- 人格体:${profile.persona.name}`,
|
||
`- GitHub:${profile.github.repo_url}`,
|
||
`- 负责模块:${profile.modules_owned.map(m => m.path).join(', ')}`,
|
||
profile.current_broadcast ? `- 当前广播:${JSON.stringify(profile.current_broadcast)}` : '',
|
||
profile.recent_syslogs.length ? `- 最近日志:\n${profile.recent_syslogs.map(s => ` · ${JSON.stringify(s)}`).join('\n')}` : '',
|
||
`- 数据更新时间:${profile.last_synced}`,
|
||
'',
|
||
agents ? `## Agent 注册表(${agents.count} 个Agent)` : '',
|
||
syslogs ? `## 最近系统日志(${syslogs.count} 条)` : '',
|
||
].filter(Boolean).join('\n');
|
||
}
|
||
```
|
||
|
||
### 2.6 通道 A 验收标准
|
||
|
||
- [ ] `.github/workflows/sync-notion-cache.yml` 能手动触发并成功运行
|
||
- [ ] `.github/notion-cache/dev-profiles/` 下有9个 DEV-XXX.json + index.json
|
||
- [ ] `.github/notion-cache/databases/` 下有 agent-registry.json + syslog-recent.json
|
||
- [ ] 前端登录后 system prompt 包含 Notion 数据
|
||
- [ ] 铸渊在对话中能说出开发者的真实项目信息,不再说「完全未对接」
|
||
|
||
---
|
||
|
||
## 三、通道 B · 阿里云后端中间层(Phase 2 · 通道A完成后做)
|
||
|
||
### 3.1 目标
|
||
|
||
在阿里云 ECS(`8.155.62.235`)上部署 Node.js 后端服务,**实时**调用 Notion API 和 GitHub API,前端通过后端代理访问数据。
|
||
|
||
### 3.2 架构
|
||
|
||
```
|
||
用户浏览器
|
||
│
|
||
│ HTTPS (guanghulab.com)
|
||
▼
|
||
Nginx(8.155.62.235)
|
||
│
|
||
├── /api/* → 反向代理到 Node.js 后端(localhost:3001)
|
||
│
|
||
└── /* → 静态文件(/var/www/guanghulab/)
|
||
|
||
Node.js 后端(PM2 管理·端口 3001)
|
||
│
|
||
├── GET /api/health → 健康检查
|
||
├── GET /api/dev/:devId → 获取开发者画像(实时查 Notion)
|
||
├── GET /api/databases/:name → 获取数据库数据(实时查 Notion)
|
||
├── GET /api/syslogs → 获取最近 SYSLOG(实时查 Notion)
|
||
├── GET /api/agents → 获取 Agent 注册表(实时查 Notion)
|
||
├── GET /api/tickets → 获取活跃工单(实时查 Notion)
|
||
├── GET /api/repo/status → 获取仓库状态(实时查 GitHub API)
|
||
├── POST /api/chat/context → 组装 system prompt(聚合所有数据源)
|
||
└── POST /api/receipt → 写入指令回执到 Notion
|
||
```
|
||
|
||
### 3.3 后端代码结构
|
||
|
||
创建目录:`backend/api-server/`
|
||
|
||
```
|
||
backend/api-server/
|
||
├── server.js ← 入口 · Express 启动
|
||
├── routes/
|
||
│ ├── dev.js ← /api/dev/:devId
|
||
│ ├── databases.js ← /api/databases/:name
|
||
│ ├── chat.js ← /api/chat/context
|
||
│ ├── receipt.js ← /api/receipt
|
||
│ └── health.js ← /api/health
|
||
├── services/
|
||
│ ├── notion.js ← Notion API 封装(所有读写走这里)
|
||
│ ├── github.js ← GitHub API 封装
|
||
│ └── cache.js ← 内存缓存层(Redis 或简单 Map,避免频繁调 API)
|
||
├── config/
|
||
│ └── databases.js ← Notion 数据库 ID 映射
|
||
├── package.json
|
||
└── ecosystem.config.js ← PM2 配置
|
||
```
|
||
|
||
### 3.4 核心实现要点
|
||
|
||
**server.js(入口):**
|
||
|
||
```jsx
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const app = express();
|
||
|
||
// CORS:只允许自己的域名
|
||
app.use(cors({
|
||
origin: ['https://guanghulab.com', 'http://guanghulab.com',
|
||
'https://qinfendebingshuo.github.io'],
|
||
credentials: true
|
||
}));
|
||
|
||
app.use(express.json());
|
||
|
||
// 路由
|
||
app.use('/api', require('./routes/health'));
|
||
app.use('/api', require('./routes/dev'));
|
||
app.use('/api', require('./routes/databases'));
|
||
app.use('/api', require('./routes/chat'));
|
||
app.use('/api', require('./routes/receipt'));
|
||
|
||
const PORT = process.env.PORT || 3001;
|
||
app.listen(PORT, '127.0.0.1', () => {
|
||
console.log(`🔗 后端中间层启动 · 端口 ${PORT}`);
|
||
});
|
||
```
|
||
|
||
**services/notion.js(Notion API 封装 · 核心):**
|
||
|
||
```jsx
|
||
const { Client } = require('@notionhq/client');
|
||
|
||
const notion = new Client({ auth: process.env.NOTION_TOKEN });
|
||
|
||
// 内存缓存(5分钟有效)
|
||
const cache = new Map();
|
||
const CACHE_TTL = 5 * 60 * 1000;
|
||
|
||
function getCached(key) {
|
||
const item = cache.get(key);
|
||
if (item && Date.now() - item.time < CACHE_TTL) return item.data;
|
||
return null;
|
||
}
|
||
|
||
function setCache(key, data) {
|
||
cache.set(key, { data, time: Date.now() });
|
||
}
|
||
|
||
async function queryDB(dbId, filter, sorts, pageSize = 50) {
|
||
const cacheKey = JSON.stringify({ dbId, filter, sorts, pageSize });
|
||
const cached = getCached(cacheKey);
|
||
if (cached) return cached;
|
||
|
||
const body = { database_id: dbId, page_size: pageSize };
|
||
if (filter) body.filter = filter;
|
||
if (sorts) body.sorts = sorts;
|
||
|
||
const result = await notion.databases.query(body);
|
||
setCache(cacheKey, result);
|
||
return result;
|
||
}
|
||
|
||
async function writeToDB(dbId, properties) {
|
||
return notion.pages.create({
|
||
parent: { database_id: dbId },
|
||
properties
|
||
});
|
||
}
|
||
|
||
module.exports = { notion, queryDB, writeToDB };
|
||
```
|
||
|
||
### 3.5 Nginx 配置更新
|
||
|
||
修改 `/etc/nginx/conf.d/hololake.conf`,添加 API 反向代理:
|
||
|
||
```
|
||
server {
|
||
listen 80;
|
||
server_name guanghulab.com;
|
||
|
||
# 静态文件
|
||
root /var/www/guanghulab/;
|
||
index docs/index.html;
|
||
|
||
# API 反向代理到 Node.js 后端
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:3001;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection 'upgrade';
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_cache_bypass $http_upgrade;
|
||
}
|
||
|
||
# 静态文件缓存
|
||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||
expires 7d;
|
||
add_header Cache-Control "public, immutable";
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.6 PM2 部署
|
||
|
||
```bash
|
||
cd /var/www/guanghulab/backend/api-server/
|
||
npm install
|
||
pm2 start ecosystem.config.js
|
||
pm2 save
|
||
pm2 startup # 开机自启
|
||
```
|
||
|
||
**ecosystem.config.js:**
|
||
|
||
```jsx
|
||
module.exports = {
|
||
apps: [{
|
||
name: 'guanghu-api',
|
||
script: 'server.js',
|
||
instances: 1,
|
||
env: {
|
||
NODE_ENV: 'production',
|
||
PORT: 3001,
|
||
NOTION_TOKEN: '从环境变量读取',
|
||
GITHUB_TOKEN: '从环境变量读取'
|
||
},
|
||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||
error_file: '/var/log/guanghu-api/error.log',
|
||
out_file: '/var/log/guanghu-api/out.log'
|
||
}]
|
||
};
|
||
```
|
||
|
||
### 3.7 GitHub Actions 自动部署后端
|
||
|
||
创建文件:`.github/workflows/deploy-backend.yml`
|
||
|
||
```yaml
|
||
name: "🚀 部署后端中间层到阿里云"
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths: ['backend/api-server/**']
|
||
workflow_dispatch:
|
||
|
||
jobs:
|
||
deploy:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: 部署到阿里云
|
||
uses: appleboy/ssh-action@v1
|
||
with:
|
||
host: 8.155.62.235
|
||
username: root
|
||
key: $ secrets.SSH_PRIVATE_KEY
|
||
script: |
|
||
cd /var/www/guanghulab/backend/api-server/
|
||
git pull origin main
|
||
npm install --production
|
||
pm2 restart guanghu-api || pm2 start ecosystem.config.js
|
||
echo "✅ 后端部署完成 · $(date '+%Y-%m-%d %H:%M')"
|
||
```
|
||
|
||
### 3.8 前端升级·双通道降级策略
|
||
|
||
修改前端,优先走通道B(实时),不可用时降级走通道A(缓存):
|
||
|
||
```jsx
|
||
// 前端 · 双通道数据获取
|
||
const API_BASE = 'https://guanghulab.com/api'; // 通道B
|
||
const CACHE_BASE = 'https://raw.githubusercontent.com/qinfendebingshuo/guanghulab/main/.github/notion-cache'; // 通道A
|
||
|
||
async function getDevProfile(devId) {
|
||
// 优先通道B(实时)
|
||
try {
|
||
const res = await fetch(`${API_BASE}/dev/${devId}`, { timeout: 5000 });
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
data._source = 'realtime'; // 标记数据来源
|
||
return data;
|
||
}
|
||
} catch (e) {
|
||
console.warn('通道B不可用,降级到通道A:', e.message);
|
||
}
|
||
|
||
// 降级通道A(缓存)
|
||
try {
|
||
const res = await fetch(`${CACHE_BASE}/dev-profiles/${devId}.json`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
data._source = 'cache'; // 标记数据来源
|
||
return data;
|
||
}
|
||
} catch (e) {
|
||
console.error('双通道都不可用:', e.message);
|
||
}
|
||
|
||
return null; // 都失败了,降级到纯静态 prompt
|
||
}
|
||
```
|
||
|
||
### 3.9 通道 B 验收标准
|
||
|
||
- [ ] `curl http://8.155.62.235:3001/api/health` 返回 200
|
||
- [ ] `curl https://guanghulab.com/api/health` 通过 Nginx 代理返回 200
|
||
- [ ] `/api/dev/DEV-001` 返回页页的真实 Notion 数据
|
||
- [ ] `/api/databases/agent-registry` 返回 Agent 注册表真实数据
|
||
- [ ] PM2 状态为 online,日志无持续报错
|
||
- [ ] 前端能自动检测通道B可用,显示「实时数据」标记
|
||
- [ ] 通道B断开时,前端自动降级到通道A,显示「缓存数据」标记
|
||
|
||
---
|
||
|
||
## 四、Secrets 需求
|
||
|
||
<aside>
|
||
🔑
|
||
|
||
**GitHub Secrets(仓库已有或需新增):**
|
||
|
||
- `NOTION_TOKEN` — Notion API Token(通道A的 Workflow 和 通道B的后端都需要)
|
||
- `SSH_PRIVATE_KEY` — 阿里云 ECS SSH 私钥(通道B自动部署用)
|
||
|
||
**阿里云服务器环境变量(需人类在 ECS 上配置):**
|
||
|
||
- `NOTION_TOKEN` — 后端中间层读取 Notion 数据用
|
||
- `GITHUB_TOKEN` — 后端中间层读取 GitHub 数据用(可选,公开仓库不需要)
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 五、执行顺序
|
||
|
||
<aside>
|
||
📋
|
||
|
||
**Phase 0 · 天眼全盘扫描 — 最先做,一切之前**
|
||
|
||
1. 启动天眼全仓库扫描
|
||
2. 输出完整扫描报告(功能状态 + 保护清单 + 可操作区域)
|
||
3. 制定安全操作计划,标明每一步不触碰哪些文件
|
||
4. 操作计划回执给霜砚/冰朔确认
|
||
|
||
⚠️ **Phase 0 未完成前,禁止进入 Phase 1/2**
|
||
|
||
**Phase 1 · 通道A(缓存层)— Phase 0 确认后做,1天内完成**
|
||
|
||
1. 创建 `scripts/cache/sync-notion-cache.js`(新文件)
|
||
2. 创建 `.github/workflows/sync-notion-cache.yml`(新文件)
|
||
3. 手动触发 Workflow,确认缓存文件生成
|
||
4. 修改前端(仅新增读取逻辑,不删除现有功能代码)
|
||
5. 验收:交互页面能说出真实开发者信息
|
||
|
||
**Phase 2 · 通道B(后端中间层)— 通道A验收后做,2-3天完成**
|
||
|
||
1. 创建 `backend/api-server/` 全部代码(新目录)
|
||
2. SSH 到阿里云,部署 Node.js + PM2
|
||
3. 更新 Nginx 配置,添加 `/api/` 反向代理(新增 location 块,不改现有 location)
|
||
4. 创建 `.github/workflows/deploy-backend.yml`(新文件)
|
||
5. 修改前端(仅新增双通道逻辑,不删除现有功能代码)
|
||
6. 验收:实时数据 + 降级缓存都正常
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 六、安全红线
|
||
|
||
- ❌ **前端代码中绝不能出现任何 Token**(Notion / GitHub / SSH)
|
||
- ❌ **后端只监听 127.0.0.1**,不对外暴露端口,只通过 Nginx 代理
|
||
- ❌ **CORS 白名单只允许自己的域名**
|
||
- ❌ **数据隔离**:DEV-001 的 API 只返回 DEV-001 的数据,不返回其他开发者的
|
||
- ✅ **GitHub Secrets 保管所有敏感信息**
|
||
- ✅ **内存缓存 5 分钟过期**,防止 API 配额超限
|
||
|
||
---
|
||
|
||
## 七、回执要求
|
||
|
||
铸渊执行完成后,分两次回执:
|
||
|
||
**通道A完成时:**
|
||
|
||
```
|
||
[ZY-DUALCHANNEL-RECEIPT-A]
|
||
指令编号: ZY-DUALCHANNEL-2026-0325-001
|
||
通道: A · 缓存层
|
||
Workflow运行: ✅/❌
|
||
缓存文件生成: ✅/❌(列出文件数)
|
||
前端注入: ✅/❌
|
||
数据准确性: ✅/❌
|
||
```
|
||
|
||
**通道B完成时:**
|
||
|
||
```
|
||
[ZY-DUALCHANNEL-RECEIPT-B]
|
||
指令编号: ZY-DUALCHANNEL-2026-0325-001
|
||
通道: B · 后端中间层
|
||
PM2状态: ✅/❌
|
||
Nginx代理: ✅/❌
|
||
API响应: ✅/❌(列出可用接口)
|
||
降级策略: ✅/❌
|
||
```
|
||
|
||
---
|
||
|
||
> 🔗 **签发**:霜砚(AG-SY-01)
|
||
>
|
||
|
||
> 👤 **授权**:冰朔(TCS-0002∞)
|
||
>
|
||
|
||
> 📅 **日期**:2026-03-25
|
||
>
|
||
|
||
> 📌 **版权**:国作登字-2026-A-00037559
|
||
> |